Java框架快速入门: Spring Security+OAuth2之实现JWT认证过滤器

概述

在现代微服务与前后端分离架构中,无状态的JWT(JSON Web Token)认证方式已成为主流选择。Spring Security 作为Java生态下强大的安全框架,通过自定义过滤器可以优雅地集成JWT认证。

本文将聚焦于如何构建一个自定义的 JwtFilter,负责从请求中提取并校验JWT,进而完成认证信息的设置,实现完全无状态的API安全保护。

纲要

  • 核心概念:JwtFilterSecurityContextHolderOncePerRequestFilter
  • 关键流程:检查请求头 → 提取Token → 解析Token → 构建认证对象 → 写入安全上下文
  • 涉及组件:
    • JwtProperties:可配置的请求头名称与Token前缀
    • JwtUtils:Token解析工具
    • JwtFilter:认证过滤器实现
    • SecurityConfig:将过滤器注册到过滤器链
  • 补充内容:完整的代码实现、项目结构、流程序列图

背景与设计思路

在Spring Security体系中,认证信息最终会被封装为一个 Authentication 对象并存储在 SecurityContextHolder 中。对于JWT这种无状态认证,每个请求都必须携带一个有效Token。自定义过滤器需要拦截所有请求,完成以下工作:

  1. 从HTTP请求的指定头(默认为 Authorization)中取出Token;
  2. 验证Token格式(是否以指定前缀开头,如 Bearer );
  3. 解析Token并提取声明(claims),如用户名、角色等;
  4. 利用声明构造 UsernamePasswordAuthenticationToken 并标记为已认证;
  5. 将认证对象设置到 SecurityContext 中,供后续过滤器及业务代码使用。

若Token非法或缺失,则应清空安全上下文,保证不会遗留错误的认证状态。整个过程无需数据库查询,因为JWT的自包含特性已经携带了所需的用户身份与权限信息。

项目结构

下面展示本文示例代码的包结构,所有类均放在 com.example.security.jwt 包下。

com.example.security.jwt
├── JwtProperties.java
├── JwtUtils.java
├── JwtFilter.java
└── SecurityConfig.java

核心代码实现

可配置的属性类 JwtProperties

为了让请求头的名称和Token前缀可灵活调整,我们将其抽取为配置属性。这样做的好处是,如果未来需要改用 X-Auth-Token 或自定义前缀,只需修改配置文件即可。

package com.example.security.jwt;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
    /** 存放JWT的请求头名称,默认为 Authorization */
    private String header = "Authorization";
    /** Token的前缀,默认为 "Bearer " (注意末尾空格) */
    private String tokenPrefix = "Bearer ";
    /** JWT签名密钥,实际使用时应为高强度随机字符串并外部化 */
    private String secret = "DefaultSecretKey";

    // getters and setters...
    public String getHeader() { return header; }
    public void setHeader(String header) { this.header = header; }
    public String getTokenPrefix() { return tokenPrefix; }
    public void setTokenPrefix(String tokenPrefix) { this.tokenPrefix = tokenPrefix; }
    public String getSecret() { return secret; }
    public void setSecret(String secret) { this.secret = secret; }
}

JWT工具类 JwtUtils

提供解析JWT并返回声明(Claims)的能力。此处为简化展示,直接使用静态工具方法;实际项目可将密钥通过 JwtProperties 注入,避免静态持有密钥。

package com.example.security.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class JwtUtils {

    private final JwtProperties jwtProperties;

    public JwtUtils(JwtProperties jwtProperties) {
        this.jwtProperties = jwtProperties;
    }

    /**
     * 解析JWT令牌,返回包含声明的Optional对象。
     * 若令牌无效或签名不匹配,则返回空Optional。
     */
    public Optional<Claims> parseToken(String token) {
        try {
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(jwtProperties.getSecret().getBytes())
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
            return Optional.of(claims);
        } catch (JwtException e) {
            return Optional.empty();
        }
    }
}

类型转换辅助工具

JWT的声明中,“authorities”字段通常以List形式存储,但解析出来可能是 ArrayList 或JSON数组,需要统一处理为List后再转换为 SimpleGrantedAuthority。下面提供一个简单的转换工具方法,放在 JwtFilter 同级包下即可。

package com.example.security.jwt;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class CollectionUtil {
    @SuppressWarnings("unchecked")
    public static List<?> convertObjectToList(Object obj) {
        if (obj instanceof List) {
            return (List<?>) obj;
        } else if (obj instanceof Collection) {
            return new ArrayList<>((Collection<?>) obj);
        } else if (obj != null && obj.getClass().isArray()) {
            return new ArrayList<>(List.of((Object[]) obj));
        }
        return new ArrayList<>();
    }
}

认证过滤器 JwtFilter

这是整个JWT认证的核心。过滤器继承自 OncePerRequestFilter,确保每个请求只会被该过滤器处理一次。

package com.example.security.jwt;

