这篇文章主要为大家展示了“Spring Security认证流程的示例分析”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“Spring Security认证流程的示例分析”这篇文章吧。
Spring Seuciry相关的内容看了实在是太多了,但总觉得还是理解地不够巩固,还是需要靠知识输出做巩固。
相关版本:
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,Spring Security Filter Chain中处理这些Filter还有其他的Filter,比如CsrfFilter
。如果非要给角色给他们,那么就当他们是保安人员
吧。
Spring Security为我们提供了3个已经实现的Filter。UsernamePasswordAuthenticationFilter
,BasicAuthenticationFilter
和 RememberMeAuthenticationFilter
。如果不做任何个性化的配置,UsernamePasswordAuthenticationFilter
和BasicAuthenticationFilter
会在默认的过滤器链中。这两种认证方式也就是默认的认证方式。
UsernamePasswordAuthenticationFilter
仅仅会对/login
路径生效,也就是说UsernamePasswordAuthenticationFilter
负责发布认证,发布认证的接口为/login
。
pubpcclassUsernamePasswordAuthenticationFilterextends AbstractAuthenticationProcessingFilter{ ... pubpcUsernamePasswordAuthenticationFilter(){ 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)
的类型判断是否由该部门处理。
pubpcinterfaceAuthenticationextendsPrincipal,Seriapzable{ //该principal具有的权限。AuthorityUtils工具类提供了一些方便的方法。 Collection<?extendsGrantedAuthority>getAuthorities(); //证明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,可以考虑继承这三个类中的一个。
pubpcStringgetName(){ 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
。
pubpcinterfaceAuthenticationManager{ Authenticationauthenticate(Authenticationauthentication) throwsAuthenticationException; }
该接口的注释中说明,必须按照如下的异常顺序进行检查和抛出:
DisabledException:账号不可用
LockedException:账号被锁
BadCredentialsException:证书不正确
Spring Security提供一个默认的实现ProviderManager
。认证管理部门(ProviderManager)
仅执行管理职能,具体的认证职能由认证部门(AuthenticationProvider)
执行。
pubpcclassProviderManagerimplementsAuthenticationManager,MessageSourceAware, InitiapzingBean{ ... pubpcProviderManager(List<AuthenticationProvider>providers){ this(providers,null); } pubpcProviderManager(List<AuthenticationProvider>providers, AuthenticationManagerparent){ Assert.notNull(providers,"providerspstcannotbenull"); this.providers=providers; this.parent=parent; checkState(); } pubpcAuthenticationauthenticate(Authenticationauthentication) throwsAuthenticationException{ Class<?extendsAuthentication>toTest=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:Avoidpolpngadditionalprovidersifauthfailureisdueto //invapdaccountstatus 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 //calpngparentandtheparent //maythrowProviderNotFoundeventhoughaproviderinthechildalready //handledtherequest } catch(AuthenticationExceptione){ lastException=parentException=e; } } //#4,如果认证通过,执行认证通过之后的操作 if(result!=null){ if(eraseCredentialsAfterAuthentication &&(resultinstanceofCredentialsContainer)){ //Authenticationiscomplete.Removecredentialsandothersecretdata //fromauthentication ((CredentialsContainer)result).eraseCredentials(); } //IftheparentAuthenticationManagerwasattemptedandsuccessfulthanitwillpubpshanAuthenticationSuccessEvent //ThischeckpreventsaduppcateAuthenticationSuccessEventiftheparentAuthenticationManageralreadypubpshedit if(parentResult==null){ eventPubpsher.pubpshAuthenticationSuccess(result); } returnresult; } //Parentwasnull,ordidn'tauthenticate(orthrowanexception). //#5,如果认证不通过,必然有抛出异常,否则表示没有配置相应的认证部门(AuthenticationProvider) if(lastException==null){ lastException=newProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", newObject[]{toTest.getName()}, "NoAuthenticationProviderfoundfor{0}")); } //IftheparentAuthenticationManagerwasattemptedandfailedthanitwillpubpshanAbstractAuthenticationFailureEvent //ThischeckpreventsaduppcateAbstractAuthenticationFailureEventiftheparentAuthenticationManageralreadypubpshedit if(parentException==null){ prepareException(lastException,authentication); } throwlastException; } ... }
遍历所有的认证部门(AuthenticationProvider),找到支持的认证部门进行认证认证部门进行认证认证通过则不再进行下一个认证部门的认证,否则抛出的异常被捕获,执行下一个认证部门(AuthenticationProvider)如果认证通过,执行认证通过之后的操作如果认证不通过,必然有抛出异常,否则表示没有配置相应的认证部门(AuthenticationProvider)
当使用到Spring Security OAuth3的时候,会看到另一个实现OAuth3AuthenticationManager
。
认证部门(AuthenticationProvider)
认证部门(AuthenticationProvider)
负责实际的认证工作,与认证管理部门(ProvderManager)
协同工作。也许其他的认证管理部门(AuthenticationManager)
并不需要认证部门(AuthenticationProvider)
的协作。
pubpcinterfaceAuthenticationProvider{ //进行认证 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:
pubpcinterfaceUserDetailsextendsSeriapzable{ Collection<?extendsGrantedAuthority>getAuthorities(); 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){ //Ensurewereturntheoriginalcredentialstheusersuppped, //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
实例。
pubpcinterfaceUserDetailsService{ UserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException; }
Spring Security提供了两个UserDetailsService
的实现:InMemoryUserDetailsManager
和JdbcUserDetailsManager
。InMemoryUserDetailsManager
为默认配置,从UserDetailsServiceAutoConfiguration
的配置中可以看出。当然也不容易理解,基于数据库的实现需要增加数据库的配置,不适合做默认实现。这两个类均为UserDetailsManager
的实现类,UserDetailsManager
定义了UserDetails
的CRUD操作。InMemoryUserDetailsManager
使用Map<String, MutableUserDetails>
做存储。
pubpcinterfaceUserDetailsManagerextendsUserDetailsService{ voidcreateUser(UserDetailsuser); voidupdateUser(UserDetailsuser); voiddeleteUser(Stringusername); voidchangePassword(StringoldPassword,StringnewPassword); booleanuserExists(Stringusername); }
如果我们需要增加一个UserDetailsService
,可以考虑实现UserDetailsService
或者UserDetailsManager
。
增加一个认证流程
到这里,我们已经知道Spring Security的流程了。从上面的内容可以知道,如要增加一个新的认证方式,只要增加一个[前台(Filter)
+ 认证部门(AuthenticationProvider)
+ 资料室(UserDetailsService)
]组合即可。事实上,资料室(UserDetailsService)
不是必须的,可根据认证部门(AuthenticationProvider)
需要实现。
以上是“Spring Security认证流程的示例分析”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注编程行业资讯频道!
Spring Security认证流程的示例分析文章到此结束,字数约17275字,希望可以帮助到大家。屹东网往后会继续推荐Spring Security认证流程的示例分析相关内容。