How to Secure API Calls with Tokens, Timestamps, and Signatures in Spring Boot

This article explains practical methods for protecting API data exchange—including token usage, timestamp validation, signature generation, duplicate‑submission prevention, and ThreadLocal context—provides implementation details with Spring Boot, Redis, and Java code examples, and discusses related security considerations such as DoS attacks.

Programmer DD
Programmer DD
Programmer DD
How to Secure API Calls with Tokens, Timestamps, and Signatures in Spring Boot

Token Overview

Token (access token) is used in APIs to identify the caller and reduce the transmission of usernames and passwords. Clients obtain an appId and key from the server; the key must be stored securely on the client side.

Tokens are usually UUIDs stored in a cache server (e.g., Redis) as keys with associated information as values. When a request arrives, the server checks the token in the cache; if it exists, the request proceeds, otherwise an error is returned. Tokens can be API tokens (for unauthenticated endpoints) or USER tokens (for authenticated endpoints).

Token validity can be one‑time or time‑limited depending on business needs. Using HTTPS is recommended; with plain HTTP, token mechanisms only reduce risk but cannot fully prevent attacks.

Timestamp Overview

Timestamp is the current time sent by the client to prevent 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 it limits the attack window. Combined with a signature, altered timestamps can be detected.

DoS

Denial of Service attacks aim to exhaust resources such as bandwidth, connections, or CPU, causing services to become unavailable. Common techniques include Pingflood, Synflood, Smurf, Land, Ping of Death, Teardrop, and PingSweep.

Signature Overview

Signature ( sign) is used to prevent parameter tampering. It is generated by concatenating all non‑empty parameters (sorted by name), token, timestamp, nonce, and a secret key, then applying an encryption algorithm (e.g., MD5). The server recomputes the signature and compares it with the received value; a mismatch indicates illegal modification.

Preventing Duplicate Submissions

For non‑idempotent operations, the first request stores the sign in Redis with an expiration equal to the timestamp window. Subsequent identical requests within that window are rejected as duplicates. The expiration of sign and the token should be consistent.

Usage Flow

Client requests an API token by providing appId, timestamp, and sign (where sign = MD5(appId + timestamp + key)).

With the API token, the client can call endpoints that do not require user login.

For user‑login endpoints, the client sends username and password to obtain a user token, which is then used for authenticated calls.

Code Examples

1. 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>

2. 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;
    }
}

3. 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, "请求过期,请重新请求");
        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);
        signString = "password=123456&username=1&12345678954556" + "ff03e64b-427b-45a7-b78b-47d9e8597d3b1529815393153sdfsdfsfs" + timestamp + "A1scr6";
        sign = MD5Util.encode(signString);
        System.out.println(sign);
    }
}

4. 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);
    }
}

5. 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);
    }
}

6. 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 exception) {}
        return resultString;
    }
}

7. ThreadLocal Usage

ThreadLocal provides a thread‑scoped context, allowing data such as the current user to be accessed across controller, service, and DAO layers without passing parameters explicitly.

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) { return (T) threadLocal.get().get(key); }
    public static <T> T get(String key, T defaultValue) { return (T) threadLocal.get().getOrDefault(key, defaultValue); }
    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()) {
            String k = entry.getKey();
            if (k.startsWith(prefix)) vars.put(k, (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;
        List<String> removeKeys = new ArrayList<>();
        for (String k : threadLocal.get().keySet()) {
            if (k.startsWith(prefix)) removeKeys.add(k);
        }
        for (String k : removeKeys) threadLocal.get().remove(k);
    }
}

Summary: The article presents a set of commonly used parameters and implementation examples for secure third‑party API interaction, covering token, timestamp, signature, duplicate‑submission protection, and ThreadLocal context, with optional extensions such as RSA or AES encryption for stronger security.

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.

JavaSpring BootTokenAPI Securitysignature
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

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.