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

This article explains practical methods for protecting data exchanged with third‑party systems by using access tokens, timestamps, cryptographic signatures, and anti‑duplicate‑submission techniques, and provides complete Spring Boot code examples for token generation, validation, Redis storage, and request interception.

Architect's Must-Have
Architect's Must-Have
Architect's Must-Have
How to Secure API Calls with Tokens, Timestamps, and Signatures in Spring Boot

In real‑world business scenarios you often need to exchange data with third‑party systems, so ensuring the security of data in transit (preventing theft) is essential. Beyond HTTPS, a common approach is to adopt a set of algorithms and conventions such as tokens, timestamps, signatures, and anti‑duplicate‑submission mechanisms.

Token Overview

Token (access token) identifies the caller of an API and reduces the need to transmit usernames and passwords repeatedly. The client first obtains an appId and a key from the server; the key is used for parameter signing and must be stored securely on the client.

Tokens are usually UUIDs stored in a cache server (Redis) as keys with associated data as values. When a request arrives, the server checks Redis for the token; if present, the request proceeds, otherwise an error is returned. Two token types are defined:

API Token – for endpoints that do not require user login (e.g., login, registration, basic data retrieval). It is obtained using appId, timestamp and sign where sign = encrypt(timestamp + key).

USER Token – for endpoints that require a logged‑in user. It is obtained using username and password.

Token validity can be one‑time or time‑bounded depending on business needs. Using HTTPS is still recommended; HTTP with token only reduces risk but cannot stop a determined attacker.

Timestamp Overview

Timestamp is the current time sent by the client and helps 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 fully prevent DoS, but it limits the attack window. If an attacker modifies the timestamp, the signature mechanism can detect the tampering.

DoS

Denial‑of‑Service attacks aim to exhaust resources (bandwidth, CPU, memory, connection slots) so that legitimate users cannot obtain service. Common variants include Ping flood, SYN flood, Smurf, Land, Ping of Death, Teardrop, and PingSweep.

Signature Overview

Signature (sign) is used for parameter signing to prevent illegal modification of critical parameters (e.g., amount). The sign value is generated by concatenating all non‑empty parameters in ascending order, then appending token, key, timestamp, and nonce, and finally applying an encryption algorithm (MD5 in the example). The server recomputes the signature and compares it with the received sign; a mismatch indicates tampering.

Preventing Duplicate Submissions

For non‑idempotent operations, the first request stores the sign as a key in Redis with an expiration equal to the timestamp window. Subsequent requests with the same sign are rejected as duplicates. Custom annotations can mark methods that require this check.

Note: Applying all security measures may be overly complex; in practice you should select the mechanisms that meet your project's security requirements.

Usage Flow

1. The client requests an API account from the server and receives appId and key. 2. The client sends appId, timestamp, and sign (where sign = encrypt(appId + timestamp + key)) to obtain an API token. 3. The client uses the API token to call endpoints that do not require login. 4. For user‑protected endpoints, the client logs in with username and password, receives a user token, and uses it for subsequent calls. 5. The sign parameter protects request integrity; the server may also return a sign for response verification.

Code Samples

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

Redis configuration:

@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;
    }
}

Token controller (simplified):

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

Web MVC configuration and token interceptor (excerpt):

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

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

Utility classes (MD5Util, NotRepeatSubmit, ApiUtil, ApiResponse, etc.) provide MD5 hashing, custom annotation for duplicate‑submission protection, parameter concatenation, and a generic API response wrapper with automatic signing.

ThreadLocal

ThreadLocal offers a thread‑scoped global context, allowing data such as the authenticated user to be stored once per request and accessed across controller, service, and DAO layers without passing parameters.

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<String, Object> map = (Map<String, Object>) threadLocal.get();
        return (T) map.get(key);
    }
    public static void set(String key, Object value){
        Map<String, Object> map = (Map<String, Object>) threadLocal.get();
        map.put(key, value);
    }
    public static void remove(){
        threadLocal.remove();
    }
    // additional helper methods omitted for brevity
}

Summary

The article presents a set of commonly used parameters and implementation examples for secure third‑party API interaction, including token generation, timestamp validation, signature creation, and duplicate‑submission prevention, while noting 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.

Backend DevelopmentSpring Boottoken authenticationAPI Securitysignatureduplicate submission
Architect's Must-Have
Written by

Architect's Must-Have

Professional architects sharing high‑quality architecture insights. Covers high‑availability, high‑performance, high‑stability designs, big data, machine learning, Java, system, distributed and AI architectures, plus internet‑driven architectural adjustments and large‑scale practice. Open to idea‑driven, sharing architects for exchange and learning.

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.