メインコンテンツまでスキップ

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);
}
}
}

プレイコールバックコントローラの実装​

Kollusサーバーから送信される POST /callback/playリクエストを受信し、リクエスト種別(kind)に応じて適切な応答JWTを生成して返却するコントローラです。(DRM、LMSなど他のコールバック処理メソッドは省略されています。)

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 java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* Kollusコールバック処理コントローラです。
* 再生(Play)、DRM、次回エピソード(Next Episode)、LMSコールバックリクエストを受信し、仕様に準拠したレスポンスを返却します。
*/
@RestController
@RequestMapping("/callback")
public class CallbackController {

/** コールバック応答JWTの有効時間 (600秒 / 10分) — 権限検証用トークンは短い有効期限を維持 */
private static final int CALLBACK_JWT_EXPIRE_SEC = 600;
/** コンテンツ再生の有効期限 (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;
/** コールバック種別 3: 再生権限の最終確認 (再生コールバック: 最終再生承認、DRMコールバック: オフライン再生権限確認) */
private static final int KIND_PLAYBACK_APPROVAL = 3;

private final KollusTokenService tokenService;
private final KollusProperties properties;

public CallbackController(KollusTokenService tokenService,
KollusProperties properties) {
this.tokenService = tokenService;
this.properties = properties;
}

/**
* POST /callback/play — 再生コールバックリクエストを処理します。
* kind 1: 再生有効期限の設定 (expiration_dateを返却)、kind 3: 最終再生承認 (resultを返却)
*
* @param kindParam コールバックリクエスト種別の識別値 (1: 再生有効期限設定、3: 最終再生承認)
* @return X-KOLLUS-USERKEYヘッダーおよび署名済みJWTボディ (text/plain)、未定義のkindの場合は400 Bad Request
*/
@PostMapping("/play")
public ResponseEntity<String> play(@RequestParam(value = "kind", required = false) String kindParam) {
Integer kind = parseIntOrNull(kindParam);
long now = Instant.now().getEpochSecond();

if (kind != null && kind == KIND_GRANT_WITH_EXPIRATION) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("expiration_date", now + CONTENT_EXPIRE_SEC);
data.put("result", 1);
return callbackJwt(payloadWithExp(data, now));
} else if (kind != null && kind == KIND_PLAYBACK_APPROVAL) {
return callbackJwt(payloadWithExp(Map.of("result", 1), now));
}
return ResponseEntity.badRequest().body("Unknown kind");
}

/** payloadデータと有効期限(exp)を結合してコールバックpayloadマップを生成します。 */
private Map<String, Object> payloadWithExp(Map<String, Object> data, long now) {
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("data", data);
payload.put("exp", now + CALLBACK_JWT_EXPIRE_SEC);
return payload;
}

/** 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を返し、呼び出し元が「不明なkind (400 Bad Request)」として処理するよう分岐
return null;
}
}
}