Spring Seuciry相关的内容看了实在是太多了,但总以为仍是理解地不够巩固,仍是须要靠知识输出作巩固。 java
相关版本:spring
java: jdk 8 spring-boot: 2.1.6.RELEASE
一个认证过程,其实就是过滤器链上的一个绿色矩形Filter所要执行的过程。 数据库
基本的认证过程有三步骤:springboot
Authentication
,交由AuthenticationManager
进行认证;AuthenticationManager
的默认实现ProviderManager
会经过AuthenticationProvider
对Authentication
进行认证,其自己不作认证处理;Authentication
返回;不然抛出异常,以表示认证不经过。要理解这个过程,能够从类UsernamePasswordAuthenticationFilter
,ProviderManager
,DaoAuthenticationProvider
和InMemoryUserDetailsManager
(UserDetailsService
实现类,由UserDetailsServiceAutoConfiguration
默认配置提供)进行了解。只要建立一个含有spring-boot-starter-security
的springboot项目,在适当地打上断点接口看到这个流程。app
) ide
请求到前台
以后,负责该请求的前台
会将请求的内容封装为一个Authentication
对象交给认证管理部门
,认证管理部门
仅管理认证部门
,不作具体的认证操做,具体的操做由与该前台
相关的认证部门
进行处理。固然,每一个认证部门
须要判断Authentication
是否为该部门负责,是则由该部门负责处理,不然交给下一个部门处理。认证部门
认证成功以后会建立一个认证经过的Authentication
返回。不然要么抛出异常表示认证不经过,要么交给下一个部门处理。spring-boot
若是须要新增认证类型,只要增长相应的前台(Filter)
和与该前台(Filter)
想对应的认证部门(AuthenticationProvider)
就便可,固然也能够增长一个与已有前台对应的认证部门
。认证部门
会经过前台
生成的Authentication
来判断该认证是否由该部门负责,于是也许提供一个二者相互认同的Authentication
. 工具
认证部门
须要人员资料时,则能够从人员资料部门
获取。不一样的系统有不一样的人员资料部门
,须要咱们提供该人员资料部门
,不然将拿到空白档案。固然,人员资料部门
不必定是惟一的,认证部门
能够有本身的专属资料部门
。post
上图还能够有以下的画法:this
这个画法可能会和FilterChain更加符合。每个前台其实就是FilterChain中的一个,客户拿着请求逐个前台请求认证,找到正确的前台以后进行认证判断。
这里的前台Filter
仅仅指实现认证的Filter,Spring Security Filter Chain中处理这些Filter还有其余的Filter,好比CsrfFilter
。若是非要给角色给他们,那么就当他们是保安人员
吧。
Spring Security为咱们提供了3个已经实现的Filter。UsernamePasswordAuthenticationFilter
,BasicAuthenticationFilter
和 RememberMeAuthenticationFilter
。若是不作任何个性化的配置,UsernamePasswordAuthenticationFilter
和BasicAuthenticationFilter
会在默认的过滤器链中。这两种认证方式也就是默认的认证方式。
UsernamePasswordAuthenticationFilter
仅仅会对/login
路径生效,也就是说UsernamePasswordAuthenticationFilter
负责发布认证,发布认证的接口为/login
。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { ... public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); } ... }
UsernamePasswordAuthenticationFilter
为抽象类AbstractAuthenticationProcessingFilter
的一个实现,而BasicAuthenticationFilter
为抽象类BasicAuthenticationFilter
的一个实现。这四个类的源码提供了不错的前台(Filter)
实现思路。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter
提供了认证先后须要作的事情,其子类只须要提供实现完成认证的抽象方法attemptAuthentication(HttpServletRequest, HttpServletResponse)
便可。使用AbstractAuthenticationProcessingFilter
时,须要提供一个拦截路径(使用AntPathMatcher
进行匹配)来拦截对应的特定的路径。
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter
做为实际的前台,会将客户端提交的username和password封装成一个UsernamePasswordAuthenticationToken
交给认证管理部门(AuthenticationManager)
进行认证。如此,她的任务就完成了。
BasicAuthenticationFilter
该前台(Filter)
只会处理含有Authorization
的Header,且小写化后的值以basic
开头的请求,不然该前台(Filter)
不负责处理。该Filter会从header中获取Base64编码以后的username和password,建立UsernamePasswordAuthenticationToken
提供给认证管理部门(AuthenticationMananager)
进行认证。
前台
接到请求以后,会从请求中获取所需的信息,建立自家认证部门(AuthenticationProvider)
所认识的认证资料(Authentication)
,认证部门(AuthenticationProvider)
则主要是经过认证资料(Authentication)
的类型判断是否由该部门处理。
public interface Authentication extends Principal, Serializable { // 该principal具备的权限。AuthorityUtils工具类提供了一些方便的方法。 Collection<? extends GrantedAuthority> getAuthorities(); // 证实Principal的身份的证书,好比密码。 Object getCredentials(); // authentication request的附加信息,好比ip。 Object getDetails(); // 当事人。在username+password模式中为username,在有userDetails以后能够为userDetails。 Object getPrincipal(); // 是否已经经过认证。 boolean isAuthenticated(); // 设置经过认证。 void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
在Authentication
被认证以后,会保存到一个thread-local的SecurityContext中。
// 设置 SecurityContextHolder.getContext().setAuthentication(anAuthentication); // 获取 Authentication existingAuth = SecurityContextHolder.getContext() .getAuthentication();
在写前台Filter
的时候,能够先检查SecurityContextHolder.getContext()
中是否已经存在经过认证的Authentication
了,若是存在,则能够直接跳过该Filter。已经经过验证的Authentication
建议设置为一个不可修改的实例。
目前从Authentication
的类图中看到的实现类,均为Authentication
的抽象子类AbstractAuthenticationToken
的实现类。实现类有好几个,与前面的讲到的Filter相关的有UsernamePasswordAuthenticationToken
和RememberMeAuthenticationToken
。
AbstractAuthenticationToken
为CredentialsContainer
和Authentication
的子类。实现了一些简单的方法,但主要的方法还须要实现。该类的getName()
方法的实现能够看到经常使用的principal类为UserDetails
、AuthenticationPrincipal
和Princial
。若是有须要将对象设置为principal,能够考虑继承这三个类中的一个。
public String getName() { if (this.getPrincipal() instanceof UserDetails) { return ((UserDetails) this.getPrincipal()).getUsername(); } if (this.getPrincipal() instanceof AuthenticatedPrincipal) { return ((AuthenticatedPrincipal) this.getPrincipal()).getName(); } if (this.getPrincipal() instanceof Principal) { return ((Principal) this.getPrincipal()).getName(); } return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString(); }
AuthenticationManager
是一个接口,认证Authentication
,若是认证经过以后,返回的Authentication
应该带上该principal所具备的GrantedAuthority
。
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
该接口的注释中说明,必须按照以下的异常顺序进行检查和抛出:
DisabledException
:帐号不可用LockedException
:帐号被锁BadCredentialsException
:证书不正确Spring Security提供一个默认的实现ProviderManager
。认证管理部门(ProviderManager)
仅执行管理职能,具体的认证职能由认证部门(AuthenticationProvider)
执行。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { ... public ProviderManager(List<AuthenticationProvider> providers) { this(providers, null); } public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) { Assert.notNull(providers, "providers list cannot be null"); this.providers = providers; this.parent = parent; checkState(); } public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; boolean debug = logger.isDebugEnabled(); for (AuthenticationProvider provider : getProviders()) { // #1, 检查是否由该认证部门进行认证`AuthenticationProvider` if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { // #2, 认证部门进行认证 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); // #3,认证经过则再也不进行下一个认证部门的认证,不然抛出的异常被捕获,执行下一个认证部门(AuthenticationProvider) break; } } catch (AccountStatusException e) { prepareException(e, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw e; } catch (InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } } if (result == null && parent != null) { // Allow the parent to try. try { result = parentResult = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { // 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 e) { lastException = parentException = e; } } // #4, 若是认证经过,执行认证经过以后的操做 if (result != null) { if (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 than it will publish an AuthenticationSuccessEvent // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it if (parentResult == null) { eventPublisher.publishAuthenticationSuccess(result); } return result; } // Parent was null, or didn't authenticate (or throw an exception). // #5,若是认证不经过,必然有抛出异常,不然表示没有配置相应的认证部门(AuthenticationProvider) if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } // If the parent AuthenticationManager was attempted and failed than 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; } ... }
当使用到Spring Security OAuth2的时候,会看到另外一个实现OAuth2AuthenticationManager
。
认证部门(AuthenticationProvider)
负责实际的认证工做,与认证管理部门(ProvderManager)
协同工做。也许其余的认证管理部门(AuthenticationManager)
并不须要认证部门(AuthenticationProvider)
的协做。
public interface AuthenticationProvider { // 进行认证 Authentication authenticate(Authentication authentication) throws AuthenticationException; // 是否由该AuthenticationProvider进行认证 boolean supports(Class<?> authentication); }
该接口有不少的实现类,其中包含了RememberMeAuthenticationProvider
(直接AuthenticationProvider)和DaoAuthenticationProvider
(经过AbastractUserDetailsAuthenticationProvider
简介继承)。这里重点讲讲AbastractUserDetailsAuthenticationProvider
和DaoAuthenticationProvider
。
顾名思义,AbastractUserDetailsAuthenticationProvider
是对UserDetails
支持的Provider,其余的Provider,如RememberMeAuthenticationProvider就不须要用到UserDetails
。该抽象类有两个抽象方法须要实现类完成:
// 获取 UserDetails protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException; protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;
retrieveUser()
方法为校验提供UserDetails
。先看下UserDetails:
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); // 帐号是否过时 boolean isAccountNonExpired(); // 帐号是否被锁 boolean isAccountNonLocked(); // 证书(password)是否过时 boolean isCredentialsNonExpired(); // 帐号是否可用 boolean isEnabled(); }
AbastractUserDetailsAuthenticationProvider#authentication(Authentication)
分为三步验证:
preAuthenticationChecks
的默认实现为DefaultPreAuthenticationChecks
,负责完成校验:
UserDetails#isAccountNonLocked()
UserDetails#isEnabled()
UserDetails#isAccountNonExpired()
postAuthenticationChecks
的默认实现为DefaultPostAuthenticationChecks
,负责完成校验:
UserDetails#user.isCredentialsNonExpired()
additionalAuthenticationChecks
须要由实现类完成。
校验成功以后,AbstractUserDetailsAuthenticationProvider
会建立并返回一个经过认证的Authentication
。
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { // Ensure we return the original credentials the user supplied, // so subsequent attempts are successful even with encoded passwords. // Also ensure we return the original getDetails(), so that future // authentication events after cache expiry contain the details UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; }
以下为DaoAuthenticationProvider
对AbstractUserDetailsAuthenticationProvider
抽象方法的实现。
// 检查密码是否正确 protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } // 经过资料室(UserDetailsService)获取UserDetails对象 protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } ... }
在以上的代码中,须要提供UserDetailsService
和PasswordEncoder
实例。只要实例化这两个类,并放入到Spring容器中便可。
UserDetailsService
接口提供认证过程所需的UserDetails
的类,如DaoAuthenticationProvider
须要一个UserDetailsService
实例。
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
Spring Security提供了两个UserDetailsService
的实现:InMemoryUserDetailsManager
和JdbcUserDetailsManager
。InMemoryUserDetailsManager
为默认配置,从UserDetailsServiceAutoConfiguration
的配置中能够看出。固然也不容易理解,基于数据库的实现须要增长数据库的配置,不适合作默认实现。这两个类均为UserDetailsManager
的实现类,UserDetailsManager
定义了UserDetails
的CRUD操做。InMemoryUserDetailsManager
使用Map<String, MutableUserDetails>
作存储。
public interface UserDetailsManager extends UserDetailsService { void createUser(UserDetails user); void updateUser(UserDetails user); void deleteUser(String username); void changePassword(String oldPassword, String newPassword); boolean userExists(String username); }
若是咱们须要增长一个UserDetailsService
,能够考虑实现UserDetailsService
或者UserDetailsManager
。
到这里,咱们已经知道Spring Security的流程了。从上面的内容能够知道,如要增长一个新的认证方式,只要增长一个[前台(Filter)
+ 认证部门(AuthenticationProvider)
+ 资料室(UserDetailsService)
]组合便可。事实上,资料室(UserDetailsService)
不是必须的,可根据认证部门(AuthenticationProvider)
须要实现。
我会在另外一篇文章中以手机号码+验证码登陆为例进行讲解。