详解Spring Security认证流程
前言
SpringSeuciry相关的内容看了实在是太多了,但总觉得还是理解地不够巩固,还是需要靠知识输出做巩固。
相关版本:
java:jdk8 spring-boot:2.1.6.RELEASE
过滤器链和认证过程
一个认证过程,其实就是过滤器链上的一个绿色矩形Filter所要执行的过程。
基本的认证过程有三步骤:
- Filter拦截请求,生成一个未认证的Authentication,交由AuthenticationManager进行认证;
- AuthenticationManager的默认实现ProviderManager会通过AuthenticationProvider对Authentication进行认证,其本身不做认证处理;
- 如果认证通过,则创建一个认证通过的Authentication返回;否则抛出异常,以表示认证不通过。
要理解这个过程,可以从类UsernamePasswordAuthenticationFilter,ProviderManager,DaoAuthenticationProvider和InMemoryUserDetailsManager(UserDetailsService实现类,由UserDetailsServiceAutoConfiguration默认配置提供)进行了解。只要创建一个含有spring-boot-starter-security的springboot项目,在适当地打上断点接口看到这个流程。
用认证部门进行讲解
请求到前台之后,负责该请求的前台会将请求的内容封装为一个Authentication对象交给认证管理部门,认证管理部门仅管理认证部门,不做具体的认证操作,具体的操作由与该前台相关的认证部门进行处理。当然,每个认证部门需要判断Authentication是否为该部门负责,是则由该部门负责处理,否则交给下一个部门处理。认证部门认证成功之后会创建一个认证通过的Authentication返回。否则要么抛出异常表示认证不通过,要么交给下一个部门处理。
如果需要新增认证类型,只要增加相应的前台(Filter)和与该前台(Filter)想对应的认证部门(AuthenticationProvider)就即可,当然也可以增加一个与已有前台对应的认证部门。认证部门会通过前台生成的Authentication来判断该认证是否由该部门负责,因而也许提供一个两者相互认同的Authentication.
认证部门需要人员资料时,则可以从人员资料部门获取。不同的系统有不同的人员资料部门,需要我们提供该人员资料部门,否则将拿到空白档案。当然,人员资料部门不一定是唯一的,认证部门可以有自己的专属资料部门。
上图还可以有如下的画法:
这个画法可能会和FilterChain更加符合。每一个前台其实就是FilterChain中的一个,客户拿着请求逐个前台请求认证,找到正确的前台之后进行认证判断。
前台(Filter)
这里的前台Filter仅仅指实现认证的Filter,SpringSecurityFilterChain中处理这些Filter还有其他的Filter,比如CsrfFilter。如果非要给角色给他们,那么就当他们是保安人员吧。
SpringSecurity为我们提供了3个已经实现的Filter。UsernamePasswordAuthenticationFilter,BasicAuthenticationFilter和RememberMeAuthenticationFilter。如果不做任何个性化的配置,UsernamePasswordAuthenticationFilter和BasicAuthenticationFilter会在默认的过滤器链中。这两种认证方式也就是默认的认证方式。
UsernamePasswordAuthenticationFilter仅仅会对/login路径生效,也就是说UsernamePasswordAuthenticationFilter负责发布认证,发布认证的接口为/login。
publicclassUsernamePasswordAuthenticationFilterextends
AbstractAuthenticationProcessingFilter{
...
publicUsernamePasswordAuthenticationFilter(){
super(newAntPathRequestMatcher("/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)进行认证。
认证资料(Authentication)
前台接到请求之后,会从请求中获取所需的信息,创建自家认证部门(AuthenticationProvider)所认识的认证资料(Authentication),认证部门(AuthenticationProvider)则主要是通过认证资料(Authentication)的类型判断是否由该部门处理。
publicinterfaceAuthenticationextendsPrincipal,Serializable{
//该principal具有的权限。AuthorityUtils工具类提供了一些方便的方法。
CollectiongetAuthorities();
//证明Principal的身份的证书,比如密码。
ObjectgetCredentials();
//authenticationrequest的附加信息,比如ip。
ObjectgetDetails();
//当事人。在username+password模式中为username,在有userDetails之后可以为userDetails。
ObjectgetPrincipal();
//是否已经通过认证。
booleanisAuthenticated();
//设置通过认证。
voidsetAuthenticated(booleanisAuthenticated)throwsIllegalArgumentException;
}
在Authentication被认证之后,会保存到一个thread-local的SecurityContext中。
//设置 SecurityContextHolder.getContext().setAuthentication(anAuthentication); //获取 AuthenticationexistingAuth=SecurityContextHolder.getContext() .getAuthentication();
在写前台Filter的时候,可以先检查SecurityContextHolder.getContext()中是否已经存在通过认证的Authentication了,如果存在,则可以直接跳过该Filter。已经通过验证的Authentication建议设置为一个不可修改的实例。
目前从Authentication的类图中看到的实现类,均为Authentication的抽象子类AbstractAuthenticationToken的实现类。实现类有好几个,与前面的讲到的Filter相关的有UsernamePasswordAuthenticationToken和RememberMeAuthenticationToken。
AbstractAuthenticationToken为CredentialsContainer和Authentication的子类。实现了一些简单的方法,但主要的方法还需要实现。该类的getName()方法的实现可以看到常用的principal类为UserDetails、AuthenticationPrincipal和Princial。如果有需要将对象设置为principal,可以考虑继承这三个类中的一个。
publicStringgetName(){
if(this.getPrincipal()instanceofUserDetails){
return((UserDetails)this.getPrincipal()).getUsername();
}
if(this.getPrincipal()instanceofAuthenticatedPrincipal){
return((AuthenticatedPrincipal)this.getPrincipal()).getName();
}
if(this.getPrincipal()instanceofPrincipal){
return((Principal)this.getPrincipal()).getName();
}
return(this.getPrincipal()==null)?"":this.getPrincipal().toString();
}
认证管理部门(AuthenticationManager)
AuthenticationManager是一个接口,认证Authentication,如果认证通过之后,返回的Authentication应该带上该principal所具有的GrantedAuthority。
publicinterfaceAuthenticationManager{
Authenticationauthenticate(Authenticationauthentication)
throwsAuthenticationException;
}
该接口的注释中说明,必须按照如下的异常顺序进行检查和抛出:
- DisabledException:账号不可用
- LockedException:账号被锁
- BadCredentialsException:证书不正确
SpringSecurity提供一个默认的实现ProviderManager。认证管理部门(ProviderManager)仅执行管理职能,具体的认证职能由认证部门(AuthenticationProvider)执行。
publicclassProviderManagerimplementsAuthenticationManager,MessageSourceAware,
InitializingBean{
...
publicProviderManager(Listproviders){
this(providers,null);
}
publicProviderManager(Listproviders,
AuthenticationManagerparent){
Assert.notNull(providers,"providerslistcannotbenull");
this.providers=providers;
this.parent=parent;
checkState();
}
publicAuthenticationauthenticate(Authenticationauthentication)
throwsAuthenticationException{
ClasstoTest=authentication.getClass();
AuthenticationExceptionlastException=null;
AuthenticationExceptionparentException=null;
Authenticationresult=null;
AuthenticationparentResult=null;
booleandebug=logger.isDebugEnabled();
for(AuthenticationProviderprovider:getProviders()){
//#1,检查是否由该认证部门进行认证`AuthenticationProvider`
if(!provider.supports(toTest)){
continue;
}
if(debug){
logger.debug("Authenticationattemptusing"
+provider.getClass().getName());
}
try{
//#2,认证部门进行认证
result=provider.authenticate(authentication);
if(result!=null){
copyDetails(authentication,result);
//#3,认证通过则不再进行下一个认证部门的认证,否则抛出的异常被捕获,执行下一个认证部门(AuthenticationProvider)
break;
}
}
catch(AccountStatusExceptione){
prepareException(e,authentication);
//SEC-546:Avoidpollingadditionalprovidersifauthfailureisdueto
//invalidaccountstatus
throwe;
}
catch(InternalAuthenticationServiceExceptione){
prepareException(e,authentication);
throwe;
}
catch(AuthenticationExceptione){
lastException=e;
}
}
if(result==null&&parent!=null){
//Allowtheparenttotry.
try{
result=parentResult=parent.authenticate(authentication);
}
catch(ProviderNotFoundExceptione){
//ignoreaswewillthrowbelowifnootherexceptionoccurredpriorto
//callingparentandtheparent
//maythrowProviderNotFoundeventhoughaproviderinthechildalready
//handledtherequest
}
catch(AuthenticationExceptione){
lastException=parentException=e;
}
}
//#4,如果认证通过,执行认证通过之后的操作
if(result!=null){
if(eraseCredentialsAfterAuthentication
&&(resultinstanceofCredentialsContainer)){
//Authenticationiscomplete.Removecredentialsandothersecretdata
//fromauthentication
((CredentialsContainer)result).eraseCredentials();
}
//IftheparentAuthenticationManagerwasattemptedandsuccessfulthanitwillpublishanAuthenticationSuccessEvent
//ThischeckpreventsaduplicateAuthenticationSuccessEventiftheparentAuthenticationManageralreadypublishedit
if(parentResult==null){
eventPublisher.publishAuthenticationSuccess(result);
}
returnresult;
}
//Parentwasnull,ordidn'tauthenticate(orthrowanexception).
//#5,如果认证不通过,必然有抛出异常,否则表示没有配置相应的认证部门(AuthenticationProvider)
if(lastException==null){
lastException=newProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
newObject[]{toTest.getName()},
"NoAuthenticationProviderfoundfor{0}"));
}
//IftheparentAuthenticationManagerwasattemptedandfailedthanitwillpublishanAbstractAuthenticationFailureEvent
//ThischeckpreventsaduplicateAbstractAuthenticationFailureEventiftheparentAuthenticationManageralreadypublishedit
if(parentException==null){
prepareException(lastException,authentication);
}
throwlastException;
}
...
}
遍历所有的认证部门(AuthenticationProvider),找到支持的认证部门进行认证认证部门进行认证认证通过则不再进行下一个认证部门的认证,否则抛出的异常被捕获,执行下一个认证部门(AuthenticationProvider)如果认证通过,执行认证通过之后的操作如果认证不通过,必然有抛出异常,否则表示没有配置相应的认证部门(AuthenticationProvider)
当使用到SpringSecurityOAuth2的时候,会看到另一个实现OAuth2AuthenticationManager。
认证部门(AuthenticationProvider)
认证部门(AuthenticationProvider)负责实际的认证工作,与认证管理部门(ProvderManager)协同工作。也许其他的认证管理部门(AuthenticationManager)并不需要认证部门(AuthenticationProvider)的协作。
publicinterfaceAuthenticationProvider{
//进行认证
Authenticationauthenticate(Authenticationauthentication)
throwsAuthenticationException;
//是否由该AuthenticationProvider进行认证
booleansupports(Class>authentication);
}
该接口有很多的实现类,其中包含了RememberMeAuthenticationProvider(直接AuthenticationProvider)和DaoAuthenticationProvider(通过AbastractUserDetailsAuthenticationProvider简介继承)。这里重点讲讲AbastractUserDetailsAuthenticationProvider和DaoAuthenticationProvider。
AbastractUserDetailsAuthenticationProvider
顾名思义,AbastractUserDetailsAuthenticationProvider是对UserDetails支持的Provider,其他的Provider,如RememberMeAuthenticationProvider就不需要用到UserDetails。该抽象类有两个抽象方法需要实现类完成:
//获取UserDetails protectedabstractUserDetailsretrieveUser(Stringusername, UsernamePasswordAuthenticationTokenauthentication) throwsAuthenticationException; protectedabstractvoidadditionalAuthenticationChecks(UserDetailsuserDetails, UsernamePasswordAuthenticationTokenauthentication) throwsAuthenticationException;
retrieveUser()方法为校验提供UserDetails。先看下UserDetails:
publicinterfaceUserDetailsextendsSerializable{
CollectiongetAuthorities();
StringgetPassword();
StringgetUsername();
//账号是否过期
booleanisAccountNonExpired();
//账号是否被锁
booleanisAccountNonLocked();
//证书(password)是否过期
booleanisCredentialsNonExpired();
//账号是否可用
booleanisEnabled();
}
AbastractUserDetailsAuthenticationProvider#authentication(Authentication)分为三步验证:
- preAuthenticationChecks.check(user);
- additionalAuthenticationChecks(user,
- (UsernamePasswordAuthenticationToken)authentication);
- postAuthenticationChecks.check(user);
preAuthenticationChecks的默认实现为DefaultPreAuthenticationChecks,负责完成校验:
- UserDetails#isAccountNonLocked()
- UserDetails#isEnabled()
- UserDetails#isAccountNonExpired()
postAuthenticationChecks的默认实现为DefaultPostAuthenticationChecks,负责完成校验:
UserDetails#user.isCredentialsNonExpired()
additionalAuthenticationChecks需要由实现类完成。
校验成功之后,AbstractUserDetailsAuthenticationProvider会创建并返回一个通过认证的Authentication。
protectedAuthenticationcreateSuccessAuthentication(Objectprincipal,
Authenticationauthentication,UserDetailsuser){
//Ensurewereturntheoriginalcredentialstheusersupplied,
//sosubsequentattemptsaresuccessfulevenwithencodedpasswords.
//AlsoensurewereturntheoriginalgetDetails(),sothatfuture
//authenticationeventsaftercacheexpirycontainthedetails
UsernamePasswordAuthenticationTokenresult=newUsernamePasswordAuthenticationToken(
principal,authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
returnresult;
}
DaoAuthenticationProvider
如下为DaoAuthenticationProvider对AbstractUserDetailsAuthenticationProvider抽象方法的实现。
//检查密码是否正确
protectedvoidadditionalAuthenticationChecks(UserDetailsuserDetails,
UsernamePasswordAuthenticationTokenauthentication)
throwsAuthenticationException{
if(authentication.getCredentials()==null){
logger.debug("Authenticationfailed:nocredentialsprovided");
thrownewBadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Badcredentials"));
}
StringpresentedPassword=authentication.getCredentials().toString();
if(!passwordEncoder.matches(presentedPassword,userDetails.getPassword())){
logger.debug("Authenticationfailed:passworddoesnotmatchstoredvalue");
thrownewBadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Badcredentials"));
}
}
//通过资料室(UserDetailsService)获取UserDetails对象
protectedfinalUserDetailsretrieveUser(Stringusername,
UsernamePasswordAuthenticationTokenauthentication)
throwsAuthenticationException{
prepareTimingAttackProtection();
try{
UserDetailsloadedUser=this.getUserDetailsService().loadUserByUsername(username);
if(loadedUser==null){
thrownewInternalAuthenticationServiceException(
"UserDetailsServicereturnednull,whichisaninterfacecontractviolation");
}
returnloadedUser;
}
...
}
在以上的代码中,需要提供UserDetailsService和PasswordEncoder实例。只要实例化这两个类,并放入到Spring容器中即可。
资料部门(UserDetailsService)
UserDetailsService接口提供认证过程所需的UserDetails的类,如DaoAuthenticationProvider需要一个UserDetailsService实例。
publicinterfaceUserDetailsService{
UserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException;
}
SpringSecurity提供了两个UserDetailsService的实现:InMemoryUserDetailsManager和JdbcUserDetailsManager。InMemoryUserDetailsManager为默认配置,从UserDetailsServiceAutoConfiguration的配置中可以看出。当然也不容易理解,基于数据库的实现需要增加数据库的配置,不适合做默认实现。这两个类均为UserDetailsManager的实现类,UserDetailsManager定义了UserDetails的CRUD操作。InMemoryUserDetailsManager使用Map
publicinterfaceUserDetailsManagerextendsUserDetailsService{
voidcreateUser(UserDetailsuser);
voidupdateUser(UserDetailsuser);
voiddeleteUser(Stringusername);
voidchangePassword(StringoldPassword,StringnewPassword);
booleanuserExists(Stringusername);
}
如果我们需要增加一个UserDetailsService,可以考虑实现UserDetailsService或者UserDetailsManager。
增加一个认证流程
到这里,我们已经知道SpringSecurity的流程了。从上面的内容可以知道,如要增加一个新的认证方式,只要增加一个[前台(Filter)+认证部门(AuthenticationProvider)+资料室(UserDetailsService)]组合即可。事实上,资料室(UserDetailsService)不是必须的,可根据认证部门(AuthenticationProvider)需要实现。
我会在另一篇文章中以手机号码+验证码登录为例进行讲解。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。