API Security Practices: Tokens, Timestamps, Signatures, and Anti‑Replay Measures with Java Examples
This article explains common API security mechanisms such as token and user token usage, timestamp validation, signature generation, anti‑replay strategies, DoS attack types, and provides Java Spring Boot code examples for token handling, request interception, custom annotations, and ThreadLocal utilities.
The article discusses how to secure data exchange between third‑party systems beyond HTTPS by introducing a set of common API security mechanisms, including tokens, timestamps, signatures, and anti‑replay techniques.
1. Token Overview – An access token identifies the caller and reduces the need to transmit usernames and passwords. API tokens are used for public endpoints, while user tokens are obtained after login for protected resources. Tokens are typically UUIDs stored in Redis with an expiration time.
2. Timestamp Overview – The client sends the current timestamp with each request. The server checks that the request time difference is within an acceptable window (e.g., 5 minutes) to mitigate DoS attacks that replay old requests.
3. Signature Overview – A signature (sign) is generated by concatenating all non‑empty request parameters (sorted by name), the token, timestamp, nonce, and a secret key, then applying an MD5 hash. The server recomputes the signature to verify request integrity.
4. Preventing Duplicate Submissions – A custom @NotRepeatSubmit annotation marks methods that must not be called repeatedly within a short period. The interceptor stores the signature in Redis with a configurable expiration; subsequent identical requests are rejected.
5. Usage Flow
Client obtains appId and key from the server.
Client calls /api/token/api_token with appId , timestamp , and sign to receive an API token.
For public APIs, the client uses the API token; for protected APIs, it logs in to receive a user token.
6. Example Code
Dependency configuration (Maven):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>Redis configuration:
@Configuration
public class RedisConfiguration {
@Bean
public JedisConnectionFactory jedisConnectionFactory(){
return new JedisConnectionFactory();
}
@Bean
public RedisTemplate
redisTemplate(){
RedisTemplate
redisTemplate = new StringRedisTemplate();
redisTemplate.setConnectionFactory(jedisConnectionFactory());
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}Token controller (simplified):
@RestController
@RequestMapping("/api/token")
public class TokenController {
@Autowired
private RedisTemplate redisTemplate;
@PostMapping("/api_token")
public ApiResponse
apiToken(String appId, @RequestHeader("timestamp") String timestamp, @RequestHeader("sign") String sign) {
Assert.isTrue(!StringUtils.isEmpty(appId) && !StringUtils.isEmpty(timestamp) && !StringUtils.isEmpty(sign), "参数错误");
long reqeustInterval = System.currentTimeMillis() - Long.valueOf(timestamp);
Assert.isTrue(reqeustInterval < 5 * 60 * 1000, "请求过期,请重新请求");
AppInfo appInfo = new AppInfo("1", "12345678954556");
String signString = timestamp + appId + appInfo.getKey();
String signature = MD5Util.encode(signString);
Assert.isTrue(signature.equals(sign), "签名错误");
AccessToken accessToken = this.saveToken(0, appInfo, null);
return ApiResponse.success(accessToken);
}
@NotRepeatSubmit(5000)
@PostMapping("user_token")
public ApiResponse
userToken(String username, String password) {
// authentication logic omitted for brevity
AppInfo appInfo = new AppInfo("1", "12345678954556");
AccessToken accessToken = this.saveToken(1, appInfo, new UserInfo(username, "hashedPwd", "salt"));
return ApiResponse.success(userInfo);
}
private AccessToken saveToken(int tokenType, AppInfo appInfo, UserInfo userInfo) {
String token = UUID.randomUUID().toString();
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, 7200);
Date expireTime = calendar.getTime();
TokenInfo tokenInfo = new TokenInfo();
tokenInfo.setTokenType(tokenType);
tokenInfo.setAppInfo(appInfo);
if (tokenType == 1) tokenInfo.setUserInfo(userInfo);
redisTemplate.opsForValue().set(token, tokenInfo, 7200, TimeUnit.SECONDS);
return new AccessToken(token, expireTime);
}
}Token interceptor that validates token, timestamp, signature and duplicate submissions:
@Component
public class TokenInterceptor extends HandlerInterceptorAdapter {
@Autowired
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
String timestamp = request.getHeader("timestamp");
String nonce = request.getHeader("nonce");
String sign = request.getHeader("sign");
Assert.isTrue(!StringUtils.isEmpty(token) && !StringUtils.isEmpty(timestamp) && !StringUtils.isEmpty(sign), "参数错误");
NotRepeatSubmit notRepeatSubmit = ApiUtil.getNotRepeatSubmit(handler);
long expireTime = notRepeatSubmit == null ? 5 * 60 * 1000 : notRepeatSubmit.value();
long reqeustInterval = System.currentTimeMillis() - Long.valueOf(timestamp);
Assert.isTrue(reqeustInterval < expireTime, "请求超时,请重新请求");
TokenInfo tokenInfo = redisTemplate.opsForValue().get(token);
Assert.notNull(tokenInfo, "token错误");
String signString = ApiUtil.concatSignString(request) + tokenInfo.getAppInfo().getKey() + token + timestamp + nonce;
String signature = MD5Util.encode(signString);
Assert.isTrue(signature.equals(sign), "签名错误");
if (notRepeatSubmit != null) {
ValueOperations
signRedis = redisTemplate.opsForValue();
Assert.isTrue(!redisTemplate.hasKey(sign), "请勿重复提交");
signRedis.set(sign, 0, expireTime, TimeUnit.MILLISECONDS);
}
return super.preHandle(request, response, handler);
}
}Utility classes (MD5Util, ApiUtil, ThreadLocalUtil) are provided to perform hashing, signature string concatenation, annotation retrieval, and per‑thread storage of contextual data.
7. ThreadLocal Usage – ThreadLocal stores request‑scoped data (e.g., the current user) without passing it through method parameters. The ThreadLocalUtil class offers set , get , and remove methods for managing such data.
Conclusion – The article presents a practical set of security measures commonly used in third‑party API integrations, covering token management, timestamp checks, signature verification, DoS mitigation, duplicate‑submission protection, and thread‑local context handling, with complete Java Spring Boot examples.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.