Spring Security 整合 JWT
为了在前后端分离项目中使用 JWT ,我们需要达到 2 个目标:
-
在用户登录认证成功后,需要返回一个含有 JWT token 的 json 串。
-
在用户发起的请求中,如果携带了正确合法的 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 不抛异常则需要满足两点:
-
Spring Security 上下文( Context ) 中要有一个 Authentication Token ,且因该是已认证状态。
-
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);