SpringSecurity 自定义JWT验证
本文最后更新于 88 天前,其中的信息可能已经有所发展或是发生改变。

之前写过一次 SpringSecurity JWT 验证相关的博客,但是实现得有些潦草。

于是本篇将对实现方式和代码逻辑进行一些优化。

依赖导入

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<!--解决 javax.xml.bind.DatatypeConverter (JDK9 之后此包已被删除)-->
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>

准备工作

首先需要创建一个用于生成JWT的工具类:

public final class JwtUtil {
    private JwtUtil() {

    }

    /**
     * 构建 Jwt Token
     * @param authorities 用户所有的权限
     * @param authentication Authentication
     * @param expiration Token 过期时间 单位毫秒
     * @return Token 字符串
     */
    public static String buildJwtToken(String signKey, Collection<? extends GrantedAuthority> authorities, Authentication authentication, long expiration) {
        StringBuilder authorityStr = new StringBuilder();
        for (GrantedAuthority authority : authorities) {
            authorityStr.append(authority).append(",");
        }

        return Jwts.builder()
                .claim("authorities", authorityStr)
                .setSubject(authentication.getName())
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(SignatureAlgorithm.HS512, signKey)
                .compact();
    }
}

Security 配置

基本的配置没什么好说的,注释里已经写的很清楚了。

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        // 需要放行的接口列表
        List<RequestMatcher> requestMatchers = new ArrayList<>();
        requestMatchers.add(new AntPathRequestMatcher("/login", HttpMethod.POST.name()));

        // 放行接口
        http.authorizeHttpRequests(registry -> {
            requestMatchers.forEach(e -> registry.requestMatchers(e).permitAll());
            registry.anyRequest().authenticated();
        });

        // JWT 验证无需 Session
        http.sessionManagement(it -> it.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        // 禁用 BasicAuth
        http.httpBasic(AbstractHttpConfigurer::disable);

        // 禁用 CSRF
        http.csrf(AbstractHttpConfigurer::disable);

        // 禁用跨域
        http.cors(AbstractHttpConfigurer::disable);

        // TODO: 自定义过滤链配置

        return http.build();
    }

    /**
     * 由 AuthenticationConfiguration 得到 AuthenticationManager
     * @param authenticationConfiguration AuthenticationConfiguration
     * @return AuthenticationManager
     */
    @Bean
    @SneakyThrows
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 使用 BCryptPasswordEncoder 作为密码加密方式
     * @return BCryptPasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

登录过滤器

接下来解决登录的问题,这次我们继承 AbstractAuthenticationProcessingFilter 来处理登录请求。

基本思路是:让 SpringSecurity 的 UserDetailsService 来验证用户密码是否正确,然后通过这个过滤器生成一个token令牌,再将结果写回给接口。

public class CustomLoginFilter extends AbstractAuthenticationProcessingFilter {

