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); // 每次启动变化,不适合生产
}

安全最佳实践

  1. 密钥强度:HS256算法至少32字符,推荐64字符随机字符串

    bash

    复制代码
    # 生成强密钥(Linux/Mac)
    openssl rand -base64 32
  2. 密钥轮换:通过密钥管理服务实现定期轮换

  3. 访问控制

    • 禁止日志打印密钥

    • 应用配置最小权限原则

  4. 配置文件安全

    properties

    复制代码
    # 禁止提交敏感数据到仓库
    /src/main/resources/application*.yml -> .gitignore

不同环境配置示例

环境 存储位置 注入方式
本地开发 .env 文件 Spring Boot @Value
测试环境 CI/CD 管道变量 部署脚本注入
生产环境 AWS Secrets Manager/Hashicorp Vault SDK动态获取

关键提示:永远不要将真实密钥提交到代码仓库!Spring Boot的配置外部化机制(优先级从高到低):

  1. 命令行参数 --jwt.secret-key=xxx

  2. 环境变量 JWT_SECRET_KEY

  3. 配置文件 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工具类未初始化完成,请确保已注入密钥");
        }
    }
}

关键实现说明:

  1. 安全注入机制

    java

    复制代码
    @Value("${jwt.secret-key}")
    public void setSecretKey(String secretKey) {
        if (SECRET_KEY == null) { // 确保密钥只设置一次
            SECRET_KEY = secretKey;
        }
    }
  2. 初始化验证

    java

    复制代码
    @PostConstruct
    private void init() {
        if (SECRET_KEY == null || SECRET_KEY.isBlank()) {
            throw new IllegalStateException("JWT密钥未正确配置");
        }
    }
  3. 使用前检查

    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}"  # 从环境变量获取

安全注意事项:

  1. 密钥管理

    • 生产环境必须通过环境变量注入密钥

    • 本地开发使用.env文件(加入.gitignore

  2. 密钥生成

    bash

    复制代码
    # 生成强密钥(32字节)
    openssl rand -base64 32
  3. 静态类限制

    • 无法直接使用Spring的依赖注入优势

    • 不能动态刷新密钥(需要重启应用)

    • 测试时需要额外初始化

  4. 替代方案建议

    java

    复制代码
    // 更推荐的实例方法模式(非静态)
    @Service
    public class AuthService {
        private final JwtUtil jwtUtil; // 注入实例
        
        public String generateToken() {
            return jwtUtil.genToken(claims);
        }
    }

静态实现的优缺点:

优点

  • 无需注入即可调用

  • 工具类方法调用简洁

  • 适合简单工具类场景

缺点

  • 密钥在类加载后保持不变

  • 无法利用Spring的配置刷新机制

  • 多环境管理更复杂

  • 测试时需手动模拟初始化

建议:在需要严格安全控制的场景(如生产环境),推荐使用非静态实现。静态实现更适合内部工具或低安全要求的场景。

相关推荐
思通数据1 小时前
AI视频监控:重构安防行业智能化新生态
人工智能·安全·目标检测·机器学习·计算机视觉·重构·数据挖掘
独泪了无痕3 小时前
一文搞懂MyBatis中的TypeHandler
数据库·后端·mybatis
程序员岳焱5 小时前
Java 调用 Python 脚本:实现 HelloWorld
java·后端·python
Giser探索家6 小时前
低空智航平台技术架构深度解析:如何用AI +空域网格破解黑飞与安全管控难题
大数据·服务器·前端·数据库·人工智能·安全·架构
Jewel Q6 小时前
终端安全检测和防御技术
安全
独行soc7 小时前
2025年大模型安全岗的面试汇总(题目+回答)
android·人工智能·安全·面试·职场和发展·渗透测试
快乐就是哈哈哈7 小时前
Java 短信验证码实战:发送、验证、防刷一步到位
后端
快乐就是哈哈哈7 小时前
Java 开发必备:注册登录功能 + MD5 盐值加密 + 异常处理实现
后端