SpringSecurity-4-认证流程源码解析

发布一下 0 0

SpringSecurity-4-认证流程源码解析

登录认证基本原理

Spring Security的登录验证核心过滤链如图所示

SpringSecurity-4-认证流程源码解析

请求阶段

  • SpringSecurity过滤器链始终贯穿一个上下文SecurityContext和一个Authentication对象(登录认证主体)。
  • 只有请求主体通过某一个过滤器认证,Authentication对象就会被填充,如果验证通过isAuthenticated=true
  • 如果请求通过了所有的过滤器,但是没有被认证,那么在最后有一个FilterSecurityInterceptor过滤器(名字看起来是拦截器,实际上是一个过滤器),来判断Authentication的认证状态,如果isAuthenticated=false(认证失败),则抛出认证异常。

响应阶段

  • 响应阶段,如果FilterSecurityInterceptor抛出异常,则会被ExceptionTranslationFilter进行相应的处理,例如:用户名密码登录异常,然后被重新跳转到登录页面。
  • 如果登录成功,请求响应会在SecurityContextPersistenceFilter过滤器中将返回的authentication的信息,如果有就放入session中,在下次请求的时候,就会直接从SecurityContextPersistenceFilter过滤器的session中获取认证信息,避免重复多次认证。

SpringSecurity多种登录认证方式

SpringSecurity使用Filter实现了多种登录认证方式,如下:

  • BasicAuthenticationFilter认证HttpBasic登录认证模式
  • UsernamePasswordAuthenticationFilter实现用户名密码登录认证
  • RememberMeAuthenticationFilter实现记住我功能
  • SocialAuthenticationFilter实现第三方社交登录认证,如微信,微博
  • Oauth2AuthenticationProcessingFilter实现Oauth2的鉴权方式

认证流程源码分析

认证流程图

SpringSecurity-4-认证流程源码解析

如图所示,用户登录使用用户密码登录认证方式的(其他认证方式也可以)。UsernamePassword AuthenticationFilter会使用用户名和密码创建一个UsernamePasswordAuthenticationToken作为登录凭证,从而获取Authentication对象,Authentication代码身份验证主体,贯穿用户认证流程始终。

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter过滤器中用于获取Authentication实体的方法是attemptAuthentication,其源码分析如下:

SpringSecurity-4-认证流程源码解析

 @Override    public Authentication attemptAuthentication(HttpServletRequest request,                                                HttpServletResponse response)            throws AuthenticationException {        //请求方式要post        if (this.postOnly && !request.getMethod().equals("POST")) {            throw new AuthenticationServiceException("Authentication method not supported: " +                    request.getMethod());        }        //从 request 中获取用户名、密码        String username = obtainUsername(request);        username = (username != null) ? username : "";        username = username.trim();        String password = obtainPassword(request);        password = (password != null) ? password : "";        // 将username和 password 构造成一个 UsernamePasswordAuthenticationToken 实例,        // 其中构建器中会是否认证设置为 authenticated=false        UsernamePasswordAuthenticationToken authRequest = new                UsernamePasswordAuthenticationToken(username, password);        //向 authRequest 对象中设置详细属性值。如添加了 remoteAddress、sessionId 值        setDetails(request, authRequest);        //调用 AuthenticationManager 的实现类 ProviderManager 进行验证        return this.getAuthenticationManager().authenticate(authRequest);    }

多种认证方式的ProviderManager

AuthenticationManager接口是对登录认证主体进行authenticate认证的,源码如下

public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException;}

ProviderManager实现了AuthenticationManager的登录验证核心类,主要代码如下

