JWT笔记

JWT笔记

一、简单介绍

JWT由三部分组成:
header,payload,Signature(头部,载荷,签证)

二、工具类

工具类是一个封装类,将一些对Jwt相关的操作进行整理,以便调用。

1. JwtToken创建

(不同版本,方法或有变动)调用构造方法:Jwts.builder();

该方法是链式调用,依次对jwt的三个组成部分进行配置。

  1. Header:
    头部主要声明类型和使用的加密算法(加密算法现在后自动添加,也可以主动声明)
  2. Payload:
    载荷由claims传入,.claims()方法接收Map类型,里面存入数据可自行定义(通常会存入用户名,id这些安全性要求不高的数据)
  3. Signature:
    签证由signWith()进行,参数为密钥和签名方法,该版本的密钥有所要求,需要符合一定要求(有些版本要求会低一些)
  4. 构建
    最后由.compact()进行构建,返回的token由根据 . 分隔的三部分组成。
    public String createToken(Map<String, Object> claims) {
        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() + expire * 1000L);

        try {
            return Jwts.builder()
                    .header() // 设置头部
                        .add("typ", "JWT") // 设置头部类型
                        .and()
                    .claims(claims) // 自定义负载
                    .issuedAt(nowDate)
                    .expiration(expireDate)
                    .signWith(getSigningKey(), Jwts.SIG.HS256)// 改为更安全的密钥处理方式SecretKey
                    .compact();
        } catch (IllegalStateException e) {
            LOGGER.error("创建JWT失败: 签名密钥不可用", e);
            throw e;
        } catch (Exception e){
            LOGGER.error("创建JWT失败", e);
            throw new RuntimeException("创建 JWT 失败", e);
        }
    }

2. 密钥构建

Keys.hmacShaKeyFor()将字节数组包装成SecretKey,即被用于签名的密钥;(推荐使用至少 32 字节,不符合安全检查会抛出异常)

所以我们的私钥如果不是字节数组,要主动将其转换(调用Decoders.BASE64.decode())

    private SecretKey getSigningKey(){
        if(secret == null || secret.trim().isEmpty()){
            LOGGER.error("JWT密钥未配置,请在配置文件中设置jwt.secret");
            throw new IllegalStateException("JWT密钥未配置,请在配置文件中设置jwt.secret");
        }
        try{
            byte[] keyBytes = Decoders.BASE64.decode(secret);//解码为字节数组
            return Keys.hmacShaKeyFor(keyBytes);//生成密钥
        } catch(IllegalArgumentException e){
            try{
                LOGGER.info("BASE64解码失败,尝试使用原始字符串作为密钥");
                return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));//备用方案,直接使用字符串的字节数组(不推荐)
            }catch (Exception ex){
                LOGGER.error("JWT密钥无效,请检查jwt.secret的配置", ex);
                throw new IllegalStateException("JWT密钥无效,请检查jwt.secret的配置", ex);
            }
        }
    }

3. token解析

固定的链式调用:

  1. 调用Jwts.parser()开启配置解析器,
  2. 调用.verifyWith()配置签名认证使用的密钥
  3. .build()返回已配置的 JwtParser 实例
  4. .parseSignedClaims()根据解析器执行解析(返回Jws用于后续操作)
  5. .getPayload()获取负载
    // 解析 JWT token,获取负载payload
    public Claims getTokenClaim(String token) {
        try {
            LOGGER.info("开始解析JWT:{}",token);
            return Jwts
                    .parser()
                    .verifyWith(getSigningKey()) // 使用 verifyWith(SecretKey),替代已弃用的 setSigningKey
                    .build()
                    .parseSignedClaims(token) // 使用 parseSignedClaims 方法解析签名的 JWT
                    .getPayload(); // 获取负载部分
        } catch (IllegalStateException e) {
            LOGGER.error("解析JWT失败: 签名密钥不可用", e);
            throw e;
        }  catch (Exception e) {
            LOGGER.info("JWT格式验证失败:{}",token);
            return null;
        }
    }

4. 读取负载信息

以过期时间为例,从解析到的负载中调用相关方法即可(claims.getExpiration())

    public Date getExpirationDateFromToken(String token) {
        try {
            Claims claims = getTokenClaim(token);
            return claims != null ? claims.getExpiration() : null;
        } catch (Exception e) {
            LOGGER.info("获取Token过期时间失败:{}", token);
            return null;
        }
    }

三、于SpringSecurity集成

