Spring Boot 3中JWT密钥安全存储方案
在Spring Boot 3中,存储和赋值JWT加密密钥的安全实践如下。核心原则是避免硬编码密钥,优先使用外部化配置和安全存储方案:
推荐方案:外部化配置 + 环境变量
1. 配置 application.yml
/application.properties
yaml
# application.yml jwt:secret-key: "${JWT_SECRET_KEY}" # 从环境变量注入
2. 通过环境变量注入密钥
本地开发:在IDE运行配置或
.env
文件中设置bash
# .env 文件(确保.gitignore) export JWT_SECRET_KEY=myStrongSecretKeyWith32Chars
生产环境:通过容器/服务器环境变量注入
bash
# Docker示例 docker run -e JWT_SECRET_KEY=your_secure_key your-app
云服务:使用云平台密钥管理(如AWS Secrets Manager/Azure Key Vault)
3. Java代码中获取密钥
java
@Component public class JwtUtil {private final String secretKey;// 构造器注入public JwtUtil(@Value("${jwt.secret-key}") String secretKey) {this.secretKey = secretKey;}public String generateToken(UserDetails userDetails) {return Jwts.builder().subject(userDetails.getUsername()).issuedAt(new Date()).expiration(new Date(System.currentTimeMillis() + 86400000)) // 24h.signWith(getSigningKey()) // 使用安全密钥.compact();}private Key getSigningKey() {byte[] keyBytes = Decoders.BASE64.decode(secretKey); // 密钥需Base64编码return Keys.hmacShaKeyFor(keyBytes);} }
进阶安全方案
方案1:密钥管理服务(生产推荐)
java
@Bean public Key jwtKey(SecretManagerService secretService) {String base64Key = secretService.getSecret("jwt-secret"); // 从Vault/AWS SM获取return Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64Key)); }
方案2:自动生成密钥(仅限开发)
java
@Bean public Key jwtKey() {return Keys.secretKeyFor(SignatureAlgorithm.HS256); // 每次启动变化,不适合生产 }
安全最佳实践
密钥强度:HS256算法至少32字符,推荐64字符随机字符串
bash
# 生成强密钥(Linux/Mac) openssl rand -base64 32
密钥轮换:通过密钥管理服务实现定期轮换
访问控制:
禁止日志打印密钥
应用配置最小权限原则
配置文件安全:
properties
# 禁止提交敏感数据到仓库 /src/main/resources/application*.yml -> .gitignore
不同环境配置示例
环境 | 存储位置 | 注入方式 |
---|---|---|
本地开发 | .env 文件 | Spring Boot @Value |
测试环境 | CI/CD 管道变量 | 部署脚本注入 |
生产环境 | AWS Secrets Manager/Hashicorp Vault | SDK动态获取 |
关键提示:永远不要将真实密钥提交到代码仓库!Spring Boot的配置外部化机制(优先级从高到低):
命令行参数
--jwt.secret-key=xxx
环境变量
JWT_SECRET_KEY
配置文件
application-{profile}.yml
通过遵循这些实践,可确保JWT密钥在Spring Boot 3应用中得到安全管理和使用。
一、非静态实现
package com.weiyu.utils;import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import java.util.Date;
import java.util.Map;/*** JWT工具类 (非静态实现)*/
@Component
public class JwtUtil {private final String secretKey;// 通过构造器注入密钥public JwtUtil(@Value("${jwt.secret-key}") String secretKey) {this.secretKey = secretKey;}/*** 生成 token 令牌* @param claims 业务数据* @return token 令牌*/public String genToken(Map<String, Object> claims) {// 10小时有效期long expirationMs = 1000 * 60 * 60 * 10;return JWT.create().withClaim("claims", claims).withIssuedAt(new Date()).withExpiresAt(new Date(System.currentTimeMillis() + expirationMs)).sign(Algorithm.HMAC256(secretKey));}/*** 验证并解析 token 令牌* @param token token 令牌* @return token 中的业务数据* @throws JWTVerificationException 当token验证失败时抛出*/public Map<String, Object> parseToken(String token) throws JWTVerificationException {return JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token).getClaim("claims").asMap();}
}
使用示例:
生成 token
@RestController
@RequestMapping("/account")
@Slf4j
public class AccountController {@Autowiredprivate JwtUtil jwtUtil;@PostMapping("/login")public Result<?> login(String account, String password) {// 前端传过来的就是password的MD5密文if (password.equalsIgnoreCase(finalPassword)) {// 登录成功// 构建令牌数据,包含id,用户名(账号)Map<String, Object> claims = new HashMap<>();claims.put("userId", loginAccount.getAccount());claims.put("userName", loginAccount.getAccount());// 生成token令牌String token = jwtUtil.genToken(claims);return Result.success(token);}}
验证 token
package com.weiyu.interceptors;import com.weiyu.utils.JwtUtil;
import com.weiyu.utils.ThreadLocalUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import java.util.Map;/*** 登录拦截器* 用于拦截请求,验证JWT令牌*/
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {@Autowiredprivate JwtUtil jwtUtil;/*** 预处理请求* 在请求到达 Controller 方法之前执行,当请求进入 Spring MVC 的 DispatcherServlet 后,首先会经过拦截器的 preHandle 方法* @param request 请求对象* @param response 响应对象* @param handler 处理器对象* @return 是否放行请求,返回 true 表示继续处理请求(放行),false 表示中断请求(不放行,需自行处理响应)* @throws Exception 异常*/@Overridepublic boolean preHandle(@NonNull HttpServletRequest request,@NonNull HttpServletResponse response,@NonNull Object handler) throws Exception {// 从请求头中获取 token 令牌String token = request.getHeader("Authorization");// 验证 token 令牌try {// 解析 token 令牌Map<String, Object> claims = jwtUtil.parseToken(token);// 将解析出来的 token 令牌数据存储到 ThreadLocalThreadLocalUtil.set(claims);// 放行return true;} catch (Exception e) {// http 响应状态码为 401response.setStatus(401);// 不放行return false;}}/*** 后处理请求* 在请求处理完成之后执行,包括 Controller 方法执行完毕、视图渲染完成(如返回 JSON 或 HTML)之后才会触发* @param request 请求对象* @param response 响应对象* @param handler 处理器对象* @param exception 异常* @throws Exception 异常*/@Overridepublic void afterCompletion(@NonNull HttpServletRequest request,@NonNull HttpServletResponse response,@NonNull Object handler,Exception exception) throws Exception {// 清空 ThreadLocal 的数据ThreadLocalUtil.remove();}
}
二、静态实现(不推荐)
以下是使用静态方法实现JWT工具类的安全方案,通过静态初始化块和@PostConstruct
确保密钥安全注入:
java
package com.weiyu.utils;import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component;import javax.annotation.PostConstruct; import java.util.Date; import java.util.Map;/*** 静态方法实现的JWT工具类* 注意:静态实现需谨慎处理密钥注入*/ @Component public class JwtUtil {private static String SECRET_KEY; // 静态密钥变量private static final long EXPIRATION_MS = 1000 * 60 * 60 * 10; // 10小时有效期// 通过setter注入密钥@Value("${jwt.secret-key}")public void setSecretKey(String secretKey) {// 禁止多次设置密钥if (SECRET_KEY == null) {SECRET_KEY = secretKey;}}// 初始化检查@PostConstructprivate void init() {if (SECRET_KEY == null || SECRET_KEY.isBlank()) {throw new IllegalStateException("JWT密钥未正确配置");}}/*** 生成 token 令牌* @param claims 业务数据* @return token 令牌*/public static String genToken(Map<String, Object> claims) {checkKeyInitialized();return JWT.create().withClaim("claims", claims).withIssuedAt(new Date()).withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_MS)).sign(Algorithm.HMAC256(SECRET_KEY));}/*** 验证并解析 token 令牌* @param token token 令牌* @return token 中的业务数据* @throws JWTVerificationException 当token验证失败时抛出*/public static Map<String, Object> parseToken(String token) throws JWTVerificationException {checkKeyInitialized();return JWT.require(Algorithm.HMAC256(SECRET_KEY)).build().verify(token).getClaim("claims").asMap();}// 密钥初始化检查private static void checkKeyInitialized() {if (SECRET_KEY == null) {throw new IllegalStateException("JWT工具类未初始化完成,请确保已注入密钥");}} }
关键实现说明:
安全注入机制:
java
@Value("${jwt.secret-key}") public void setSecretKey(String secretKey) {if (SECRET_KEY == null) { // 确保密钥只设置一次SECRET_KEY = secretKey;} }
初始化验证:
java
@PostConstruct private void init() {if (SECRET_KEY == null || SECRET_KEY.isBlank()) {throw new IllegalStateException("JWT密钥未正确配置");} }
使用前检查:
java
private static void checkKeyInitialized() {if (SECRET_KEY == null) {throw new IllegalStateException("JWT工具类未初始化完成");} }
使用示例:
java
// 在Spring管理的Bean中使用 @Service public class AuthService {public String generateUserToken(User user) {Map<String, Object> claims = Map.of("userId", user.getId(),"role", user.getRole());return JwtUtil.genToken(claims); // 静态调用}public User validateToken(String token) {try {Map<String, Object> claims = JwtUtil.parseToken(token);return User.fromClaims(claims);} catch (JWTVerificationException e) {throw new UnauthorizedException("无效令牌");}} }
配置要求 (application.yml):
yaml
jwt:secret-key: "${JWT_SECRET}" # 从环境变量获取
安全注意事项:
密钥管理:
生产环境必须通过环境变量注入密钥
本地开发使用
.env
文件(加入.gitignore
)
密钥生成:
bash
# 生成强密钥(32字节) openssl rand -base64 32
静态类限制:
无法直接使用Spring的依赖注入优势
不能动态刷新密钥(需要重启应用)
测试时需要额外初始化
替代方案建议:
java
// 更推荐的实例方法模式(非静态) @Service public class AuthService {private final JwtUtil jwtUtil; // 注入实例public String generateToken() {return jwtUtil.genToken(claims);} }
静态实现的优缺点:
优点:
无需注入即可调用
工具类方法调用简洁
适合简单工具类场景
缺点:
密钥在类加载后保持不变
无法利用Spring的配置刷新机制
多环境管理更复杂
测试时需手动模拟初始化
建议:在需要严格安全控制的场景(如生产环境),推荐使用非静态实现。静态实现更适合内部工具或低安全要求的场景。