SpringSecurity-4-认证流程源码解析
登录认证基本原理
Spring Security的登录验证核心过滤链如图所示
请求阶段
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的鉴权方式
认证流程源码分析
认证流程图
如图所示,用户登录使用用户密码登录认证方式的(其他认证方式也可以)。UsernamePassword AuthenticationFilter会使用用户名和密码创建一个UsernamePasswordAuthenticationToken作为登录凭证,从而获取Authentication对象,Authentication代码身份验证主体,贯穿用户认证流程始终。
UsernamePasswordAuthenticationFilter
在UsernamePasswordAuthenticationFilter过滤器中用于获取Authentication
实体的方法是attemptAuthentication,其源码分析如下:
@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的登录验证核心类,主要代码如下
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的接口实现有多种,如图所示
- RememberMeAuthenticationProvider定义了“记住我”功能的登录验证逻辑
- DaoAuthenticationProvider加载数据库用户信息,进行用户密码的登录验证
DaoAuthenticationProvider
DaoAuthenticationProvider使用数据库加载用户信息 ,源码如下图
我们发现DaoAuthenticationProvider继承了AbstractUserDetailsAuthenticationProvider;AbstractUserDetailsAuthenticationProvider是一个抽象类,是 AuthenticationProvider 的核心实现类,实现了DaoAuthenticationProvider类中的authenticate方法,代码如下
AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvide的Authentication方法源码
@Override public 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方法
当我们需要使用数据库方式加载用户信息的时候,我么就需要实现UserDetailsService接口,重写loadUserByUsername方法
SecurityContext
登录认证完成以后,就需要Authtication信息,放入到SecurityContext中,后续就直接从SecurityContextFilter获取认证,避免重复多次认证。
注:注意查看我代码中的中文注释
如果您觉得本文不错,欢迎关注,点赞,收藏支持,您的关注是我坚持的动力!