内容简介:前面一节,通过简单配置即可实现SpringSecurity表单认证功能,而今天这一节将通过阅读源码的形式来学习SpringSecurity是如何实现这些功能, 前方高能预警,前面我说过SpringSecurity是基于过滤器链的形式,那么我解析将会介绍一下具体有哪些过滤器。由于过滤器链中的过滤器实在太多,我没有一一列举,调了几个比较重要的介绍一下。
前面一节,通过简单配置即可实现SpringSecurity表单认证功能,而今天这一节将通过阅读源码的形式来学习SpringSecurity是如何实现这些功能, 前方高能预警, 本篇分析源码篇幅较长
。
过滤器链
前面我说过SpringSecurity是基于过滤器链的形式,那么我解析将会介绍一下具体有哪些过滤器。
Filter Class | 介绍 |
---|---|
SecurityContextPersistenceFilter | 判断当前用户是否登录 |
CrsfFilter | 用于防止csrf攻击 |
LogoutFilter | 处理注销请求 |
UsernamePasswordAuthenticationFilter | 处理表单登录的请求(也是我们今天的主角) |
BasicAuthenticationFilter | 处理http basic认证的请求 |
由于过滤器链中的过滤器实在太多,我没有一一列举,调了几个比较重要的介绍一下。
通过上面我们知道SpringSecurity对于表单登录的认证请求是交给了UsernamePasswordAuthenticationFilter处理的,那么具体的认证流程如下:
从上图可知, UsernamePasswordAuthenticationFilter
继承于抽象类 AbstractAuthenticationProcessingFilter
。
具体认证是:
- 进入doFilter方法,判断是否要认证,如果需要认证则进入attemptAuthentication方法,如果不需要直接结束
- attemptAuthentication方法中根据username跟password构造一个UsernamePasswordAuthenticationToken对象(此时的token是未认证的),并且将它交给ProviderManger来完成认证。
- ProviderManger中维护这一个AuthenticationProvider对象列表,通过遍历判断并且最后选择DaoAuthenticationProvider对象来完成最后的认证。
- DaoAuthenticationProvider根据ProviderManger传来的token取出username,并且调用我们写的UserDetailsService的loadUserByUsername方法从数据库中读取用户信息,然后对比用户密码,如果认证通过,则返回用户信息也是就是UserDetails对象,在重新构造UsernamePasswordAuthenticationToken(此时的token是 已经认证通过了的)。
接下来我们将通过源码来分析具体的整个认证流程。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter 是一个抽象类。所有的认证认证请求的过滤器都会继承于它,它主要将一些公共的功能实现,而具体的验证逻辑交给子类实现,有点类似于父类设置好认证流程,子类负责具体的认证逻辑,这样跟 设计模式 的 模板方法模式 有点相似。
现在我们分析一下 它里面比较重要的方法
1、doFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { // 省略不相干代码。。。 // 1、判断当前请求是否要认证 if (!requiresAuthentication(request, response)) { // 不需要直接走下一个过滤器 chain.doFilter(request, response); return; } try { // 2、开始请求认证,attemptAuthentication具体实现给子类,如果认证成功返回一个认证通过的Authenticaion对象 authResult = attemptAuthentication(request, response); if (authResult == null) { return; } // 3、登录成功 将认证成功的用户信息放入session SessionAuthenticationStrategy接口,用于扩展 sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { //2.1、发生异常,登录失败,进入登录失败handler回调 unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { //2.1、发生异常,登录失败,进入登录失败处理器 unsuccessfulAuthentication(request, response, failed); return; } // 3.1、登录成功,进入登录成功处理器。 successfulAuthentication(request, response, chain, authResult); } 复制代码
2、successfulAuthentication
登录成功处理器
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //1、登录成功 将认证成功的Authentication对象存入SecurityContextHolder中 // SecurityContextHolder本质是一个ThreadLocal SecurityContextHolder.getContext().setAuthentication(authResult); //2、如果开启了记住我功能,将调用rememberMeServices的loginSuccess 将生成一个token // 将token放入cookie中这样 下次就不用登录就可以认证。具体关于记住我rememberMeServices的相关分析我 们下面几篇文章会深入分析的。 rememberMeServices.loginSuccess(request, response, authResult); // Fire event //3、发布一个登录事件。 if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } //4、调用我们自己定义的登录成功处理器,这样也是我们扩展得知登录成功的一个扩展点。 successHandler.onAuthenticationSuccess(request, response, authResult); } 复制代码
3、unsuccessfulAuthentication
登录失败处理器
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { //1、登录失败,将SecurityContextHolder中的信息清空 SecurityContextHolder.clearContext(); //2、关于记住我功能的登录失败处理 rememberMeServices.loginFail(request, response); //3、调用我们自己定义的登录失败处理器,这里可以扩展记录登录失败的日志。 failureHandler.onAuthenticationFailure(request, response, failed); } 复制代码
关于AbstractAuthenticationProcessingFilter主要分析就到这。我们可以从源码中知道,当请求进入该过滤器中具体的流程是
- 判断该请求是否要被认证
- 调用
attemptAuthentication
方法开始认证,由于是抽象方法具体认证逻辑给子类 - 如果登录成功,则将认证结果
Authentication
对象根据session策略写入session中,将认证结果写入到SecurityContextHolder
,如果开启了记住我功能,则根据记住我功能,生成token并且写入cookie中,最后调用一个successHandler
对象的方法,这个对象可以是我们配置注入的,用于处理我们的自定义登录成功的一些逻辑(比如记录登录成功日志等等)。 - 如果登录失败,则清空
SecurityContextHolder
中的信息,并且调用我们自己注入的failureHandler
对象,处理我们自己的登录失败逻辑。
UsernamePasswordAuthenticationFilter
从上面分析我们可以知道, UsernamePasswordAuthenticationFilter
是继承于 AbstractAuthenticationProcessingFilter
,并且实现它的 attemptAuthentication
方法,来实现认证具体的逻辑实现。接下来,我们通过阅读 UsernamePasswordAuthenticationFilter
的源码来解读,它是如何完成认证的。 由于这里会涉及 UsernamePasswordAuthenticationToken
对象构造,所以我们先看看 UsernamePasswordAuthenticationToken
的源码
1、UsernamePasswordAuthenticationToken
// 继承至AbstractAuthenticationToken // AbstractAuthenticationToken主要定义一下在SpringSecurity中toke需要存在一些必须信息 // 例如权限集合 Collection<GrantedAuthority> authorities; 是否认证通过boolean authenticated = false;认证通过的用户信息Object details; public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { // 未登录情况下 存的是用户名 登录成功情况下存的是UserDetails对象 private final Object principal; // 密码 private Object credentials; /** * 构造函数,用户没有登录的情况下,此时的authenticated是false,代表尚未认证 */ public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); } /** * 构造函数,用户登录成功的情况下,多了一个参数 是用户的权限集合,此时的authenticated是true,代表认证成功 */ public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); // must use super, as we override } } 复制代码
接下来我们就可以分析attemptAuthentication方法了。
2、attemptAuthentication
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 1、判断是不是post请求,如果不是则抛出AuthenticationServiceException异常,注意这里抛出的异常都在AbstractAuthenticationProcessingFilter#doFilter方法中捕获,捕获之后会进入登录失败的逻辑。 if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } // 2、从request中拿用户名跟密码 String username = obtainUsername(request); String password = obtainPassword(request); // 3、非空处理,防止NPE异常 if (username == null) { username = ""; } if (password == null) { password = ""; } // 4、除去空格 username = username.trim(); // 5、根据username跟password构造出一个UsernamePasswordAuthenticationToken对象 从上文分析可知道,此时的token是未认证的。 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // 6、配置一下其他信息 ip 等等 setDetails(request, authRequest); // 7、调用ProviderManger的authenticate的方法进行具体认证逻辑 return this.getAuthenticationManager().authenticate(authRequest); } 复制代码
ProviderManager
维护一个AuthenticationProvider列表,进行认证逻辑验证
1、authenticate
public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 1、拿到token的类型。 Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; // 2、遍历AuthenticationProvider列表 for (AuthenticationProvider provider : getProviders()) { // 3、AuthenticationProvider不支持当前token类型,则直接跳过 if (!provider.supports(toTest)) { continue; } try { // 4、如果Provider支持当前token,则交给Provider完成认证。 result = provider.authenticate(authentication); } catch (AccountStatusException e) { throw e; } catch (InternalAuthenticationServiceException e) { throw e; } catch (AuthenticationException e) { lastException = e; } } // 5、登录成功 返回登录成功的token if (result != null) { eventPublisher.publishAuthenticationSuccess(result); return result; } } 复制代码
AbstractUserDetailsAuthenticationProvider
1、authenticate
AbstractUserDetailsAuthenticationProvider
实现了 AuthenticationProvider
接口,并且实现了部分方法, DaoAuthenticationProvider
继承于 AbstractUserDetailsAuthenticationProvider
类,所以我们先来看看 AbstractUserDetailsAuthenticationProvider
的实现。
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { // 国际化处理 protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); /** * 对token一些检查,具体检查逻辑交给子类实现,抽象方法 */ protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException; /** * 认证逻辑的实现,调用抽象方法retrieveUser根据username获取UserDetails对象 */ public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 1、获取usernmae String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); // 2、尝试去缓存中获取UserDetails对象 UserDetails user = this.userCache.getUserFromCache(username); // 3、如果为空,则代表当前对象没有缓存。 if (user == null) { cacheWasUsed = false; try { //4、调用retrieveUser去获取UserDetail对象,为什么这个方法是抽象方法大家很容易知道,如果UserDetail信息存在关系数据库 则可以重写该方法并且去关系数据库获取用户信息,如果UserDetail信息存在其他地方,可以重写该方法用其他的方法去获取用户信息,这样丝毫不影响整个认证流程,方便扩展。 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { // 捕获异常 日志处理 并且往上抛出,登录失败。 if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { throw notFound; } } } try { // 5、前置检查 判断当前用户是否锁定,禁用等等 preAuthenticationChecks.check(user); // 6、其他的检查,在DaoAuthenticationProvider是检查密码是否一致 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { } // 7、后置检查,判断密码是否过期 postAuthenticationChecks.check(user); // 8、登录成功通过UserDetail对象重新构造一个认证通过的Token对象 return createSuccessAuthentication(principalToReturn, authentication, user); } protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { // 调用第二个构造方法,构造一个认证通过的Token对象 UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; } } 复制代码
接下来我们具体看看 retrieveUser
的实现,没看源码大家应该也可以知道, retrieveUser
方法应该是调用 UserDetailsService
去数据库查询是否有该用户,以及用户的密码是否一致。
DaoAuthenticationProvider
DaoAuthenticationProvider 主要是通过UserDetailService来获取UserDetail对象。
1、retrieveUser
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { try { // 1、调用UserDetailsService接口的loadUserByUsername方法获取UserDeail对象 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); // 2、如果loadedUser为null 代表当前用户不存在,抛出异常 登录失败。 if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } // 3、返回查询的结果 return loadedUser; } } 复制代码
2、additionalAuthenticationChecks
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { // 1、如果密码为空,则抛出异常、 if (authentication.getCredentials() == null) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } // 2、获取用户输入的密码 String presentedPassword = authentication.getCredentials().toString(); // 3、调用passwordEncoder的matche方法 判断密码是否一致 if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value"); // 4、如果不一致 则抛出异常。 throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } 复制代码
总结
至此,整认证流程已经分析完毕,大家如果有什么不懂可以关注我的公众号一起讨论。
学习是一个漫长的过程,学习源码可能会很困难但是只要努力一定就会有获取,大家一致共勉。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 表单 – 避免Symfony强制显示表单字段
- 细说 Angular 2+ 的表单(二):响应式表单
- 8款最新CSS3表单 环形表单很酷
- 动态表单 form-create 2.5 版本来啦,帮你轻松搞定表单
- 开源 | vue-form-making:基于 Vue 的表单设计器,让表单开发简单而高效
- 表单验证(AngularJs)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。