迎着风跑  

Spring Security 整合 JWT

为了在前后端分离项目中使用 JWT ,我们需要达到 2 个目标:

  1. 在用户登录认证成功后,需要返回一个含有 JWT token 的 json 串。

  2. 在用户发起的请求中,如果携带了正确合法的 JWT token ,后台需要放行,运行它对当前 URI 的访问。

#1. 返回 JWT token

Spring Security 中的登录认证功能是由 UsernamePasswordAuthenticationFilter 完成的,默认情况下,在登陆成功后,接下来就是页面跳转,显示你原本想要访问的 URI( 或 / ),现在,我们需要返回 JSON(其中还要包含 JWT token )。

Spring Security 支持通过实现 AuthenticationSuccessHandler 接口,来自定义在登陆成功之后你所要做的事情(之前有讲过这部分内容)

http.formLogin()
    .successHandler(new JWTAuthenticationSuccessHandler());
Copied!
public class JWTAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest req,HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {

        // authentication 对象携带了当前登陆用户名等相关信息
        User user = (User) authentication.getPrincipal();

        resp.setContentType("application/json;charset=UTF-8");
        String jwtStr = ...;
        String jsonStr = ...;

        PrintWriter out = resp.getWriter();
        out.write(jsonStr);
        out.flush();
        out.close();
    }
}
Copied!

#2. 放行携带 JWT Token 的请求

放行请求的关键在于 FilterSecurityInterceptor 不要抛异常,而 FilterSecurityInterceptor 不抛异常则需要满足两点:

  1. Spring Security 上下文( Context ) 中要有一个 Authentication Token ,且因该是已认证状态。

  2. Authentication Token 中所包含的 User 的权限信息要满足访问当前 URI 的权限要求。

所以实现思路的关键在于:在 FilterSecurityInterceptor 之前( 废话 )要有一个 Filter 将用户请求中携带的 JWT 转化为 Authentication Token 存在 Spring Security 上下文( Context )中给 “后面” 的 FilterSecurityInterceptor 用。

基于上述思路,我们要实现一个 Filter :

@Component
public class JwtFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 在【我】之前已经有过滤器为 FilterSecurityInterceptor 做好了准备工作,那么【我】就啥事不干了嘛。
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            log.info("Security Context 中已有 Authentication Token");
            filterChain.doFilter(request, response);
            return;
        }

        // 【我】来为 FilterSecurityInterceptor 创造 Authentication Token 。
        String jwtStr = request.getHeader("x-jwt-token");

        if (!StringUtils.hasText(jwtStr)) {
            // 请求头中无 username 和 userid,那么逻辑上,那么当前请求就是一个无须鉴权就能访问的请求(例如,匿名可访问)。
            filterChain.doFilter(request, response);
            return;
        }

        String username = ... ;

        // 从数据库中查用户信息只是方案之一,你的可以将用户信息(特别是权限信息存在别处)
        UserDetails user = userDetailsService.loadUserByUsername(username);
        // 生成 Authentication Token 并存入 Spring Security 上下文中
        authentication = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 放行请求,让【最后一个过滤器】触发执行:从公共区取出 Authentication Token 作鉴权操作。
        filterChain.doFilter(request, response);
    }

}
Copied!

虽然 Spring Security Filter Chain 对过滤器没有特殊要求,只要实现了 Filter 接口即可,但是在 Spring 体系中,推荐使用 OncePerRequestFilter 来实现,它可以确保一次请求只会通过一次该过滤器(而普通的 Filter 并不能保证这一点)

配置:

http.formLogin().successHandler(new JWTAuthenticationSuccessHandler());
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.csrf().disable();

http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
 
posted on 2021-12-04 14:24  迎着风跑  阅读(196)  评论(0编辑  收藏  举报