1. token过滤器

在SpringSecurity的验证过滤器前新增过滤器用于token验证(原本我认为是要重写验证,网上方案却多是新增过滤器)

@Component
public class JwtValidationFilter extends OncePerRequestFilter{
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtValidationFilter.class);
    // 注入UserDetailsService,SecretKey,JwtUtil
    private final UserDetailsService userDetailsService;
    private final JwtUtil jwtUtil;
    @Value("${jwt.secret}")
    private String secretKey;

    @Autowired
    public JwtValidationFilter(UserDetailsService userDetailsService, JwtUtil jwtUtil) {
        this.userDetailsService = userDetailsService;
        this.jwtUtil = jwtUtil;
    }


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 0.获取请求token
        String token = request.getHeader("Authorization");
        // 1.检查请求头内Authorization信息
        try{
            // 1.1 如果没有,放行
            // 1.2 如果有,且不是以Bearer开头,放行
            if(StringTools.isBlank(token) || !token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)){
                filterChain.doFilter(request, response);
                return;
            }
            // 2.去除Bearer前缀
            token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
            // 3.解析 Token
            boolean isValidDate = jwtUtil.isTokenExpired(token);
            // 4.检查 Token有效,无效捕获并则抛出异常
            if(!isValidDate){
                LOGGER.error("JwtValidationFilter error: token is invalid");
                throw new RuntimeException(ResultEnum.UNAUTHORIZED.getMessage());
            }
            // 4.1.有效,进行验证
            Claims payloads = jwtUtil.getTokenClaim(token);//获取负载
            String username = payloads.getSubject();//都读取负载上用户名
            UserDetailsImpl userDetails = (UserDetailsImpl) userDetailsService.loadUserByUsername(username);//根据用户名加载用户信息
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());//创建认证令牌
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));//设置请求详情
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);//将认证令牌存入安全上下文
        } catch (Exception e) {
            // 4.2.无效,清除上下文并抛出异常
            LOGGER.error("JwtValidationFilter error: {}", e.getMessage());
            SecurityContextHolder.clearContext();
        }
        // 5.继续执行过滤链
        filterChain.doFilter(request,response);
    }
}

2. 配置SecurityConfig

在配置文件中设置token的过滤器

@Configuration
public class SecurityConfig {
    private final UserDetailsService userDetailsService;
    private final JwtUtil jwtUtil;

    @Autowired
    public SecurityConfig(UserDetailsService userDetailsService, JwtUtil jwtUtil) {
        this.userDetailsService = userDetailsService;
        this.jwtUtil = jwtUtil;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 也可用有参构造,取值范围是 4 到 31,默认值为 10。数值越大,加密计算越复杂
        return new BCryptPasswordEncoder();	
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
    
    //过滤链配置
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http
            .authorizeHttpRequests(authorize -> authorize //开启授权
                .requestMatchers("/login","/register","/api/register", "/register.html", "/api/login") //放行请求
                .permitAll() //允许所有人访问
                .anyRequest() //允许所有请求
                .authenticated() //认证后访问自动授权
            )
            .formLogin(Customizer.withDefaults()) //使用默认的登陆登出页面进行授权登陆
            .rememberMe(Customizer.withDefaults()); // 启用“记住我”功能
        http.csrf(csrf -> csrf.disable()); //关闭csrf
        http.addFilterBefore(new JwtValidationFilter(userDetailsService, jwtUtil), UsernamePasswordAuthenticationFilter.class); //添加JWT过滤器
        return http.build();
    }
}

3. 登录生成token

现在有了验证流程,生成token的流程还没有加入。

  1. 在登录模块调用token的生成方法
  2. 将生成的token封装进dto中(封装的数据结构)
    @Override
    public LoginDto login(String username, String password) {

        //TODO 0.准备工作,注入AuthenticationManager、SecurityProperties
        LoginDto loginDto = new LoginDto();
        String token;
        Long expiration;
        try{
            //TODO 1.调用认证方法
            Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
            SecurityContextHolder.getContext().setAuthentication(authentication);
            //TODO 2.生成JWT
            token = jwtUtil.createToken(authentication);

            loginDto.setToken(token);
        }catch (Exception e){
            LOGGER.error("登录异常:{}", e.getMessage());
            throw new ApiException(e.getMessage());
        }
        //TODO 3.返回封装信息
        return loginDto;
    }
posted @ 2025-11-14 22:39  Insanial  阅读(2)  评论(0)    收藏  举报