Java (Spring Boot) 実装例
サンプルコードリポジトリリンク
設定
コールバック応答の生成に必要なセキュリティキーとカスタムキー(Custom key)は、ソースコードにハードコーディングせず、application-local.yaml設定ファイルを介してインジェクト(注入)されるよう構成します。
リポジトリのapplication-local.yaml.exampleファイルをコピーし、実際に発行されたキーの値に置き換えて使用してください。
kollus:
security-key: "YOUR_SECURITY_KEY" # Kollus VOD コンソール > [設定] > [セキュリティキー]
custom-key: "YOUR_CUSTOM_KEY" # Kollus VOD コンソール > [設定] > [カスタムキー]
JWT署名の実装
外部のJWTライブラリを使用せず、標準のJavaセキュリティAPIを活用してHS256(HMAC-SHA256)ベースでコールバック応答JWTに署名を行うサービスです。(プレーヤートークン生成など、コールバックに直接関連しないメソッドは省略されています。)
package com.example.kollus.service;
import org.springframework.stereotype.Service;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Map;
/**
* KollusプレーヤーJWTトークンの生成および署名ユーティリティです。
* 外部JWTライブラリを使用せず、HS256(HMAC-SHA256)アルゴリズムを使用してKollusコールバック応答Payloadを直接エンコードおよび署名します。
*/
@Service
public class KollusTokenService {
/** HMAC-SHA256署名アルゴリズム識別子 */
private static final String HMAC_ALGORITHM = "HmacSHA256";
/** 固定JWTヘッダー仕様 (typ: JWT, alg: HS256) */
private static final Map<String, Object> JWT_HEADER = Map.of("typ", "JWT", "alg", "HS256");
private final ObjectMapper objectMapper;
private final Base64.Encoder base64Url = Base64.getUrlEncoder().withoutPadding();
public KollusTokenService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
/**
* Kollusコールバック応答用JWTをHS256アルゴリズムで署名します。
*
* @param payload JWT Payloadオブジェクト
* @param securityKey HMAC署名に使用するセキュリティキー
* @return Base64URLでエンコードされた署名済みJWT文字列
*/
public String signCallbackJwt(Object payload, String securityKey) {
return encodeJwt(payload, securityKey);
}
/**
* {@code header.payload.signature} 形式の標準JWT文字列を生成します。
*/
private String encodeJwt(Object payload, String securityKey) {
String header = base64UrlJson(JWT_HEADER);
String body = base64UrlJson(payload);
String content = header + "." + body;
String signature = sign(content, securityKey);
return content + "." + signature;
}
/**
* オブジェクトをJSONにシリアライズした後、Base64URL(パディングなし)形式でエンコードします。
*/
private String base64UrlJson(Object value) {
try {
byte[] json = objectMapper.writeValueAsBytes(value);
return base64Url.encodeToString(json);
} catch (JacksonException e) {
// シリアライズ失敗はランタイム不具合のため、非チェック例外(IllegalStateException)でラップして送出
throw new IllegalStateException("JWT payloadシリアライズ失敗", e);
}
}
/**
* HMAC-SHA256アルゴリズムで本文に署名し、Base64URL文字列として返します。
*/
private String sign(String content, String securityKey) {
try {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
mac.init(new SecretKeySpec(securityKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM));
byte[] raw = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
return base64Url.encodeToString(raw);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
// HmacSHA256は標準JVMで必須サポートされているアルゴリズムのため、正常なランタイム環境では発生しません
throw new IllegalStateException("JWT署名失敗", e);
}
}
}
DRMダウンロードコールバックコントローラの実装
Kollusサーバーから送信されるPOST /callback/drmリクエストのitemsパラメータ(JSON配列文字列)を受信して各項目ごとの権限応答データを構成し、これをdata配列に格納して署名済みJWTとして返却するコントローラです。(その他のコールバック処理メソッドは省略されています。)
package com.example.kollus.controller;
import com.example.kollus.config.KollusProperties;
import com.example.kollus.service.KollusTokenService;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import tools.jackson.core.JacksonException;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Kollusコールバック処理コントローラです。
* 再生(Play)、DRM、次回エピソード(Next Episode)、LMSコールバックリクエストを受信し、仕様に準拠したレスポンスを返却します。
*/
@RestController
@RequestMapping("/callback")
public class CallbackController {
/** DRMコンテンツの有効期限 (30日、秒単位) */
private static final long CONTENT_EXPIRE_SEC = 60L * 60 * 24 * 30;
/** Kollusコールバック応答ヘッダー名 (カスタムキー伝達用) */
private static final String USERKEY_HEADER = "X-KOLLUS-USERKEY";
/** コールバック種別 1: 承認および有効期限の設定 (DRMコールバック: ダウンロード承認および有効期限設定) */
private static final int KIND_GRANT_WITH_EXPIRATION = 1;
/** コールバック種別 2: ダウンロード完了通知 (DRMコールバック専用) */
private static final int KIND_DOWNLOAD_COMPLETED = 2;
/** コールバック種別 3: オフライン再生権限の最終確認 */
private static final int KIND_PLAYBACK_APPROVAL = 3;
private final KollusTokenService tokenService;
private final KollusProperties properties;
private final ObjectMapper objectMapper;
public CallbackController(KollusTokenService tokenService,
KollusProperties properties,
ObjectMapper objectMapper) {
this.tokenService = tokenService;
this.properties = properties;
this.objectMapper = objectMapper;
}
/**
* POST /callback/drm — DRMダウンロードおよびオフライン再生権限コールバックリクエストを処理します。
* リクエストボディのitemsパラメータ(JSON配列文字列)をパースして各コンテンツごとの応答データを構成し、JWTとして返却します。
*
* @param itemsJson itemsパラメータ (kind, media_content_key, start_atなどの項目を含むJSON配列文字列)
* @return X-KOLLUS-USERKEYヘッダーおよびdata配列を含む署名済みJWTボディ (text/plain)、パース失敗時は400 Bad Request
*/
@PostMapping("/drm")
public ResponseEntity<String> drm(@RequestParam(value = "items", required = false) String itemsJson) {
List> items;
try {
items = objectMapper.readValue(itemsJson, new TypeReference<>() {
});
} catch (JacksonException | IllegalArgumentException e) {
// itemsパラメータのデシリアライズ失敗時は400 Bad Requestを返却
return ResponseEntity.badRequest().body("Invalid items");
}
long now = Instant.now().getEpochSecond();
List> data = new ArrayList<>();
for (Map<String, Object> item : items) {
Integer kind = parseIntOrNull(String.valueOf(item.get("kind")));
Object mediaContentKey = item.get("media_content_key");
if (kind == null) {
continue;
}
Map<String, Object> entry = new LinkedHashMap<>();
if (kind == KIND_GRANT_WITH_EXPIRATION) {
// コールバック種別 1: ダウンロード承認および再生有効期限の設定
entry.put("kind", KIND_GRANT_WITH_EXPIRATION);
entry.put("media_content_key", mediaContentKey);
entry.put("expiration_date", now + CONTENT_EXPIRE_SEC);
entry.put("result", 1);
data.add(entry);
} else if (kind == KIND_DOWNLOAD_COMPLETED) {
// コールバック種別 2: ダウンロード完了通知の受信確認
entry.put("kind", KIND_DOWNLOAD_COMPLETED);
entry.put("media_content_key", mediaContentKey);
entry.put("result", 1);
data.add(entry);
} else if (kind == KIND_PLAYBACK_APPROVAL) {
// コールバック種別 3: オフライン再生権限の最終確認
entry.put("kind", KIND_PLAYBACK_APPROVAL);
entry.put("media_content_key", mediaContentKey);
entry.put("start_at", item.get("start_at"));
entry.put("result", 1);
data.add(entry);
}
}
return callbackJwt(Map.of("data", data));
}
/** Kollusコールバック仕様に従い、X-KOLLUS-USERKEYヘッダーと署名済みJWTボディで構成されたレスポンスを返却します。 */
private ResponseEntity<String> callbackJwt(Object payload) {
String jwt = tokenService.signCallbackJwt(payload, properties.securityKey());
return ResponseEntity.ok()
.header(USERKEY_HEADER, properties.customKey())
.contentType(MediaType.TEXT_PLAIN)
.body(jwt);
}
/** 文字列を整数にパースし、無効な形式または失敗時はnullを返却します。 */
private Integer parseIntOrNull(String value) {
if (value == null) {
return null;
}
try {
return Integer.parseInt(value.trim());
} catch (NumberFormatException e) {
// 数値パース失敗時はnullを返し、該当項目の処理を安全にスキップ
return null;
}
}
}