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);
}
}
}
DRM download callback controller implementation
This controller receives the POST /callback/drm request with the items parameter (a JSON array string) from the Kollus server, constructs authorization response data for each item, and returns it as a signed JWT wrapped in a data array. (Other callback handling methods 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 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;
/**
* Controller for handling Kollus callbacks.
* Receives Play, DRM, Next Episode, and LMS callback requests and returns compliant responses.
*/
@RestController
@RequestMapping("/callback")
public class CallbackController {
/** DRM content 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 (DRM callback: download approval & expiration setup) */
private static final int KIND_GRANT_WITH_EXPIRATION = 1;
/** Callback kind 2: Download completed notification (DRM callback exclusive) */
private static final int KIND_DOWNLOAD_COMPLETED = 2;
/** Callback kind 3: Final confirmation of offline playback permission */
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 — Handles DRM download and offline playback permission callback requests.
* Parses the items parameter (JSON array string) in the request body to construct response data for each content item, returning it as a JWT.
*
* @param itemsJson items parameter (JSON array string containing kind, media_content_key, start_at, etc.)
* @return X-KOLLUS-USERKEY header and signed JWT body (text/plain) containing the data array; 400 Bad Request on parse failure
*/
@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) {
// Return 400 Bad Request if items parameter deserialization fails
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) {
// Callback kind 1: Approve download and set playback expiration timestamp
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) {
// Callback kind 2: Acknowledge download completion notification
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) {
// Callback kind 3: Final confirmation of offline playback permission
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));
}
/** 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 to safely skip processing this item
return null;
}
}
}