Java (Spring Boot) Implementation
This document is a machine-translated draft and is currently undergoing review. Some content may be inaccurate or differ from the original Korean version. For the most precise information, refer to the Korean documentation.
Example code repository link
Configuration
Configure the security key and custom key required for generating callback responses to be injected via the application-local.yaml configuration file rather than hardcoded in the source code.
Copy the repository's application-local.yaml.example file and replace the placeholders with your actual issued key values before use.
kollus:
security-key: "YOUR_SECURITY_KEY" # Kollus VOD Console > [Settings] > [Security Key]
custom-key: "YOUR_CUSTOM_KEY" # Kollus VOD Console > [Settings] > [Custom Key]
JWT signing implementation
This service signs callback response JWTs based on HS256 (HMAC-SHA256) using standard Java security APIs without requiring external JWT libraries. (Methods unrelated to callbacks, such as player token generation, have been omitted.)
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 player JWT token generation and signing utility.
* Encodes and signs Kollus callback response payloads directly using the HS256 (HMAC-SHA256) algorithm without external JWT libraries.
*/
@Service
public class KollusTokenService {
/** HMAC-SHA256 signature algorithm identifier */
private static final String HMAC_ALGORITHM = "HmacSHA256";
/** Fixed JWT header specification (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;
}
/**
* Signs the JWT for Kollus callback responses using the HS256 algorithm.
*
* @param payload JWT payload object
* @param securityKey Security key used for HMAC signing
* @return Base64URL-encoded signed JWT string
*/
public String signCallbackJwt(Object payload, String securityKey) {
return encodeJwt(payload, securityKey);
}
/**
* Generates a standard JWT string in the format {@code header.payload.signature}.
*/
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;
}
/**
* Serializes an object into JSON and encodes it into Base64URL format (without padding).
*/
private String base64UrlJson(Object value) {
try {
byte[] json = objectMapper.writeValueAsBytes(value);
return base64Url.encodeToString(json);
} catch (JacksonException e) {
// Serialization failure is a runtime defect; rethrow wrapped as an unchecked exception (IllegalStateException)
throw new IllegalStateException("JWT payload serialization failed", e);
}
}
/**
* Signs the content using the HMAC-SHA256 algorithm and returns it as a Base64URL string.
*/
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 is mandatory across standard JVMs; this should not occur in a normal runtime environment
throw new IllegalStateException("JWT signing failed", e);
}
}
}
Play callback controller implementation
This controller receives the POST /callback/play request from the Kollus server and generates and returns an appropriate response JWT based on the request type (kind). (Other callback handling methods, such as DRM and LMS, have been omitted.)
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;
/**
* Controller for handling Kollus callbacks.
* Receives Play, DRM, Next Episode, and LMS callback requests and returns compliant responses.
*/
@RestController
@RequestMapping("/callback")
public class CallbackController {
/** Callback response JWT expiration time (600 seconds / 10 minutes) — short expiration time is maintained for permission verification tokens */
private static final int CALLBACK_JWT_EXPIRE_SEC = 600;
/** Content playback expiration period (30 days, in seconds) */
private static final long CONTENT_EXPIRE_SEC = 60L * 60 * 24 * 30;
/** Kollus callback response header name (for passing custom key) */
private static final String USERKEY_HEADER = "X-KOLLUS-USERKEY";
/** Callback kind 1: Grant & set expiration (Play callback: set playback expiration, DRM callback: approve download) */
private static final int KIND_GRANT_WITH_EXPIRATION = 1;
/** Callback kind 3: Final confirmation of playback permission (Play callback: final playback approval, DRM callback: confirm offline playback permission) */
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 — Handles play callback requests.
* kind 1: Set playback expiration timestamp (returns expiration_date), kind 3: Final playback approval (returns result)
*
* @param kindParam Callback request type identifier (1: set playback expiration, 3: final playback approval)
* @return X-KOLLUS-USERKEY header and signed JWT body (text/plain); 400 Bad Request if kind is undefined
*/
@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");
}
/** Combines payload data and expiration timestamp (exp) to create a callback payload map. */
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;
}
/** Returns a response consisting of the X-KOLLUS-USERKEY header and a signed JWT body according to Kollus callback specifications. */
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);
}
/** Converts a string to an integer, returning null if the format is invalid or conversion fails. */
private Integer parseIntOrNull(String value) {
if (value == null) {
return null;
}
try {
return Integer.parseInt(value.trim());
} catch (NumberFormatException e) {
// Return null on numeric parsing failure so the caller branches to 'unknown kind (400 Bad Request)'
return null;
}
}
}