签名机制 + JWT 鉴权 + Redis 防重放机制
完整项目整合:签名机制 + JWT 鉴权 + Redis 防重放机制,这是一个企业级、高安全性前后端分离项目的核心安全架构,适用于:
- 涉及敏感数据的接口(如支付、用户信息、实名认证)
- 需要身份认证(JWT)
- 需要防止请求被篡改(签名)
- 需要防止请求被重复提交 / 重放攻击(Redis nonce 校验)
✅ 一、目标:整合三大安全机制
模块 | 技术方案 | 作用 |
---|---|---|
1. JWT 鉴权 | 用户登录后返回 JWT Token,后续请求在 Header 中携带,用于身份认证 | 实现「谁在访问」 |
2. 签名机制(带 timestamp / nonce / sign) | 前端根据业务参数、时间戳、随机数、密钥生成签名,后端验签防止篡改 | 实现「请求是否被篡改」 |
3. Redis 防重放(nonce 去重) | 每个请求携带唯一 nonce,服务端用 Redis 校验该 nonce 是否已使用,防止重复请求 | 实现「请求是否被重放」 |
✅ 二、整体架构流程(一次安全请求的生命周期)
-
用户登录
- 前端提交用户名密码
- 后端校验通过,返回 JWT Token
- 前端存储 Token(如 localStorage)
-
发起安全请求(如获取用户信息、支付等)
- 前端:
- 携带 JWT Token(Authorization header)
- 生成:timestamp、nonce、业务参数
- 用密钥 + 参数生成 sign
- 发送请求到后端:
POST /api/secure-action
- 后端:
- 从 Header 解析 JWT,校验用户身份
- 校验请求中的 timestamp(是否过期)
- 校验 签名(sign)是否合法
- 校验 nonce 是否已经使用过(Redis 去重)
- 全部校验通过 ⇒ 执行业务逻辑
- 前端:
-
安全保障
- ✅ 身份合法(JWT)
- ✅ 请求未被篡改(签名)
- ✅ 请求未被重复提交(nonce + Redis)
✅ 三、技术栈与工具
技术 | 说明 |
---|---|
后端 | Spring Boot + Spring Security(可选)+ JWT + Redis |
前端 | Vue 3 + Axios + crypto-js(生成签名) |
加密/安全 | JWT、MD5/SHA256 签名、AES(可选)、Redis |
通信协议 | HTTPS(必须!) |
✅ 四、项目模块划分(后端 - Spring Boot)
src/main/java/com/example/secureapi/
├── config/ # 配置类(可选)
├── controller/
│ ├── AuthController.java // 登录接口,返回 JWT
│ └── SecureController.java // 受保护的安全接口(带签名 + nonce 校验)
├── dto/
│ ├── LoginRequest.java
│ └── LoginResponse.java
├── model/ // 可选,如 User
├── security/ // JWT 相关(可选)
│ ├── JwtUtil.java
│ └── JwtAuthenticationFilter.java
├── util/
│ ├── SignUtil.java // 签名生成与校验
│ └── RedisNonceUtil.java // Redis nonce 去重工具
├── SecureApiApplication.java // 启动类
✅ 五、1. JWT 鉴权模块(用户登录,返回 Token)
🔐 JwtUtil.java(生成和校验 JWT)
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;public class JwtUtil {private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);private static final long EXPIRATION_MS = 1000 * 60 * 60 * 24; // 1天public static String generateToken(String username) {return Jwts.builder().setSubject(username).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS)).signWith(SECRET_KEY).compact();}public static String getUsernameFromToken(String token) {return Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token).getBody().getSubject();}public static boolean validateToken(String token) {try {Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token);return true;} catch (Exception e) {return false;}}
}
🛡️ AuthController.java(登录接口返回 JWT)
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/auth")
public class AuthController {@PostMapping("/login")public Map<String, String> login(@RequestBody LoginRequest request) {// 模拟校验用户名密码(实际应查数据库)if ("admin".equals(request.getUsername()) && "123456".equals(request.getPassword())) {String token = JwtUtil.generateToken(request.getUsername());return Map.of("token", token);} else {throw new RuntimeException("用户名或密码错误");}}
}
🧩 JwtAuthenticationFilter.java(可选,校验请求头中的 JWT)
如果你希望所有安全接口都先校验 JWT 身份,可以实现一个 Spring Filter 拦截请求,解析并校验 JWT,设置用户身份到 SecurityContext。(可参考之前提供的 JWT Filter 示例,或使用 Spring Security 集成)
✅ 六、2. 签名机制模块(timestamp + nonce + sign)
🔐 SignUtil.java(签名生成与校验工具)
import java.util.Map;
import java.util.TreeMap;public class SignUtil {public static String generateSign(Map<String, String> params, String timestamp, String nonce, String secretKey) {TreeMap<String, String> sorted = new TreeMap<>(params);StringBuilder sb = new StringBuilder();for (Map.Entry<String, String> entry : sorted.entrySet()) {sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");}sb.append("timestamp=").append(timestamp).append("&nonce=").append(nonce).append("&key=").append(secretKey);return md5(sb.toString()); // 或 SHA256,推荐生产用 SHA-256}// 简单 MD5,生产推荐使用 crypto-js SHA256,后端对应调整private static String md5(String input) {try {java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");byte[] digest = md.digest(input.getBytes());StringBuilder sb = new StringBuilder();for (byte b : digest) sb.append(String.format("%02x", b));return sb.toString();} catch (Exception e) {throw new RuntimeException(e);}}public static boolean verifySign(Map<String, String> params, String timestamp, String nonce, String secretKey) {String serverSign = generateSign(params, timestamp, nonce, secretKey);String clientSign = params.get("sign");return serverSign != null && serverSign.equals(clientSign);}
}
✅ 七、3. Redis 防重放模块(nonce 去重)
🧠 RedisNonceUtil.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;@Component
public class RedisNonceUtil {@Autowiredprivate StringRedisTemplate redisTemplate;private static final String NONCE_KEY_PREFIX = "api:nonce:";public boolean isNonceValid(String nonce, long expireSeconds) {Boolean success = redisTemplate.opsForValue().setIfAbsent(NONCE_KEY_PREFIX + nonce,"1",expireSeconds,TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}
}
✅ 八、4. 安全接口(整合 JWT + 签名 + Redis 防重放)
🛡️ SecureController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;@RestController
@RequestMapping("/api")
public class SecureController {private static final String SECRET_KEY = "my_super_secret_key_123";private static final long TIMESTAMP_WINDOW_MS = 5 * 60 * 1000; // 5分钟private static final long NONCE_EXPIRE_SECONDS = 300; // 5分钟@Autowiredprivate RedisNonceUtil redisNonceUtil;@PostMapping("/secure-action")public Map<String, String> secureAction(@RequestBody Map<String, String> request) {// --- 1. 获取请求参数 ---String token = request.get("token"); // 假设前端把 JWT 放在请求体或 headerString timestamp = request.get("timestamp");String nonce = request.get("nonce");String sign = request.get("sign");// --- 2. 校验 JWT 身份(简化版,真实项目推荐用 Filter 或 Spring Security)---// 这里简化处理,真实项目应该解析 JWT,校验用户名、过期时间等if (token == null || !token.startsWith("Bearer ")) {return Map.of("code", "401", "message", "未提供有效 Token");}String jwt = token.substring(7);if (!JwtUtil.validateToken(jwt)) {return Map.of("code", "401", "message", "Token 无效或已过期");}// --- 3. 校验时间戳(防过期请求)---long reqTime = Long.parseLong(timestamp);long currTime = System.currentTimeMillis();if (Math.abs(currTime - reqTime) > TIMESTAMP_WINDOW_MS) {return Map.of("code", "403", "message", "请求已过期");}// --- 4. 校验签名(防篡改)---// 假设业务参数是 "info",去掉 sign / timestamp / nonce / tokenMap<String, String> params = new java.util.HashMap<>(request);params.remove("sign");params.remove("timestamp");params.remove("nonce");params.remove("token");boolean isSignValid = SignUtil.verifySign(params, timestamp, nonce, SECRET_KEY);if (!isSignValid) {return Map.of("code", "403", "message", "签名校验失败");}// --- 5. 校验 nonce 是否重复(防重放)---boolean isNonceValid = redisNonceUtil.isNonceValid(nonce, NONCE_EXPIRE_SECONDS);if (!isNonceValid) {return Map.of("code", "403", "message", "请求已被重放(nonce 重复)");}// --- 6. 所有校验通过,执行业务逻辑 ---String info = params.getOrDefault("info", "");return Map.of("code", "200", "message", "请求合法,已处理", "data", info);}
}
✅ 九、前端整合(简要说明)
前端需要做:
步骤 | 说明 |
---|---|
1. 用户登录 | 调用 /auth/login ,获取 JWT Token |
2. 每次安全请求 | 携带 Token(如放在请求头 Authorization: Bearer <token> 或请求体) |
3. 生成签名 | 用 crypto-js,根据业务参数 + timestamp + nonce + 密钥 生成 sign |
4. 生成 nonce | 每次请求用 Math.random().toString(36).substring(2,15) 或 uuid/nanoid |
5. 发送请求 | 把 token、timestamp、nonce、sign、业务参数一起提交到 /api/secure-action |
✅ 十、总结:你已完成整合
安全模块 | 是否整合 | 功能 |
---|---|---|
JWT 鉴权 | ✅ | 用户登录后返回 Token,后续接口鉴权 |
签名机制 | ✅ | 防篡改,校验请求参数一致性 |
Redis 防重放 | ✅ | 校验 nonce 唯一性,防止请求重复提交 |
整体安全请求流程 | ✅ | 身份合法 + 数据可信 + 请求唯一 |
✅ 十一、安全建议(生产环境必做)
建议 | 说明 |
---|---|
使用 HTTPS | 所有接口必须走 HTTPS,否则签名和 token 可能被窃听 |
使用 SHA-256 签名 | MD5 安全性不足,推荐 SHA-256 或 HMAC-SHA256 |
密钥管理 | secretKey 不要硬编码,推荐通过登录后下发或使用环境变量 |
Redis 过期策略 | nonce 和 timestamp 校验时间窗口要匹配 |
接口限流与监控 | 对频繁非法请求进行限流、记录、告警 |
✅ 十二、下一步可扩展
功能 | 说明 |
---|---|
Spring Security 集成 JWT | 更专业的身份认证与权限控制 |
动态密钥下发 | 登录后返回临时密钥,增强安全性 |
权限控制(@PreAuthorize) | 基于角色/权限控制接口访问 |
完整项目模板 / GitHub 示例 | 如你想要,我可以帮你整理成可运行的项目结构 |