How to Add License Key Validation to Your Spring Boot Application
Spring Boot is the de facto standard for enterprise Java applications. When you need to monetize your Spring Boot APIs, microservices, or SaaS products, license key validation provides a clean, scalable solution. This guide walks you through a complete integration with Traffic Orchestrator.
Why License Keys in Spring Boot?
Spring Boot powers enterprise-grade applications that need:
- Per-tenant licensing in multi-tenant architectures
- Feature-flagged access based on subscription tier
- API monetization with usage-based or seat-based pricing
- Offline verification for on-premise deployments
Step 1: Add Dependencies
Add the required dependencies to your pom.xml:
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
Step 2: Create the License Client Service
Use WebClient for non-blocking license validation:
// LicenseService.java
@Service
public class LicenseService {
private final WebClient webClient;
public LicenseService(@Value("${to.base-url:https://api.trafficorchestrator.com/api/v1}") String baseUrl) { this.webClient = WebClient.builder() .baseUrl(baseUrl) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); }
public Mono<LicenseResult> validate(String key, String domain) { return webClient.post() .uri("/validate") .bodyValue(Map.of("key", key, "domain", domain)) .retrieve() .bodyToMono(LicenseResult.class) .onErrorReturn(new LicenseResult(false, null, List.of(), "Service unavailable")); } }
public record LicenseResult( boolean valid, String key, List<String> features, String error ) {} ```
Step 3: Create a License Validation Filter
A servlet filter enforces licensing across all requests:
// LicenseFilter.java
@Component
@Order(1)
public class LicenseFilter extends OncePerRequestFilter {
private final LicenseService licenseService;
public LicenseFilter(LicenseService licenseService) { this.licenseService = licenseService; }
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String licenseKey = request.getHeader("X-License-Key");
if (licenseKey == null || licenseKey.isBlank()) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); response.getWriter().write("{\"error\": \"License key required\"}"); return; }
LicenseResult result = licenseService.validate(licenseKey, request.getServerName()).block();
if (result == null || !result.valid()) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json"); response.getWriter().write("{\"error\": \"Invalid license\"}"); return; }
request.setAttribute("license", result); chain.doFilter(request, response); }
@Override protected boolean shouldNotFilter(HttpServletRequest request) { // Skip validation for public endpoints String path = request.getRequestURI(); return path.startsWith("/public") || path.equals("/health"); } } ```
Step 4: Custom Annotation for Feature Gating
Create a custom annotation to protect specific endpoints:
// RequireLicense.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireLicense {
String feature() default "";
}
// LicenseAspect.java @Aspect @Component public class LicenseAspect { @Around("@annotation(requireLicense)") public Object checkLicense(ProceedingJoinPoint joinPoint, RequireLicense requireLicense) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); LicenseResult license = (LicenseResult) request.getAttribute("license");
if (license == null || !license.valid()) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Valid license required"); }
String feature = requireLicense.feature(); if (!feature.isEmpty() && !license.features().contains(feature)) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Feature not included in your plan"); }
return joinPoint.proceed(); } } ```
Use it on controller methods:
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/data") @RequireLicense public ResponseEntity<Map<String, Object>> getData(HttpServletRequest request) { LicenseResult license = (LicenseResult) request.getAttribute("license"); return ResponseEntity.ok(Map.of("data", "premium content", "plan", license.key())); }
@GetMapping("/analytics") @RequireLicense(feature = "analytics") public ResponseEntity<Map<String, Object>> getAnalytics() { return ResponseEntity.ok(Map.of("analytics", Map.of("views", 12345))); } } ```
Step 5: Webhook Handler
Process real-time license events:
// WebhookController.java
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
@Value("${to.webhook-secret}") private String webhookSecret;
@PostMapping("/license") public ResponseEntity<Map<String, Boolean>> handleWebhook( @RequestHeader("X-TO-Signature") String signature, @RequestHeader("X-TO-Timestamp") String timestamp, @RequestBody String body) {
// Reject stale webhooks long ts = Long.parseLong(timestamp); if (System.currentTimeMillis() / 1000 - ts > 300) { return ResponseEntity.status(401).body(Map.of("received", false)); }
// Verify HMAC String expected = HmacUtils.hmacSha256Hex(webhookSecret, timestamp + "." + body); if (!MessageDigest.isEqual(expected.getBytes(), signature.getBytes())) { return ResponseEntity.status(401).body(Map.of("received", false)); }
// Process event return ResponseEntity.ok(Map.of("received", true)); } } ```
Step 6: Configuration
Add configuration to application.yml:
# application.yml
to:
base-url: https://api.trafficorchestrator.com/api/v1
api-key: ${TO_API_KEY}
webhook-secret: ${TO_WEBHOOK_SECRET}
Next Steps
- Browse the REST API documentation for all available endpoints
- Set up webhook integration for event-driven licensing
- Learn about domain binding for SaaS protection
- Explore offline verification with Ed25519 signatures
Traffic Orchestrator's REST API is framework-agnostic — it works with Spring Boot, Quarkus, Micronaut, or any Java HTTP client. Edge validation completes in under 10ms, adding negligible latency to your API responses.
Ship licensing in your next release
5 licenses, 500 validations/month, full API access. Set up in under 5 minutes — no credit card required.