    public CustomLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
        super(defaultFilterProcessesUrl, authenticationManager);
    }

    /**
     * 用户验证逻辑
     * @param request HttpServletRequest
     * @param response HttpServletResponse
     * @return Authentication
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(username, password));
    }

    /**
     * 验证成功后的逻辑 成功则返回 jwt token
     * @param request HttpServletRequest
     * @param response HttpServletResponse
     * @param chain FilterChain
     * @param authResult Authentication
     * @throws IOException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
        Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();

        response.setContentType("application/json;charset=utf-8");
        String userToken = JwtUtil.buildJwtToken("jwtSignKey", authorities, authResult, 15 * 60 * 1000);
        response.getWriter().write(userToken);
        response.getWriter().flush();
        response.getWriter().close();
    }

    /**
     * 验证失败后的逻辑
     * @param request HttpServletRequest
     * @param response HttpServletResponse
     * @param failed AuthenticationException
     * @throws IOException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write("Username or password incorrect");
        response.getWriter().flush();
        response.getWriter().close();
    }
}

attemptAuthentication

在这个方法中,前端调用 /login 接口传来的用户名密码可以通过 HttpServletRequest 来获取到,然后构建一个 UsernamePasswordAuthenticationToken 交给验证管理器进行验证。

如果经过 UserDetailsService 获取到了用户并且密码正确,则调用下面的 successfulAuthentication() 方法,失败则调用 unsuccessfulAuthentication() 方法

接口验证过滤器

接口验证很简单,只要拦截每一个请求,并且进行token验证即可,所以使用 GenericFilterBean 即可。

先把基本的框架搭建起来吧:

public class AuthorizationFilter extends GenericFilterBean {
    private final Iterable<RequestMatcher> permitAllMatchers;

    public AuthorizationFilter(Iterable<RequestMatcher> permitAllMatchers) {
        this.permitAllMatchers = permitAllMatchers;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        // 此过滤链在 FilterSecurityInterceptor 之前 因此检查 Token 前需要先排除已放行的路径
        for (RequestMatcher matcher : permitAllMatchers) {
            if (matcher.matches(request)) {
                filterChain.doFilter(request, response);
                return;
            }
        }

    }
}

这里我添加了一个构造函数用于接收 RequestMatcher 对象,因为这个过滤链最终会被添加到 FilterSecurityInterceptor 之前,而之前在 Security 配置中放行的接口会失效。

因此只需要经过这个过滤链时再判断一下,如果符合条件就直接放行。

接下来继续把Token验证的逻辑写完:

public class AuthorizationFilter extends GenericFilterBean {
    private final Iterable<RequestMatcher> permitAllMatchers;

    public AuthorizationFilter(Iterable<RequestMatcher> permitAllMatchers) {
        this.permitAllMatchers = permitAllMatchers;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        // 此过滤链在 FilterSecurityInterceptor 之前 因此检查 Token 前需要先排除已放行的路径
        for (RequestMatcher matcher : permitAllMatchers) {
            if (matcher.matches(request)) {
                filterChain.doFilter(request, response);
                return;
            }
        }

        String tokenStr = request.getHeader("Authorization");
        try {
            if (tokenStr == null) {
                throw new IllegalStateException("Token invalid");
            }
            Claims claims = Jwts.parser()
                    // 必须与 CustomLoginFilter 中的 key 保持一致
                    .setSigningKey("jwtSignKey")
                    .parseClaimsJws(tokenStr.replace("Bearer",""))
                    .getBody();
            String username = claims.getSubject();
            List<? extends GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"));
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities);
            SecurityContextHolder.getContext().setAuthentication(token);
            filterChain.doFilter(request, servletResponse);
        } catch (SignatureException | ExpiredJwtException | MalformedJwtException e) {
            // 当 Token 无法被解析时抛出 SignatureException 异常
            // JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.

            // 当 Token 格式不正确时抛出 MalformedJwtException 异常
            // JWT strings must contain exactly 2 period characters. Found: 0

            // e.printStackTrace();
            servletResponse.setContentType("application/json;charset=utf-8");
            // 将请求失败的信息写回给接口
            servletResponse.getWriter().write("Invalid token");
            servletResponse.getWriter().flush();
            servletResponse.getWriter().close();
        }
    }
}

添加过滤器

// 由自定义的登录验证过滤器返回 jwt token
http.addFilterBefore(
        new CustomLoginFilter("/login", authenticationConfiguration.getAuthenticationManager()),
        UsernamePasswordAuthenticationFilter.class
);
// 验证用户 Token 是否有效
http.addFilterBefore(new AuthorizationFilter(requestMatchers), UsernamePasswordAuthenticationFilter.class);

将这段代码添加到上面的 TODO 后面即可。

接口测试

POST http://localhost:8080/login

username: lovelycat

password: lovelycat@2024

这里记得替换成你自己的数据

得到的结果如下 eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IiIsInVpZCI6MTgxOTI2ODgyOTA3MDkwNTM0Niwic3ViIjoibG92ZWx5Y2F0IiwiZXhwIjoxNzIyNTgzNTUyfQ.8EHPJkyERrXmot_N6F5wgB2-d-u-Sr7m8O0gfLhTDh875vtQX8rtzJVvTOW7CL5HuUrBqF2gIZWW25eF6kj-_w

未经允许禁止转载本站内容,经允许转载后请严格遵守CC-BY-NC-ND知识共享协议4.0,代码部分则采用GPL v3.0协议
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