Spring Boot实现接口时间戳鉴权

Spring Boot实现接口时间戳鉴权,签名(sign)和时间戳(ts)放入请求头(Header)。

一、请求头参数设计

参数名 类型 说明
ts Long 13位时间戳(Unix毫秒值),必填,标识请求有效期
sign String 签名值,必填,用于校验请求合法性

二、签名算法调整(Header版)

**1. 构造原始字符串 **
plaintext 复制代码
str = key + url_encode(path) + ts
  • path :请求的URL路径部分(不含查询参数和域名),例如:/api/v1/user
  • url_encode(path) :需对路径中的特殊字符进行URL编码(如中文、/等无需编码,但空格需转义为%20)。
2. 生成签名 SIGN
plaintext 复制代码
sgin = md5(S).toLowerCase()
  • 结果转换为小写字符串,与请求头中的sign对比校验。

三、Spring Boot实现流程

1. 创建拦截器(Interceptor)

用于拦截请求,校验tssign的合法性。

java 复制代码
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Objects;

public class AuthInterceptor implements HandlerInterceptor {
    private final String secretKey; // 从配置文件获取密钥,例如application.properties
    private final long maxClockSkew = 60 * 1000; // 时间窗口:60秒(允许客户端与服务端的时间误差)

    public AuthInterceptor(String secretKey) {
        this.secretKey = secretKey;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 1. 校验时间戳是否存在
        String ts = request.getHeader("ts");
        if (StringUtils.isEmpty(ts)) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.setContentType("application/json");
            // 返回错误信息:缺少时间戳
            return false;
        }

        // 2. 校验时间戳格式及有效性
        long ts;
        try {
            ts = Long.parseLong(ts );
        } catch (NumberFormatException e) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            // 返回错误信息:时间戳格式错误
            return false;
        }
        long currentTime = System.currentTimeMillis();
        if (currentTime - ts > maxClockSkew|| ts - currentTime > maxClockSkew) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            // 返回错误信息:请求已过期
            return false;
        }

        // 3. 校验签名是否存在
        String sign= request.getHeader("sign");
        if (StringUtils.isEmpty(sign)) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            // 返回错误信息:缺少签名
            return false;
        }

        // 4. 构造原始签名字符串并生成签名
        String path = request.getRequestURI(); // 获取路径,例如"/api/v1/user"
        String encodedPath = URLEncoder.encode(path, StandardCharsets.UTF_8.toString())
                .replace("+", "%20") // 处理空格编码差异(URLEncoder默认用+,此处统一为%20)
                .replace("%7E", "~"); // 保留波浪线~的原始格式
        String rawString = secretKey + encodedPath + tsHeader;

        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] digest = md.digest(rawString.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b)); // 转换为小写16进制字符串
            }
            String generatedSign = sb.toString();

            // 5. 对比签名
            if (!generatedSign.equals(sign)) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                // 返回错误信息:签名校验失败
                return false;
            }
        } catch (Exception e) {
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            return false;
        }

        return true; // 校验通过,允许请求继续
    }
}
2. 配置拦截器(WebMvcConfigurer)

将拦截器注册到Spring Boot的拦截器链中,指定需要鉴权的接口路径。

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    private final String secretKey; // 从配置文件获取密钥,例如通过@Value注入

    public WebConfig(String secretKey) {
        this.secretKey = secretKey;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        AuthInterceptor authInterceptor = new AuthInterceptor(secretKey);
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/v1/**") // 需要鉴权的接口路径(如/api/v1下所有接口)
                .excludePathPatterns("/api/v1/public/**"); // 无需鉴权的公共接口(可选)
    }
}
3. 配置文件(application.properties)
properties 复制代码
# 密钥(建议从环境变量或配置中心获取,而非硬编码)
auth.secret-key=your_secret_key_here

四、客户端请求示例(以Postman为例)

1. 请求头参数
名称
ts 1686048000000(当前时间戳,13位Long)
sign 计算得到的签名值(如e10adc3949ba59abbe56e057f20f883e
2. 签名计算步骤(JavaScript示例)
javascript 复制代码
function generateSignature(key, path, ts) {
    const encodedPath = encodeURIComponent(path).replace(/[!'()*]/g, encodeURIComponent); // 严格编码特殊字符
    const rawString = key + encodedPath + ts;
    const md5 = require('crypto-js/md5'); // 需要安装crypto-js库
    return md5(rawString).toString().toLowerCase();
}

// 示例调用
const key = 'your_secret_key';
const path = '/api/v1/user'; // 接口路径
const ts = Date.now(); // 当前时间戳(13位)
const sign = generateSignature(key, path, ts);
console.log(sign); // 输出签名值
相关推荐
你的人类朋友2 分钟前
【Docker】说说卷挂载与绑定挂载
后端·docker·容器
间彧26 分钟前
在高并发场景下,如何平衡QPS和TPS的监控资源消耗?
后端
间彧27 分钟前
QPS和TPS的区别,在实际项目中,如何准确测量和监控QPS和TPS?
后端
zizisuo43 分钟前
解决在使用Lombok时maven install 找不到符号的问题
java·数据库·maven
间彧1 小时前
消息队列(RocketMQ、RabbitMQ、Kafka、ActiveMQ)对比与选型指南
后端·消息队列
笨蛋少年派1 小时前
JAVA基础语法
java·开发语言
Haooog1 小时前
654.最大二叉树(二叉树算法)
java·数据结构·算法·leetcode·二叉树
我真的是大笨蛋1 小时前
依赖倒置原则(DIP)
java·设计模式·性能优化·依赖倒置原则·设计规范
brzhang2 小时前
AI Agent 干不好活,不是它笨,告诉你一个残忍的现实,是你给他的工具太难用了
前端·后端·架构
东方芷兰2 小时前
JavaWeb 课堂笔记 —— 20 SpringBootWeb案例 配置文件
java·开发语言·笔记·算法·log4j·intellij-idea·lua