Information Security 21 min read

Implementing Token-Based Authentication, Anti‑DoS, and Repeat‑Submission Prevention in Spring Boot

This article explains how to design a secure token mechanism with timestamp validation, describes common DoS attack types, and provides practical Spring Boot code examples—including Redis configuration, token generation, interceptor logic, MD5 signing utilities, and ThreadLocal usage—to prevent repeat submissions and protect APIs.

Top Architect
Top Architect
Top Architect
Implementing Token-Based Authentication, Anti‑DoS, and Repeat‑Submission Prevention in Spring Boot

1. Token Introduction: a timestamp is used to prevent DoS attacks by ensuring the request time is within an acceptable window; the token is generated and validated together with the timestamp and a signature.

2. DoS Overview: detailed descriptions of various denial‑of‑service attacks such as Pingflood, Synflood, Smurf, Land‑based, Ping of Death, Teardrop, and PingSweep, explaining how they exhaust network or system resources.

3. Preventing Repeat Submissions: when a request is first received, its sign is stored in Redis with an expiration equal to the token timeout; subsequent identical requests are rejected. The @NotRepeatSubmit annotation can be applied to methods that must not be called repeatedly.

4. Example Code:

<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>
@Configuration
public class RedisConfiguration {
    @Bean
    public JedisConnectionFactory jedisConnectionFactory(){
        return new JedisConnectionFactory();
    }
    @Bean
    public RedisTemplate
redisTemplate(){
        RedisTemplate
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;
    }
}
@Slf4j
@RestController
@RequestMapping("/api/token")
public class TokenController {
    @Autowired
    private RedisTemplate redisTemplate;
    @PostMapping("/api_token")
    public ApiResponse
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
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
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);
    }
}
@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
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
signRedis = redisTemplate.opsForValue();
            boolean exists = redisTemplate.hasKey(sign);
            Assert.isTrue(!exists, "请勿重复提交");
            signRedis.set(sign, 0, expireTime, TimeUnit.MILLISECONDS);
        }
        return super.preHandle(request, response, handler);
    }
}
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;
    }
}
public class ThreadLocalUtil
{
    private static final ThreadLocal
> threadLocal = new ThreadLocal() {
        @Override
        protected Map
initialValue() {
            return new HashMap<>(4);
        }
    };
    public static Map
getThreadLocal(){
        return threadLocal.get();
    }
    public static
T get(String key) {
        Map map = (Map)threadLocal.get();
        return (T)map.get(key);
    }
    public static
T get(String key,T defaultValue) {
        Map map = (Map)threadLocal.get();
        return (T)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 set(Map
keyValueMap) {
        Map map = (Map)threadLocal.get();
        map.putAll(keyValueMap);
    }
    public static void remove() {
        threadLocal.remove();
    }
    public static
Map
fetchVarsByPrefix(String prefix) {
        Map
vars = new HashMap<>();
        if(prefix == null) return vars;
        Map map = (Map)threadLocal.get();
        Set
set = map.entrySet();
        for(Map.Entry entry : set){
            Object key = entry.getKey();
            if(key instanceof String && ((String)key).startsWith(prefix)){
                vars.put((String)key,(T)entry.getValue());
            }
        }
        return vars;
    }
    public static
T remove(String key) {
        Map map = (Map)threadLocal.get();
        return (T)map.remove(key);
    }
    public static void clear(String prefix) {
        if(prefix == null) return;
        Map map = (Map)threadLocal.get();
        Set
set = map.entrySet();
        List
removeKeys = new ArrayList<>();
        for(Map.Entry entry : set){
            Object key = entry.getKey();
            if(key instanceof String && ((String)key).startsWith(prefix)){
                removeKeys.add((String)key);
            }
        }
        for(String key : removeKeys){
            map.remove(key);
        }
    }
}

5. ThreadLocal Usage: provides a per‑thread storage mechanism to hold user or request data (e.g., user info) without passing it through method parameters, simplifying code in controllers, services, and DAOs.

Conclusion: The article delivers a comprehensive, production‑ready guide for securing API endpoints with token‑based authentication, mitigating DoS threats, preventing duplicate submissions, and managing request‑scoped data via ThreadLocal in a Spring Boot environment.

javaRedisSpring Boottoken authenticationThreadLocalDoS protection
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

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