回到序章
来来来,点这
流程详解
在介绍认证流程的时候,整理过一个表单登陆的大致流程如下:
- 终端发起登陆请求
- 认证过滤器
UsernamePasswordAuthenticationFilter 拦截到请求,处理认证逻辑,在这里获得登陆信息,根据获得的登陆信息创建出未认证的 Authentication( 这里对应的是 UsernamePasswordAuthenticationToken 对象 ),然后将对象交给认证管理器
- 认证管理器循环所有的
AuthenticationProvider,找到对应的 provider 进行认证
3.1. 认证管理器 AuthenticationManager 维护着许多 AuthenticationProvider,这些 AuthenticationProvider 分别用来认证不同类型的请求,例如:表单、第三方 等
- 认证成功后,生成一个已认证的
Authentication,并将其通过 SecurityContextHolder.getContext().setAuthentication(authResult); 写到 SecurityContext 中
在之前实现验证码校验,是通过加一个过滤器,然后通过 http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class); 将过滤器加到 spring security 的过滤器链里。但这种有个不太好的地方,你可以挂个 debug 在这个过滤器的代码里,可以发现每触发一次请求,就会进一次这串代码。
当然这种方式是可行的,反正也就是每次请求多一个判断,符合登陆的就处理,不符合的就抛掉,功能实现了也没啥好纠结的。
不过这里主要目的还是提一下另一种,就是自定义 AuthenticationProvider。
首先先看一下 AuthenticationProvider:
1 2 3 4 5 6 7 8 9 10
| package org.springframework.security.authentication;
import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException;
public interface AuthenticationProvider { Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1); }
|
其中,authenticate() 方法就是拿来做认证,检验用户传进来的数据和合法性,然后返回一个 Authentication,里面装着用户信息;而 supports() 是用来判断当前的 AuthenticationProvider 能否支持对应的 Authentication。
通过 IDEA 生成 Authentication 的继承和实现图如下:

再看看 AuthenticationProvider,因为结构图有点大,可以自己去折腾着玩( 右键类,选择 Diagrams,出来的图里右键该类图,点击 Show Implementations,然后选出来就完了 ),这里以 UsernamePasswordAuthenticationToken 往上推进行介绍,首先找到 supports() 方法内使用到这个类的 Provider,有如下:
- [abstract] AbstractUserDetailsAuthenticationProvider
- DaoAuthenticationProvider [extends AbstractUserDetailsAuthenticationProvider]
- [abstract] AbstractJaasAuthenticationProvider
- DefaultJaasAuthenticationProvider extends AbstractJaasAuthenticationProvider
- JaasAuthenticationProvider extends AbstractJaasAuthenticationProvider
因为 Jaas 认证的没提到过,这里要提的话,还得先介绍这个,以后再找一个合适的机会单独出一章。所以便以 AbstractUserDetailsAuthenticationProvider 开说。
AbstractUserDetailsAuthenticationProvider 这个其实就是拿到用户的信息,并检验密码。
顺嘴提一下认证流程篇的:一次完整的认证,会拥有多个 AuthenticationProvider,而 ProviderManager 会统一管理 AuthenticationProvider。
首先看一下 DaoAuthenticationProvider 这个子类,可以看到没有重写核心的 authenticate() 和 supports(),所以主要关注 AbstractUserDetailsAuthenticationProvider 这个类。
首先看一下 supports(),前面提到的这个方法是用来判断当前的 AuthenticationProvider 能否支持对应的 Authentication,而这里的 Authentication 是 UsernamePasswordAuthenticationToken,也就是这里的 supports() 是判断当前 Provider 是否支持 UsernamePasswordAuthenticationToken。看代码没啥毛病:
1 2 3 4
| @Override public boolean supports(Class<?> authentication) { return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); }
|
再看一下 authenticate(),这个方法是来做认证的,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); String username = determineUsername(authentication); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException ex) { this.logger.debug("Failed to find user '" + username + "'"); if (!this.hideUserNotFoundExceptions) { throw ex; } throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { this.preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException ex) { if (!cacheWasUsed) { throw ex; } cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); this.preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); }
|
这个类的处理流程直译下来:
- 从入参的
Authentication 里拿到 username
- 根据 username 从缓存中去获得用户信息
- 如果获得的用户信息为空,调用
retrieveUser() 方法获得用户信息,通过 this.getUserDetailsService().loadUserByUsername(username); 获得
- 调用
additionalAuthenticationChecks() 方法检验
- 调用
createSuccessAuthentication() 创建 UsernamePasswordAuthenticationToken 返回
过滤器之外的自定义认证的思路
过滤器实现验证码篇
结合上面的,思路的话就是创建一个类,然后继承 DaoAuthenticationProvider,重写 additionalAuthenticationChecks() 方法,然后将自定义的一些逻辑代码码进去就完了。
例如验证码校验,注释掉原来的 Filter,然后增加 MyAuthenticationProvider:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest; import java.util.Objects;
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
@Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { HttpServletRequest req = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
if ("POST".equalsIgnoreCase(req.getMethod())) { String code = req.getParameter("code"); String sessionCode = req.getSession().getAttribute("code") + ""; if (code == null || "".equals(code)) { throw new AuthenticationServiceException("验证码不能为空"); } else if (!code.equals(sessionCode)) { throw new AuthenticationServiceException("验证码错误"); } } super.additionalAuthenticationChecks(userDetails, authentication); } }
|
再看看配置类 SecurityWebConfiguration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| private PasswordEncoder passwordEncoder() { DelegatingPasswordEncoder encoder = (DelegatingPasswordEncoder) PasswordEncoderFactories.createDelegatingPasswordEncoder(); encoder.setDefaultPasswordEncoderForMatches(NoOpPasswordEncoder.getInstance()); return encoder; }
private MyAuthenticationProvider myAuthenticationProvider() { MyAuthenticationProvider provider = new MyAuthenticationProvider(); provider.setUserDetailsService(userLoginService); provider.setPasswordEncoder(passwordEncoder()); return provider; }
@Override protected AuthenticationManager authenticationManager() throws Exception { return new ProviderManager(myAuthenticationProvider()); }
|