Common API Security Practices: Token, Timestamp, Signature, and Duplicate Submission Prevention in Java

This article explains practical API security techniques for protecting data exchange with third‑party systems, covering token generation and storage, timestamp validation to mitigate DoS attacks, MD5‑based request signing with nonce, preventing duplicate submissions using Redis, and illustrates the concepts with comprehensive Java code examples.

Java Captain
Java Captain
Java Captain
Common API Security Practices: Token, Timestamp, Signature, and Duplicate Submission Prevention in Java

In real‑world business scenarios, interacting with third‑party systems requires additional security beyond HTTPS, such as a unified set of algorithms and conventions to protect data transmission.

1. Token Overview – An access token identifies the caller and reduces the need to transmit usernames and passwords. Tokens (usually UUIDs) are generated by the server, stored in Redis with associated metadata, and come in two types: API Token (for unauthenticated endpoints like login) and User Token (for endpoints requiring user login).

2. Timestamp Overview – The timestamp represents the client’s current time in milliseconds. It is used to prevent DoS attacks by rejecting requests whose timestamp differs from the server time by more than a configurable interval (e.g., 5 minutes).

3. Signature (sign) Overview – A nonce (random string) and a sign parameter are added to each request. The sign is generated by concatenating all non‑empty parameters (sorted by name), the token, timestamp, nonce, and a secret key, then applying MD5. The server recomputes the signature to verify that parameters have not been tampered with.

4. Preventing Duplicate Submissions – Methods annotated with @NotRepeatSubmit store the sign in Redis with an expiration equal to the request timeout. Subsequent requests with the same sign are rejected, ensuring idempotent operations.

5. Usage Flow 1) The client requests an appId and key from the server. 2) It calls the /api/token/api_token endpoint with appId, timestamp, and sign to obtain an API token. 3) The token is used to call public APIs. 4) For protected APIs, the client logs in with username and password to receive a user token, which is then used for subsequent calls.

6. Example Code

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();
    }

    @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 (core API endpoints):

@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 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<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 (registers the interceptor):

@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 (validation logic):

@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, "请求超时,请重新请求");
        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);
    }
}

Utility classes (MD5Util, ApiUtil, ThreadLocalUtil) and data models (AccessToken, AppInfo, TokenInfo, UserInfo, ApiResult, ApiResponse, ApiCodeEnum) are provided to support signing, parameter concatenation, annotation processing, and thread‑local storage of request‑specific data.

ThreadLocal Usage – A ThreadLocalUtil class stores a map per thread, allowing any layer (controller, service, DAO) to retrieve or set contextual data such as the current user without passing parameters explicitly.

Conclusion – The article presents a set of commonly used API security mechanisms (token, timestamp, signature, duplicate‑submission protection) with complete Java implementations, and mentions that stronger encryption (RSA, AES) can be added at the cost of higher CPU consumption.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaBackend Developmentinformation securityTokenAPI Securitysignature
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.