API Security Practices: Token, Timestamp, Signature, and Replay Protection in Java
This article explains common API security mechanisms such as token, timestamp, signature, and anti‑replay techniques, demonstrates how to prevent duplicate submissions, and provides complete Java Spring Boot code examples including interceptors, Redis configuration, and utility classes for secure API design.
Token Overview
Token (access token) is used to identify the caller of an API, reducing the need to transmit username and password repeatedly. The client obtains an appId and a key from the server; the key is used for request signing and must be stored securely on the client side.
Tokens are usually UUIDs stored in a cache server (e.g., Redis). When a request arrives, the server checks the token in the cache; if present, the request proceeds, otherwise an error is returned. Two token types are described:
API Token: used for endpoints that do not require user login (e.g., login, registration). It is obtained by sending appId, timestamp and sign where sign = encrypt(timestamp + key).
User Token: used for endpoints that require a logged‑in user; obtained by sending username and password.
Token validity can be one‑time or time‑limited depending on business needs. Using HTTPS is recommended; if HTTP is used, token alone only reduces risk but cannot fully prevent attacks.
Timestamp Overview
Timestamp is the current time sent by the client and is used to mitigate DoS attacks. The server compares the request timestamp with its own time; if the difference exceeds a configured threshold (e.g., 5 minutes), the request is rejected. Timestamp alone cannot stop DoS, but combined with a signature it helps.
Signature (sign) Overview
Signature prevents parameter tampering. The client generates a random nonce (typically a 6‑character alphanumeric string) and concatenates all non‑empty parameters in ascending order, then appends token, key, timestamp, and nonce. The concatenated string is hashed (e.g., MD5) to produce sign. The server recomputes the signature and compares it with the received sign; a mismatch indicates illegal modification.
Preventing Duplicate Submissions
For non‑idempotent operations, the first request stores its sign as a key in Redis with an expiration equal to the request timeout. Subsequent requests with the same sign are rejected, ensuring that replayed or duplicate submissions are blocked.
Usage Flow
Client requests an API account from the server and receives appId and key.
Client sends appId, timestamp, and sign (where sign = encrypt(appId + timestamp + key)) to obtain an API token.
Client uses the API token to call public endpoints.
For user‑protected endpoints, the client logs in with username and password, receives a user token, and uses it for subsequent calls.
Example Code
Below are the essential Java Spring Boot classes used in the implementation.
dependency
<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>RedisConfiguration
@Configuration
public class RedisConfiguration {
@Bean
public JedisConnectionFactory jedisConnectionFactory(){
return new JedisConnectionFactory();
}
/**
* Support storing objects
*/
@Bean
public RedisTemplate<String, String> redisTemplate(){
RedisTemplate<String, String> 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;
}
}TokenController
@Slf4j
@RestController
@RequestMapping("/api/token")
public class TokenController {
@Autowired
private RedisTemplate redisTemplate;
@PostMapping("/api_token")
public ApiResponse<AccessToken> apiToken(String appId, @RequestHeader("timestamp") String timestamp, @RequestHeader("sign") String sign){
Assert.isTrue(!StringUtils.isEmpty(appId) && !StringUtils.isEmpty(timestamp) && !StringUtils.isEmpty(sign), "参数错误");
long requestInterval = System.currentTimeMillis() - Long.valueOf(timestamp);
Assert.isTrue(requestInterval < 5*60*1000, "请求过期,请重新请求");
// 1. Query app secret
AppInfo appInfo = new AppInfo("1", "12345678954556");
// 2. Verify signature
String signString = timestamp + appId + appInfo.getKey();
String signature = MD5Util.encode(signString);
Assert.isTrue(signature.equals(sign), "签名错误");
// 3. Generate token and store in Redis
AccessToken accessToken = this.saveToken(0, appInfo, null);
return ApiResponse.success(accessToken);
}
@NotRepeatSubmit(5000)
@PostMapping("user_token")
public ApiResponse<UserInfo> userToken(String username, String password){
UserInfo userInfo = new UserInfo(username, "81255cb0dca1a5f304328a70ac85dcbd", "111111");
String pwd = password + userInfo.getSalt();
String passwordMD5 = MD5Util.encode(pwd);
Assert.isTrue(passwordMD5.equals(userInfo.getPassword()), "密码错误");
AppInfo appInfo = new AppInfo("1", "12345678954556");
AccessToken accessToken = this.saveToken(1, appInfo, userInfo);
userInfo.setAccessToken(accessToken);
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();
ValueOperations<String, TokenInfo> operations = redisTemplate.opsForValue();
TokenInfo tokenInfo = new TokenInfo();
tokenInfo.setTokenType(tokenType);
tokenInfo.setAppInfo(appInfo);
if(tokenType == 1){
tokenInfo.setUserInfo(userInfo);
}
operations.set(token, tokenInfo, 7200, TimeUnit.SECONDS);
return new AccessToken(token, expireTime);
}
public static void main(String[] args){
long timestamp = System.currentTimeMillis();
System.out.println(timestamp);
String signString = timestamp + "1" + "12345678954556";
String sign = MD5Util.encode(signString);
System.out.println(sign);
System.out.println("-------------------");
signString = "password=123456&username=1&12345678954556" + "ff03e64b-427b-45a7-b78b-47d9e8597d3b1529815393153sdfsdfsfs" + timestamp + "A1scr6";
sign = MD5Util.encode(signString);
System.out.println(sign);
}
}WebMvcConfiguration
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
private static final String[] excludePathPatterns = {"/api/token/api_token"};
@Autowired
private TokenInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry){
super.addInterceptors(registry);
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns(excludePathPatterns);
}
}TokenInterceptor
@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 requestInterval = System.currentTimeMillis() - Long.valueOf(timestamp);
Assert.isTrue(requestInterval < expireTime, "请求超时,请重新请求");
ValueOperations<String, TokenInfo> tokenRedis = redisTemplate.opsForValue();
TokenInfo tokenInfo = tokenRedis.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<String, Integer> signRedis = redisTemplate.opsForValue();
boolean exists = redisTemplate.hasKey(sign);
Assert.isTrue(!exists, "请勿重复提交");
signRedis.set(sign, 0, expireTime, TimeUnit.MILLISECONDS);
}
return super.preHandle(request, response, handler);
}
}MD5Util (MD5 utility class)
public class MD5Util {
private static final String[] hexDigits = {"0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"};
private static String byteArrayToHexString(byte[] b){
StringBuffer resultSb = new StringBuffer();
for(int i = 0; i < b.length; i++)
resultSb.append(byteToHexString(b[i]));
return resultSb.toString();
}
private static String byteToHexString(byte b){
int n = b;
if(n < 0) n += 256;
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
public static String encode(String origin){
return encode(origin, "UTF-8");
}
public static String encode(String origin, String charsetname){
String resultString = null;
try{
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if(charsetname == null || "".equals(charsetname))
resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
else
resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
}catch(Exception e){}
return resultString;
}
}@NotRepeatSubmit (custom annotation)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotRepeatSubmit {
/** Expiration time in milliseconds */
long value() default 5000;
}Data Classes (AccessToken, AppInfo, TokenInfo, UserInfo, ApiCodeEnum, ApiResult, ApiResponse)
@Data
@AllArgsConstructor
public class AccessToken {
private String token;
private Date expireTime;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AppInfo {
private String appId;
private String key;
}
@Data
public class TokenInfo {
private Integer tokenType; // 0: api, 1: user
private AppInfo appInfo;
private UserInfo userInfo;
}
@Data
public class UserInfo {
private String username;
private String mobile;
private String email;
private String password;
private String salt;
private AccessToken accessToken;
public UserInfo(String username, String password, String salt){
this.username = username;
this.password = password;
this.salt = salt;
}
}
public enum ApiCodeEnum {
SUCCESS("10000","success"),
UNKNOW_ERROR("ERR0001","未知错误"),
PARAMETER_ERROR("ERR0002","参数错误"),
TOKEN_EXPIRE("ERR0003","认证过期"),
REQUEST_TIMEOUT("ERR0004","请求超时"),
SIGN_ERROR("ERR0005","签名错误"),
REPEAT_SUBMIT("ERR0006","请不要频繁操作");
private String code;
private String msg;
ApiCodeEnum(String code, String msg){ this.code = code; this.msg = msg; }
public String getCode(){ return code; }
public String getMsg(){ return msg; }
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResult {
private String code;
private String msg;
}
@Data
@Slf4j
public class ApiResponse<T> {
private ApiResult result;
private T data;
private String sign;
public static <T> ApiResponse<T> success(T data){
return response(ApiCodeEnum.SUCCESS.getCode(), ApiCodeEnum.SUCCESS.getMsg(), data);
}
public static ApiResponse error(String code, String msg){
return response(code, msg, null);
}
public static <T> ApiResponse<T> response(String code, String msg, T data){
ApiResult result = new ApiResult(code, msg);
ApiResponse<T> response = new ApiResponse<>();
response.setResult(result);
response.setData(data);
String sign = signData(data);
response.setSign(sign);
return response;
}
private static <T> String signData(T data){
String key = "12345678954556";
Map<String, String> responseMap = null;
try{ responseMap = getFields(data); }catch(IllegalAccessException e){ return null; }
String urlComponent = ApiUtil.concatSignString(responseMap);
String signature = urlComponent + "key=" + key;
return MD5Util.encode(signature);
}
public static Map<String, String> getFields(Object data) throws IllegalAccessException {
if(data == null) return null;
Map<String, String> map = new HashMap<>();
Field[] fields = data.getClass().getDeclaredFields();
for(Field field : fields){
field.setAccessible(true);
Object value = field.get(data);
if(value != null) map.put(field.getName(), value.toString());
}
return map;
}
}ThreadLocal
ThreadLocal provides a thread‑scoped global context, allowing data to be shared across methods without passing parameters. It is often used to store the authenticated user after token validation, making the user information accessible in controllers, services, and DAOs.
ThreadLocalUtil
public class ThreadLocalUtil<T>{
private static final ThreadLocal<Map<String,Object>> threadLocal = new ThreadLocal(){
@Override
protected Map<String,Object> initialValue(){
return new HashMap<>(4);
}
};
public static Map<String,Object> getThreadLocal(){ return threadLocal.get(); }
public static <T> T get(String key){
Map map = (Map)threadLocal.get();
return (T)map.get(key);
}
public static <T> T get(String key, T defaultValue){
Map map = (Map)threadLocal.get();
return map.get(key) == null ? defaultValue : (T)map.get(key);
}
public static void set(String key, Object value){
Map map = (Map)threadLocal.get();
map.put(key, value);
}
public static void remove(){ threadLocal.remove(); }
// Additional helper methods omitted for brevity
}Summary
The article presents commonly used parameters and implementation examples for secure third‑party API interactions, including token, timestamp, signature, and anti‑replay mechanisms. For higher security, additional encryption methods such as RSA, RSA2, or AES can be added, though they increase CPU overhead.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.
