Information Security 9 min read

Securing Frontend‑Integrated APIs with Token, Timestamp, and Signature Validation in Spring

This article explains how to protect API endpoints that interact with front‑end applications by using token‑based authentication, timestamp checks, and MD5 signatures, detailing the implementation of open and secured controllers, login logic, signature verification, replay‑attack mitigation, and a Spring interceptor.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Securing Frontend‑Integrated APIs with Token, Timestamp, and Signature Validation in Spring

Overview

When an API that interfaces with the front end is intercepted and its parameters are maliciously altered, data leakage or tampering can occur. This guide secures the API using three mechanisms: timestamps, tokens, and signatures.

Open Interface

An unrestricted endpoint that simply returns data based on supplied parameters, typical for public services such as weather or parcel tracking.

/*
 * Description: Open interface
 * @author huangweicheng
 * @date 2020/12/21
 */
@RestController
@RequestMapping("/token")
public class TokenSignController {

    @Autowired
    private TokenSignService tokenSignService;

    @RequestMapping(value = "openDemo", method = RequestMethod.GET)
    public List
openDemo(int personId) {
        return tokenSignService.getPersonList(personId);
    }
}

Token Authentication Retrieval

After a successful login, the server issues a ticket (token) that must accompany every subsequent request. The ticket is stored in Redis with a 10‑minute TTL and is silently refreshed before expiration.

@RequestMapping(value = "login", method = RequestMethod.POST)
public JSONObject login(@NotNull String username, @NotNull String password) {
    return tokenSignService.login(username, password);
}
/**
 * Description: Validate login, store ticket in cache
 */
public JSONObject login(String username, String password) {
    JSONObject result = new JSONObject();
    PersonEntity personEntity = personDao.findByLoginName(username);
    if (personEntity == null || (personEntity != null && !personEntity.getPassword().equals(password))) {
        result.put("success", false);
        result.put("ticket", "");
        result.put("code", "999");
        result.put("message", "用户名和密码不匹配");
        return result;
    }
    if (personEntity.getLoginName().equals(username) && personEntity.getPassword().equals(password)) {
        String ticket = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(ticket, personEntity.getLoginName(), 10L, TimeUnit.MINUTES);
        result.put("success", true);
        result.put("ticket", ticket);
        result.put("code", 200);
        result.put("message", "登录成功");
        return result;
    }
    result.put("success", false);
    result.put("ticket", "");
    result.put("code", "1000");
    result.put("message", "未知异常,请重试");
    return result;
}

Signature Generation

All request parameters are concatenated with a secret key and hashed using MD5 to produce a signature (sign). The server recomputes the signature to verify integrity.

public static Boolean checkSign(HttpServletRequest request, String sign) {
    Boolean flag = false;
    // Check if sign is expired
    Enumeration
pNames = request.getParameterNames();
    Map
params = new HashMap<>();
    while (pNames.hasMoreElements()) {
        String pName = (String) pNames.nextElement();
        if ("sign".equals(pName)) continue;
        String pValue = request.getParameter(pName);
        params.put(pName, pValue);
    }
    System.out.println("现在的sign-->" + sign);
    System.out.println("验证的sign-->" + getSign(params, secretKeyOfWxh));
    if (sign.equals(getSign(params, secretKeyOfWxh))) {
        flag = true;
    }
    return flag;
}

Replay Attack Prevention

A timestamp parameter ensures the request is valid only within a one‑minute window, matching the server’s current time.

public static long getTimestamp() {
    long timestampLong = System.currentTimeMillis();
    long timestampsStr = timestampLong / 1000;
    return timestampsStr;
}

Interceptor

The interceptor checks that the ticket, sign, and timestamp are present and valid before allowing the request to proceed.

public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        JSONObject jsonObject = new JSONObject();
        String ticket = request.getParameter("ticket");
        String sign = request.getParameter("sign");
        String ts = request.getParameter("ts");
        if (StringUtils.isEmpty(ticket) || StringUtils.isEmpty(sign) || StringUtils.isEmpty(ts)) {
            jsonObject.put("success", false);
            jsonObject.put("message", "args is isEmpty");
            jsonObject.put("code", "1001");
            response.getWriter().write(jsonObject.toJSONString());
            return false;
        }
        // Verify ticket existence in Redis
        if (redisTemplate.hasKey(ticket)) {
            String values = (String) redisTemplate.opsForValue().get(ticket);
            // Refresh ticket if about to expire
            if (redisTemplate.opsForValue().getOperations().getExpire(ticket) != -2 &&
                redisTemplate.opsForValue().getOperations().getExpire(ticket) < 20) {
                redisTemplate.opsForValue().set(ticket, values, 10L, TimeUnit.MINUTES);
            }
            // Replay attack check
            if (SignUtils.getTimestamp() - Long.valueOf(ts) > 600) {
                jsonObject.put("success", false);
                jsonObject.put("message", "Overtime to connect to server");
                jsonObject.put("code", "1002");
                response.getWriter().write(jsonObject.toJSONString());
                return false;
            }
            // Signature verification
            if (!SignUtils.checkSign(request, sign)) {
                jsonObject.put("success", false);
                jsonObject.put("message", "sign is invalid");
                jsonObject.put("code", "1003");
                response.getWriter().write(jsonObject.toJSONString());
                return false;
            }
            return true;
        } else {
            jsonObject.put("success", false);
            jsonObject.put("message", "ticket is invalid, Relogin.");
            jsonObject.put("code", "1004");
            response.getWriter().write(jsonObject.toJSONString());
        }
        return false;
    }
}

Access

First log in to obtain a valid ticket, then generate a correct sign and timestamp, and finally call the openDemo endpoint. Using HTTPS and optional parameter encryption further strengthens security.

https://github.com/hwc4110/spring-demo1221

END

Spring BootInterceptortoken authenticationAPI securitySignatureReplay Attack
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

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.