java
53 lines · 10 steps
Building an API key rotation filter in Spring
A once-per-request servlet filter that authenticates API keys, caches lookups, and signals clients when their key is due for rotation.
Explained by
highlit
1@Component
2public class ApiKeyRotationFilter extends OncePerRequestFilter {
3
4 private static final String API_KEY_HEADER = "X-Api-Key";
5 private static final Duration ROTATION_WINDOW = Duration.ofDays(7);
6
7 private final ApiKeyRepository apiKeyRepository;
8 private final Cache keyCache;
9
10 public ApiKeyRotationFilter(ApiKeyRepository apiKeyRepository, CacheManager cacheManager) {
11 this.apiKeyRepository = apiKeyRepository;
12 this.keyCache = cacheManager.getCache("apiKeys");
13 }
14
15 @Override
16 protected boolean shouldNotFilter(HttpServletRequest request) {
17 return request.getServletPath().startsWith("/actuator");
18 }
19
20 @Override
21 protected void doFilterInternal(HttpServletRequest request,
22 HttpServletResponse response,
23 FilterChain chain) throws ServletException, IOException {
24
25 String presented = request.getHeader(API_KEY_HEADER);
26 if (!StringUtils.hasText(presented)) {
27 response.sendError(HttpStatus.UNAUTHORIZED.value(), "Missing API key");
28 return;
29 }
30
31 ApiKey key = keyCache.get(presented, () -> apiKeyRepository.findByHashedValue(sha256(presented)).orElse(null));
32
33 if (key == null || key.isRevoked()) {
34 response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid API key");
35 return;
36 }
37
38 Instant age = key.getRotatedAt().plus(ROTATION_WINDOW);
39 if (Instant.now().isAfter(age)) {
40 response.setHeader("X-Api-Key-Rotation", "required");
41 response.setHeader("X-Api-Key-Expired-At", age.toString());
42 keyCache.evict(presented);
43 } else if (Instant.now().isAfter(age.minus(Duration.ofDays(2)))) {
44 response.setHeader("X-Api-Key-Rotation", "pending");
45 }
46
47 chain.doFilter(request, response);
48 }
49
50 private static String sha256(String value) {
51 return DigestUtils.sha256Hex(value.getBytes(StandardCharsets.UTF_8));
52 }
53}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A OncePerRequestFilter is the natural place to enforce cross-cutting authentication before requests reach controllers.
- 2Caching hashed-key lookups keeps per-request database hits low while still allowing eviction on revocation or expiry.
- 3Advisory response headers let a server nudge clients toward rotation without hard-failing valid-but-aging credentials.
Related explainers
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
ruby
class LogAggregator BUCKET_FORMAT = "%Y-%m-%dT%H:%M" def initialize(entries)
Bucketing log entries by the minute in Ruby
aggregation
hashing
enumerable
Intermediate
5 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-an-api-key-rotation-filter-in-spring-explained-java-dc7c/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.