SpringSecurity-4-认证流程源码解析

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { private static final Log logger = LogFactory.getLog(ProviderManager.class); private List<AuthenticationProvider> providers = Collections.emptyList(); @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException {  //获取当前的Authentication的认证类型        Class<? extends Authentication> toTest = authentication.getClass();  AuthenticationException lastException = null;  AuthenticationException parentException = null;  Authentication result = null;  Authentication parentResult = null;  int currentPosition = 0;  int size = this.providers.size();        // 迭代认证提供者,不同认证方式有不同提供者,如:用户名密码认证提供者,手机短信认证提供者  for (AuthenticationProvider provider : getProviders()) {            // 选取当前认证方式对应的提供者   if (!provider.supports(toTest)) {    continue;   }   if (logger.isTraceEnabled()) {    logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",      provider.getClass().getSimpleName(), ++currentPosition, size));   }   try {                // 进行认证操作                // AbstractUserDetailsAuthenticationProvider》DaoAuthenticationProvider    result = provider.authenticate(authentication);    if (result != null) {                    //认证通过的话,将认证结果的details赋值到当前认证对象authentication。然后跳出循环     copyDetails(authentication, result);     break;    }   }   catch (AccountStatusException | InternalAuthenticationServiceException ex) {    prepareException(ex, authentication);    // SEC-546: Avoid polling additional providers if auth failure is due to    // invalid account status    throw ex;   }   catch (AuthenticationException ex) {    lastException = ex;   }  }  if (result == null && this.parent != null) {   // Allow the parent to try.   try {    parentResult = this.parent.authenticate(authentication);    result = parentResult;   }   catch (ProviderNotFoundException ex) {    // ignore as we will throw below if no other exception occurred prior to    // calling parent and the parent    // may throw ProviderNotFound even though a provider in the child already    // handled the request   }   catch (AuthenticationException ex) {    parentException = ex;    lastException = ex;   }  }  if (result != null) {   if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {    // Authentication is complete. Remove credentials and other secret data    // from authentication    ((CredentialsContainer) result).eraseCredentials();   }   // If the parent AuthenticationManager was attempted and successful then it   // will publish an AuthenticationSuccessEvent   // This check prevents a duplicate AuthenticationSuccessEvent if the parent   // AuthenticationManager already published it   if (parentResult == null) {    this.eventPublisher.publishAuthenticationSuccess(result);   }   return result;  }  // Parent was null, or didn't authenticate (or throw an exception).  if (lastException == null) {   lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",     new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));  }  // If the parent AuthenticationManager was attempted and failed then it will  // publish an AbstractAuthenticationFailureEvent  // This check prevents a duplicate AbstractAuthenticationFailureEvent if the  // parent AuthenticationManager already published it  if (parentException == null) {   prepareException(lastException, authentication);  }  throw lastException; } @SuppressWarnings("deprecation") private void prepareException(AuthenticationException ex, Authentication auth) {  this.eventPublisher.publishAuthenticationFailure(ex, auth); } public List<AuthenticationProvider> getProviders() {  return this.providers; }}

请注意查看我的中文注释

AuthenticationProvider

认证是由 AuthenticationManager 来管理的,真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider,每一种登录认证方式都可以尝试对登录认证主体进行认证。只要有一种方式被认证成功,Authentication对象就成为被认可的主体,Spring Security 默认会使用 DaoAuthenticationProvider

public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class<?> authentication);}

AuthenticationProvider的接口实现有多种,如图所示

SpringSecurity-4-认证流程源码解析

  • RememberMeAuthenticationProvider定义了“记住我”功能的登录验证逻辑
  • DaoAuthenticationProvider加载数据库用户信息,进行用户密码的登录验证

DaoAuthenticationProvider

DaoAuthenticationProvider使用数据库加载用户信息 ,源码如下图

SpringSecurity-4-认证流程源码解析

我们发现DaoAuthenticationProvider继承了AbstractUserDetailsAuthenticationProvider;AbstractUserDetailsAuthenticationProvider是一个抽象类,是 AuthenticationProvider 的核心实现类,实现了DaoAuthenticationProvider类中的authenticate方法,代码如下

AbstractUserDetailsAuthenticationProvider

AbstractUserDetailsAuthenticationProvide的Authentication方法源码

@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {   //如果authentication不是UsernamePasswordAuthenticationToken类型,则抛出异常   Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,         () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",               "Only UsernamePasswordAuthenticationToken is supported"));\   // 获取用户名   String username = determineUsername(authentication);   boolean cacheWasUsed = true;   //从缓存中获取UserDetails   UserDetails user = this.userCache.getUserFromCache(username);   //当缓存中没有UserDetails,则从子类DaoAuthenticationProvider中获取   if (user == null) {      cacheWasUsed = false;      try {         //子类DaoAuthenticationProvider中实现获取用户信息,          // 就是调用对应UserDetailsService#loadUserByUsername         user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);      }      catch (UsernameNotFoundException ex) {        ...      }      ...   }   try {       //前置检查。DefaultPreAuthenticationChecks 检测帐户是否锁定,是否可用,是否过期      this.preAuthenticationChecks.check(user);      // 检查密码是否正确      additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);   }   catch (AuthenticationException ex) {       // 异常则是重新认证      if (!cacheWasUsed) {         throw ex;      }      cacheWasUsed = false;       // 调用 loadUserByUsername 查询登录用户信息      user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);      this.preAuthenticationChecks.check(user);      additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);   }    //后检查。由DefaultPostAuthenticationChecks实现(检测密码是否过期)   this.postAuthenticationChecks.check(user);   if (!cacheWasUsed) {//是否放到缓存中      this.userCache.putUserInCache(user);   }   Object principalToReturn = user;   if (this.forcePrincipalAsString) {      principalToReturn = user.getUsername();   }    //将认证成功用户信息封装成 UsernamePasswordAuthenticationToken 对象并返回   return createSuccessAuthentication(principalToReturn, authentication, user);}

DaoAuthenticationProvider从数据库获取用户信息

DaoAuthenticationProvider类中的retrieveUser方法

SpringSecurity-4-认证流程源码解析

当我们需要使用数据库方式加载用户信息的时候,我么就需要实现UserDetailsService接口,重写loadUserByUsername方法

SecurityContext

登录认证完成以后,就需要Authtication信息,放入到SecurityContext中,后续就直接从SecurityContextFilter获取认证,避免重复多次认证。

:注意查看我代码中的中文注释

如果您觉得本文不错,欢迎关注,点赞,收藏支持,您的关注是我坚持的动力!

原创不易,转载请注明出处,感谢支持!如果本文对您有用,欢迎转发分享!

版权声明:内容来源于互联网和用户投稿 如有侵权请联系删除

本文地址:http://0561fc.cn/75386.html