Spring security 自定义 token 身份验证
👉 请投票支持这款 全新设计的脚手架 ,让 Java 再次伟大!
前言
既然 Spring securtiy 的核心是 Filter chains,那我们只需要定义一个符合 security 标准的 filter ,再把自己的 chain 加入到 VirtualFilterChain 里面,就可以自定义身份验证的认证逻辑了。
之前提到过,大部分身份认证相关的过滤器都继承了 AbstractAuthenticationProcessingFilter。但是这一次我们让接下来的自定义过滤器不选择继承它,而是继承 OncePerRequestFilter。
这个类之前我们没有见过,其实它也是 security 框架提供的一个 VirtualFilter 父类。继承一个陌生的类听起来有点吓人,不过很快你就知道 AbstractAuthenticationProcessingFilter 也好,OncePerRequestFilter 也罢,都是 filter 而已。
自定义一个 Filter
我们参照 security 的基础身份认证过滤器 UsernamePasswordAuthenticationFilter 来编写我们的过滤器。这是不是意味着我们也必须使用模板设计模式来编写自定义过滤器的父类,然后聚合 providerManager 再自定义 provider 实现一整套 security 范式的身份认证代码?
不是的,你完全可以把 provider 的逻辑全部抽离出来加入到你的自定义过滤器里面。实际上我的过滤器一共就三个逻辑。非常简单并且可以工作的很好!
- 通过确认 token 的有效性来验证请求的身份。类似于 DaoAuthenticationProvider.additionalAuthenticationChecks。
- 从数据库检索出用户的详情,方便后续取用。类似于 DaoAuthenticationProvider. retrieveUser()。
- 将验证通过的身份资产(principal)封装到 UsernamePasswordAuthenticationToken,并设置到 security context。类似于 AbstractAuthenticationProcessingFilter.successfulAuthentication。
@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain)throws ServletException, IOException {String header = request.getHeader(HttpHeaders.AUTHORIZATION);if (StringUtils.isEmpty(header) || !header.startsWith(BEARER_SPACE)) {globalExceptionHandler.commence(request, response, new AuthenticationException("非法的 token 请求参数") {});return;}String token = header.split(" ")[1].trim();try {TokenUtil.verifyToken(token, base64Secret);} catch (FoundationAppException e) {globalExceptionHandler.commence(request, response, new AuthenticationException("非法的 token") {});return;}// 返回 401// jwt 认证通过即代表认证成功。将 principal 资源装配到 contextUserDetails userDetails = userAppService.loadUserByUsername(TokenUtil.getUsername(token) + "@" + TokenUtil.getDomain(token));UsernamePasswordAuthenticationTokenauthentication = new UsernamePasswordAuthenticationToken(userDetails, null,userDetails.getAuthorities());if (!userDetails.isEnabled()) {globalExceptionHandler.commence(request, response, new DisabledException("当前账户已锁定"));return;}authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authentication);chain.doFilter(request, response);}
实现 loadByUsername
让我们的任何一个类——我建议是和 user 相关处理的 service 类实现 UserDetailsService 接口,就可以重写 loadByUsername 方法了。
实现这个接口很重要,因为他定义了 UserDetails 接口——这个你的用户对象应该遵守的规范和行为。
@Overridepublic UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {String[] split = name.split("@");String username = split[0];String domainCode = split[1];Domain domain = domainRepository.findByCode(domainCode).orElseThrow(() -> new UsernameNotFoundException(String.format("User with domain - %s, not found", domainCode)));AuthUser user = authUserRepository.findByDomainAndUsername(domain, username).orElseThrow(() -> new UsernameNotFoundException(String.format("User with username and domain - %s, not found", name)));return user;}
考虑一下异常处理
自定义 token 过滤器的工作这就基本完成了。不过健壮的处理程序,往往都伴随着异常逻辑的完美捕获。回忆之前的内容,filter 的异常处理都是通过端点处理程序——AuthenticationEntryPointFailureHandler 来处理的。所以只要我们自定义端点处理程序并重写 commence 方法就完全可以达到一样的效果。
AuthenticationEntryPointFailureHandler
@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,AuthenticationException exception) throws IOException, ServletException {this.authenticationEntryPoint.commence(request, response, exception);}
自定义异常处理程序
@Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {log.error("spring security 认证发生异常。", e);HttpResponseWriter.sendError(httpServletResponse, HttpServletResponse.SC_UNAUTHORIZED, "身份认证失败");}@Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException {log.error("spring security 鉴权发生异常。", e);HttpResponseWriter.sendError(httpServletResponse, HttpServletResponse.SC_FORBIDDEN, "鉴权失败");}
将自定义内容装配进 security 架构
要想我们的过滤器先一步生效,需要将过滤器加入到 security 自带的认证过滤器之前;接着还需要把自定义异常配置到异常处理流程中。是的,就这些工作内容,没有更多了。
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserAppService userAppService;@Autowiredprivate JwtTokenFilter jwtTokenFilter;@Autowiredprivate GlobalExceptionHandler globalExceptionHandler;/*** 配置密码加密装置*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 配置 Dao Authenticationprovider 协调器*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// configure authentication managerauth.userDetailsService(userAppService);}@Overridepublic void configure(WebSecurity web) {web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**").antMatchers("/swagger-ui/index.html").antMatchers("/test/**");}/*** web 安全配置*/@Overrideprotected void configure(HttpSecurity http) throws Exception {// Enable CORS and disable CSRFhttp = http.cors().and().csrf().disable();// Set session management to statelesshttp = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();// exception handlerhttp.exceptionHandling().authenticationEntryPoint(globalExceptionHandler).accessDeniedHandler(globalExceptionHandler);// Set permissions on endpointshttp.authorizeRequests()// Our public endpoints.antMatchers("/foundation/api/example/**").permitAll().anyRequest().authenticated();// Add JWT token filterhttp.addFilterBefore(jwtTokenFilter,UsernamePasswordAuthenticationFilter.class);}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
结语
这就是自定义 token 身份验证与配置的全部工作内容。由于 spring security 大量使用了组合与设计模式使得任何开发者都可以定义出各式各样的身份验证逻辑,所以只要你的自定义程序可以嵌合到 security 框架里并且可以很好的工作,那他就是适合你的身份验证。