在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应用中得到安全管理和使用。
一、非静态实现
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 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
java
@RestController
@RequestMapping("/account")
@Slf4j
public class AccountController {
@Autowired
private 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
java
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 {
@Autowired
private JwtUtil jwtUtil;
/**
* 预处理请求
* 在请求到达 Controller 方法之前执行,当请求进入 Spring MVC 的 DispatcherServlet 后,首先会经过拦截器的 preHandle 方法
* @param request 请求对象
* @param response 响应对象
* @param handler 处理器对象
* @return 是否放行请求,返回 true 表示继续处理请求(放行),false 表示中断请求(不放行,需自行处理响应)
* @throws Exception 异常
*/
@Override
public 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 令牌数据存储到 ThreadLocal
ThreadLocalUtil.set(claims);
// 放行
return true;
} catch (Exception e) {
// http 响应状态码为 401
response.setStatus(401);
// 不放行
return false;
}
}
/**
* 后处理请求
* 在请求处理完成之后执行,包括 Controller 方法执行完毕、视图渲染完成(如返回 JSON 或 HTML)之后才会触发
* @param request 请求对象
* @param response 响应对象
* @param handler 处理器对象
* @param exception 异常
* @throws Exception 异常
*/
@Override
public 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;
}
}
// 初始化检查
@PostConstruct
private 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的配置刷新机制
-
多环境管理更复杂
-
测试时需手动模拟初始化
建议:在需要严格安全控制的场景(如生产环境),推荐使用非静态实现。静态实现更适合内部工具或低安全要求的场景。