import io.jsonwebtoken.Claims;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class JwtFilter extends OncePerRequestFilter {

    private final JwtProperties jwtProperties;
    private final JwtUtils jwtUtils;

    public JwtFilter(JwtProperties jwtProperties, JwtUtils jwtUtils) {
        this.jwtProperties = jwtProperties;
        this.jwtUtils = jwtUtils;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        // 第一步:检查请求是否携带合法的JWT头
        if (isJwtHeaderPresent(request)) {
            // 第二、三步:解析Token并设置安全上下文
            parseAndSetAuthentication(request);
        } else {
            // 未携带合法头部时,清空上下文,避免线程残留
            SecurityContextHolder.clearContext();
        }
        filterChain.doFilter(request, response);
    }

    /**
     * 检查请求头中是否存在合法的JWT头部。
     * 判断依据:头部值不为空,且以配置的前缀开头。
     */
    private boolean isJwtHeaderPresent(HttpServletRequest request) {
        String headerValue = request.getHeader(jwtProperties.getHeader());
        return headerValue != null && headerValue.startsWith(jwtProperties.getTokenPrefix());
    }

    /**
     * 从请求头中提取Token,解析并构建认证对象设置到安全上下文。
     */
    private void parseAndSetAuthentication(HttpServletRequest request) {
        String headerValue = request.getHeader(jwtProperties.getHeader());
        // 去除前缀获得纯Token
        String token = headerValue.replace(jwtProperties.getTokenPrefix(), "");

        Optional<Claims> claimsOptional = jwtUtils.parseToken(token);
        claimsOptional.ifPresentOrElse(
                this::setupAuthentication,          // 有值的情况
                SecurityContextHolder::clearContext  // 解析失败或无Claims,清空上下文
        );
    }

    /**
     * 使用解析出的 Claims 构造认证对象并写入 SecurityContext。
     */
    private void setupAuthentication(Claims claims) {
        // 提取角色列表,并转换为 GrantedAuthority 集合
        List<?> rawAuthorities = CollectionUtil.convertObjectToList(claims.get("authorities"));
        List<SimpleGrantedAuthority> authorities = rawAuthorities.stream()
                .map(String::valueOf)
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());

        // 构建已认证的 Authentication 对象
        UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(
                        claims.getSubject(),   // principal: 用户名
                        null,                  // credentials: 无需凭据
                        authorities            // 权限集合
                );
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }
}

代码关键点:

  • isJwtHeaderPresent 仅做格式校验,不涉及签名验证。
  • 提取Token后,利用 JwtUtils 解析出 Claims
  • ifPresentOrElse 方法将处理逻辑分为有值与无值两条路径,符合函数式风格,代码清晰。
  • 认证对象通过 UsernamePasswordAuthenticationToken 的三参数构造器创建,该构造器会调用 setAuthenticated(true),表示已认证。
  • 若无有效Claims,则清空 SecurityContext,防止上下文污染。

配置类 SecurityConfig

必须将自定义的 JwtFilter 添加到Spring Security的过滤器链中,通常置于 UsernamePasswordAuthenticationFilter 之前,以便提前完成JWT认证。

package com.example.security.jwt;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {

    private final JwtProperties jwtProperties;
    private final JwtUtils jwtUtils;

    public SecurityConfig(JwtProperties jwtProperties, JwtUtils jwtUtils) {
        this.jwtProperties = jwtProperties;
        this.jwtUtils = jwtUtils;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态
            .and()
            .authorizeRequests()
            .antMatchers("/auth/login").permitAll()   // 假设登录接口公开
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public JwtFilter jwtFilter() {
        return new JwtFilter(jwtProperties, jwtUtils);
    }
}

至此,JWT过滤器及其配套组件已经搭建完毕。当客户端访问受保护资源时,只需在请求头中携带 Authorization: Bearer <your_jwt> 即可通过认证。

流程可视化

以下序列图展示了 JwtFilter 工作的核心流程:

FilterChainSecurityContextJwtUtilsJwtFilterClientFilterChainSecurityContextJwtUtilsJwtFilterClientalt[Claims非空][Claims为空]alt[头部合法][头部缺失或格式错误]HTTP Request (带 Authorization 头)检查请求头是否存在且以指定前缀开头去除前缀,提取纯TokenparseToken(token)返回 Optional<Claims>从 Claims 取出用户名、角色,构造 AuthenticationsetAuthentication(authentication)clearContext()clearContext()调用 doFilter() 继续链处理

测试验证

你可以通过以下步骤验证过滤器的正确性:

  1. 启动应用,向 /auth/login 发送登录请求,获得JWT Token(该端点及Token生成逻辑需自行实现)。
  2. 使用Postman或curl访问其他受保护接口,请求头设置:
    Authorization: Bearer <your_token>
  3. 若不携带或携带错误Token,应当收到 403 响应,且日志中不会看到异常(过滤器已清空上下文,后续权限校验会拒绝访问)。
  4. 携带正确Token,可正常访问资源,并在业务代码中通过 SecurityContextHolder.getContext().getAuthentication() 获取当前用户信息。

总结

本文详细介绍了在Spring Security框架下自定义JWT认证过滤器的实现方式。通过对请求头的检测、Token的解析以及安全上下文的设置,我们构建了一个无状态、轻量级的认证机制。

文中所有代码均可直接复制运行(需引入 jjwt 等依赖),并遵循了配置外部化、关注点分离的最佳实践。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值