API Security Practices: Token, Timestamp, Signature, and Replay Prevention in Spring Boot
This article explains how to protect data exchange with third‑party systems by using access tokens, timestamps, cryptographic signatures, and duplicate‑submission safeguards, providing detailed Java/Spring Boot examples and code snippets for implementing secure API authentication and request validation.
In real‑world business scenarios, interacting with third‑party systems requires more than HTTPS to ensure data confidentiality and integrity; a common approach is to adopt a unified set of parameters—token, timestamp, sign, and nonce—to protect API calls.
Token is an access credential issued by the server after the client presents an appId and a secret key. The server stores the token as a key in Redis (or another cache) with associated metadata. Two token types are distinguished: an API token for public endpoints (login, registration, basic data) and a USER token for authenticated operations.
Timestamp records the client’s request time and is used to mitigate DoS attacks. The server compares the received timestamp with its current time; if the difference exceeds a configurable window (e.g., 5 minutes), the request is rejected.
Sign (signature) is generated by concatenating all non‑empty request parameters (sorted by name), the token, timestamp, nonce, and the shared secret key, then applying an MD5 hash. Because the secret key is never transmitted, an attacker cannot forge a valid signature, ensuring request parameters cannot be tampered with.
Duplicate‑submission prevention stores the incoming sign in Redis with the same expiration as the timestamp. Subsequent requests with the same signature are rejected, protecting non‑idempotent operations from being executed multiple times.
The typical workflow is:
Client requests an appId and key from the API provider.
Client sends appId, timestamp, and sign to obtain an api_token.
For public APIs, the client uses the api_token directly.
For protected APIs, the client logs in with username/password, receives a user_token, and includes it in subsequent calls.
Below are the essential Java code examples used in the implementation.
Dependencies
<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. Retrieve app secret from DB (mocked here)
AppInfo appInfo = new AppInfo("1", "12345678954556");
// 2. Verify signature
String signString = timestamp + appId + appInfo.getKey();
String signature = MD5Util.encode(signString);
log.info(signature);
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.setTime(new Date());
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 (signature helper)
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 exception) { }
return resultString;
}
}Custom annotation to block repeat submissions
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotRepeatSubmit {
/** Expiration time in milliseconds */
long value() default 5000;
}Domain objects
@Data
@AllArgsConstructor
public class AccessToken {
/** token */
private String token;
/** expiration time */
private Date expireTime;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AppInfo {
/** App id */
private String appId;
/** API secret key */
private String key;
}
@Data
public class TokenInfo {
/** token type: api=0, user=1 */
private Integer tokenType;
/** App information */
private AppInfo appInfo;
/** User information */
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;
}
}Result and response wrappers
@Data
public class ApiResult {
private String code;
private String msg;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
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);
response.setSign(signData(data));
return response;
}
private static <T> String signData(T data) {
String key = "12345678954556"; // placeholder
Map<String, String> responseMap;
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<>();
for (Field field : data.getClass().getDeclaredFields()) {
field.setAccessible(true);
Object value = field.get(data);
if (value != null) map.put(field.getName(), value.toString());
}
return map;
}
}Utility for building signature strings and retrieving annotations
public class ApiUtil {
public static String concatSignString(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterMap().forEach((k, v) -> paramMap.put(k, v[0]));
String[] keys = paramMap.keySet().toArray(new String[0]);
Arrays.sort(keys);
StringBuilder sb = new StringBuilder();
for (String k : keys) {
if ("sign".equals(k)) continue;
String v = paramMap.get(k).trim();
if (!v.isEmpty()) sb.append(k).append("=").append(v).append("&");
}
return sb.toString();
}
public static NotRepeatSubmit getNotRepeatSubmit(Object handler) {
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
return hm.getMethodAnnotation(NotRepeatSubmit.class);
}
return null;
}
}ThreadLocal utility for per‑thread context
public class ThreadLocalUtil<T> {
private static final ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<Map<String, Object>>() {
@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){
return (T) threadLocal.get().get(key);
}
public static <T> T get(String key, T defaultValue){
Map<String, Object> map = threadLocal.get();
return map.get(key) == null ? defaultValue : (T) map.get(key);
}
public static void set(String key, Object value){
threadLocal.get().put(key, value);
}
public static void set(Map<String, Object> keyValueMap){
threadLocal.get().putAll(keyValueMap);
}
public static void remove(){
threadLocal.remove();
}
public static <T> Map<String, T> fetchVarsByPrefix(String prefix){
Map<String, T> vars = new HashMap<>();
if (prefix == null) return vars;
for (Map.Entry<String, Object> entry : threadLocal.get().entrySet()) {
if (entry.getKey().startsWith(prefix)) {
vars.put(entry.getKey(), (T) entry.getValue());
}
}
return vars;
}
public static <T> T remove(String key){
return (T) threadLocal.get().remove(key);
}
public static void clear(String prefix){
if (prefix == null) return;
Map<String, Object> map = threadLocal.get();
List<String> toRemove = new ArrayList<>();
for (String k : map.keySet()) {
if (k.startsWith(prefix)) toRemove.add(k);
}
for (String k : toRemove) map.remove(k);
}
}In summary, the article provides a practical guide to securing third‑party API interactions by combining token‑based authentication, timestamp validation, cryptographic signatures, and repeat‑submission checks, all illustrated with complete Spring Boot and Redis code examples.
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.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
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.
