Spring Security 架构与源码分析
本文内容纲要:
-核心对象
-SecurityContextHolder,SecurityContext和Authentication
-UserDetails与UserDetailsService
-GrantedAuthority
-小结
-Authentication认证
-AuthenticationManager
-AuthenticationProvider
-定制AuthenticationManagers
-授权与访问控制
-websecurity如何实现
-参考
SpringSecurity主要实现了Authentication(认证,解决whoareyou?)和AccessControl(访问控制,也就是whatareyouallowedtodo?,也称为Authorization)。SpringSecurity在架构上将认证与授权分离,并提供了扩展点。
核心对象
主要代码在spring-security-core
包下面。要了解SpringSecurity,需要先关注里面的核心对象。
SecurityContextHolder,SecurityContext和Authentication
SecurityContextHolder是SecurityContext的存放容器,默认使用ThreadLocal存储,意味SecurityContext在相同线程中的方法都可用。
SecurityContext主要是存储应用的principal信息,在SpringSecurity中用Authentication来表示。
获取principal:
Objectprincipal=SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principalinstanceofUserDetails){
Stringusername=((UserDetails)principal).getUsername();
}else{
Stringusername=principal.toString();
}
在SpringSecurity中,可以看一下Authentication定义:
publicinterfaceAuthenticationextendsPrincipal,Serializable{
Collection<?extendsGrantedAuthority>getAuthorities();
/**
*通常是密码
*/
ObjectgetCredentials();
/**
*Storesadditionaldetailsabouttheauthenticationrequest.ThesemightbeanIP
*address,certificateserialnumberetc.
*/
ObjectgetDetails();
/**
*用来标识是否已认证,如果使用用户名和密码登录,通常是用户名
*/
ObjectgetPrincipal();
/**
*是否已认证
*/
booleanisAuthenticated();
voidsetAuthenticated(booleanisAuthenticated)throwsIllegalArgumentException;
}
在实际应用中,通常使用UsernamePasswordAuthenticationToken
:
publicabstractclassAbstractAuthenticationTokenimplementsAuthentication,
CredentialsContainer{
}
publicclassUsernamePasswordAuthenticationTokenextendsAbstractAuthenticationToken{
}
一个常见的认证过程通常是这样的,创建一个UsernamePasswordAuthenticationToken,然后交给authenticationManager认证(后面详细说明),认证通过则通过SecurityContextHolder存放Authentication信息。
UsernamePasswordAuthenticationTokenauthenticationToken=
newUsernamePasswordAuthenticationToken(loginVM.getUsername(),loginVM.getPassword());
Authenticationauthentication=this.authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails与UserDetailsService
UserDetails是SpringSecurity里的一个关键接口,他用来表示一个principal。
publicinterfaceUserDetailsextendsSerializable{
/**
*用户的授权信息,可以理解为角色
*/
Collection<?extendsGrantedAuthority>getAuthorities();
/**
*用户密码
*
*@returnthepassword
*/
StringgetPassword();
/**
*用户名
* */
StringgetUsername();
booleanisAccountNonExpired();
booleanisAccountNonLocked();
booleanisCredentialsNonExpired();
booleanisEnabled();
}
UserDetails提供了认证所需的必要信息,在实际使用里,可以自己实现UserDetails,并增加额外的信息,比如email、mobile等信息。
在Authentication中的principal通常是用户名,我们可以通过UserDetailsService来通过principal获取UserDetails:
publicinterfaceUserDetailsService{
UserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException;
}
GrantedAuthority
在UserDetails里说了,GrantedAuthority可以理解为角色,例如ROLE_ADMINISTRATOR
orROLE_HR_SUPERVISOR
。
小结
SecurityContextHolder
,用来访问SecurityContext
.SecurityContext
,用来存储Authentication
.Authentication
,代表凭证.GrantedAuthority
,代表权限.UserDetails
,用户信息.UserDetailsService
,获取用户信息.
Authentication认证
AuthenticationManager
实现认证主要是通过AuthenticationManager接口,它只包含了一个方法:
publicinterfaceAuthenticationManager{
Authenticationauthenticate(Authenticationauthentication)
throwsAuthenticationException;
}
authenticate()方法主要做三件事:
- 如果验证通过,返回Authentication(通常带上authenticated=true)。
- 认证失败抛出
AuthenticationException
- 如果无法确定,则返回null
AuthenticationException
是运行时异常,它通常由应用程序按通用方式处理,用户代码通常不用特意被捕获和处理这个异常。
AuthenticationManager
的默认实现是ProviderManager
,它委托一组AuthenticationProvider
实例来实现认证。
AuthenticationProvider
和AuthenticationManager
类似,都包含authenticate
,但它有一个额外的方法supports
,以允许查询调用方是否支持给定Authentication
类型:
publicinterfaceAuthenticationProvider{
Authenticationauthenticate(Authenticationauthentication)
throwsAuthenticationException;
booleansupports(Class<?>authentication);
}
ProviderManager包含一组AuthenticationProvider
,执行authenticate时,遍历Providers,然后调用supports,如果支持,则执行遍历当前provider的authenticate方法,如果一个provider认证成功,则break。
publicAuthenticationauthenticate(Authenticationauthentication)
throwsAuthenticationException{
Class<?extendsAuthentication>toTest=authentication.getClass();
AuthenticationExceptionlastException=null;
Authenticationresult=null;
booleandebug=logger.isDebugEnabled();
for(AuthenticationProviderprovider:getProviders()){
if(!provider.supports(toTest)){
continue;
}
if(debug){
logger.debug("Authenticationattemptusing"
+provider.getClass().getName());
}
try{
result=provider.authenticate(authentication);
if(result!=null){
copyDetails(authentication,result);
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=parent.authenticate(authentication);
}
catch(ProviderNotFoundExceptione){
//ignoreaswewillthrowbelowifnootherexceptionoccurredpriorto
//callingparentandtheparent
//maythrowProviderNotFoundeventhoughaproviderinthechildalready
//handledtherequest
}
catch(AuthenticationExceptione){
lastException=e;
}
}
if(result!=null){
if(eraseCredentialsAfterAuthentication
&&(resultinstanceofCredentialsContainer)){
//Authenticationiscomplete.Removecredentialsandothersecretdata
//fromauthentication
((CredentialsContainer)result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
returnresult;
}
//Parentwasnull,ordidn'tauthenticate(orthrowanexception).
if(lastException==null){
lastException=newProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
newObject[]{toTest.getName()},
"NoAuthenticationProviderfoundfor{0}"));
}
prepareException(lastException,authentication);
throwlastException;
}
从上面的代码可以看出,ProviderManager
有一个可选parent,如果parent不为空,则调用parent.authenticate(authentication)
AuthenticationProvider
AuthenticationProvider
有多种实现,大家最关注的通常是DaoAuthenticationProvider
,继承于AbstractUserDetailsAuthenticationProvider
,核心是通过UserDetails
来实现认证,DaoAuthenticationProvider
默认会自动加载,不用手动配。
先来看AbstractUserDetailsAuthenticationProvide
r,看最核心的authenticate
:
publicAuthenticationauthenticate(Authenticationauthentication)
throwsAuthenticationException{
//必须是UsernamePasswordAuthenticationToken
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class,authentication,
messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"OnlyUsernamePasswordAuthenticationTokenissupported"));
//获取用户名
Stringusername=(authentication.getPrincipal()==null)?"NONE_PROVIDED"
:authentication.getName();
booleancacheWasUsed=true;
//从缓存获取
UserDetailsuser=this.userCache.getUserFromCache(username);
if(user==null){
cacheWasUsed=false;
try{
//retrieveUser抽象方法,获取用户
user=retrieveUser(username,
(UsernamePasswordAuthenticationToken)authentication);
}
catch(UsernameNotFoundExceptionnotFound){
logger.debug("User'"+username+"'notfound");
if(hideUserNotFoundExceptions){
thrownewBadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Badcredentials"));
}
else{
thrownotFound;
}
}
Assert.notNull(user,
"retrieveUserreturnednull-aviolationoftheinterfacecontract");
}
try{
//预先检查,DefaultPreAuthenticationChecks,检查用户是否被lock或者账号是否可用
preAuthenticationChecks.check(user);
//抽象方法,自定义检验
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken)authentication);
}
catch(AuthenticationExceptionexception){
if(cacheWasUsed){
//Therewasaproblem,sotryagainafterchecking
//we'reusinglatestdata(i.e.notfromthecache)
cacheWasUsed=false;
user=retrieveUser(username,
(UsernamePasswordAuthenticationToken)authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken)authentication);
}
else{
throwexception;
}
}
//后置检查DefaultPostAuthenticationChecks,检查isCredentialsNonExpired
postAuthenticationChecks.check(user);
if(!cacheWasUsed){
this.userCache.putUserInCache(user);
}
ObjectprincipalToReturn=user;
if(forcePrincipalAsString){
principalToReturn=user.getUsername();
}
returncreateSuccessAuthentication(principalToReturn,authentication,user);
}
上面的检验主要基于UserDetails实现,其中获取用户和检验逻辑由具体的类去实现,默认实现是DaoAuthenticationProvider,这个类的核心是让开发者提供UserDetailsService来获取UserDetails以及PasswordEncoder来检验密码是否有效:
privateUserDetailsServiceuserDetailsService;
privatePasswordEncoderpasswordEncoder;
看具体的实现,retrieveUser
,直接调用userDetailsService获取用户:
protectedfinalUserDetailsretrieveUser(Stringusername,
UsernamePasswordAuthenticationTokenauthentication)
throwsAuthenticationException{
UserDetailsloadedUser;
try{
loadedUser=this.getUserDetailsService().loadUserByUsername(username);
}
catch(UsernameNotFoundExceptionnotFound){
if(authentication.getCredentials()!=null){
StringpresentedPassword=authentication.getCredentials().toString();
passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
presentedPassword,null);
}
thrownotFound;
}
catch(ExceptionrepositoryProblem){
thrownewInternalAuthenticationServiceException(
repositoryProblem.getMessage(),repositoryProblem);
}
if(loadedUser==null){
thrownewInternalAuthenticationServiceException(
"UserDetailsServicereturnednull,whichisaninterfacecontractviolation");
}
returnloadedUser;
}
再来看验证:
protectedvoidadditionalAuthenticationChecks(UserDetailsuserDetails,
UsernamePasswordAuthenticationTokenauthentication)
throwsAuthenticationException{
Objectsalt=null;
if(this.saltSource!=null){
salt=this.saltSource.getSalt(userDetails);
}
if(authentication.getCredentials()==null){
logger.debug("Authenticationfailed:nocredentialsprovided");
thrownewBadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Badcredentials"));
}
//获取用户密码
StringpresentedPassword=authentication.getCredentials().toString();
//比较passwordEncoder后的密码是否和userdetails的密码一致
if(!passwordEncoder.isPasswordValid(userDetails.getPassword(),
presentedPassword,salt)){
logger.debug("Authenticationfailed:passworddoesnotmatchstoredvalue");
thrownewBadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Badcredentials"));
}
}
小结:要自定义认证,使用DaoAuthenticationProvider,只需要为其提供PasswordEncoder和UserDetailsService就可以了。
定制AuthenticationManagers
SpringSecurity提供了一个Builder类AuthenticationManagerBuilder
,借助它可以快速实现自定义认证。
看官方源码说明:
SecurityBuilderusedtocreateanAuthenticationManager.Allowsforeasilybuildinginmemoryauthentication,LDAPauthentication,JDBCbasedauthentication,addingUserDetailsService,andaddingAuthenticationProvider's.
AuthenticationManagerBuilder可以用来Build一个AuthenticationManager,可以创建基于内存的认证、LDAP认证、JDBC认证,以及添加UserDetailsService和AuthenticationProvider。
简单使用:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true)
publicclassApplicationSecurityextendsWebSecurityConfigurerAdapter{
publicSecurityConfiguration(AuthenticationManagerBuilderauthenticationManagerBuilder,UserDetailsServiceuserDetailsService,TokenProvidertokenProvider,CorsFiltercorsFilter,SecurityProblemSupportproblemSupport){
this.authenticationManagerBuilder=authenticationManagerBuilder;
this.userDetailsService=userDetailsService;
this.tokenProvider=tokenProvider;
this.corsFilter=corsFilter;
this.problemSupport=problemSupport;
}
@PostConstruct
publicvoidinit(){
try{
authenticationManagerBuilder
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}catch(Exceptione){
thrownewBeanInitializationException("Securityconfigurationfailed",e);
}
}
@Override
protectedvoidconfigure(HttpSecurityhttp)throwsException{
http
.addFilterBefore(corsFilter,UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.csrf()
.disable()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/register").permitAll()
.antMatchers("/api/activate").permitAll()
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/api/account/reset-password/init").permitAll()
.antMatchers("/api/account/reset-password/finish").permitAll()
.antMatchers("/api/profile-info").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/v2/api-docs/**").permitAll()
.antMatchers("/swagger-resources/configuration/ui").permitAll()
.antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN)
.and()
.apply(securityConfigurerAdapter());
}
}
授权与访问控制
一旦认证成功,我们可以继续进行授权,授权是通过AccessDecisionManager
来实现的。框架有三种实现,默认是AffirmativeBased,通过AccessDecisionVoter
决策,有点像ProviderManager
委托给AuthenticationProviders
来认证。
publicvoiddecide(Authenticationauthentication,Objectobject,
Collection<ConfigAttribute>configAttributes)throwsAccessDeniedException{
intdeny=0;
//遍历DecisionVoter
for(AccessDecisionVotervoter:getDecisionVoters()){
//投票
intresult=voter.vote(authentication,object,configAttributes);
if(logger.isDebugEnabled()){
logger.debug("Voter:"+voter+",returned:"+result);
}
switch(result){
caseAccessDecisionVoter.ACCESS_GRANTED:
return;
caseAccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
//一票否决
if(deny>0){
thrownewAccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied","Accessisdenied"));
}
//Togetthisfar,everyAccessDecisionVoterabstained
checkAllowIfAllAbstainDecisions();
}
来看AccessDecisionVoter:
booleansupports(ConfigAttributeattribute);
booleansupports(Class<?>clazz);
intvote(Authenticationauthentication,Sobject,
Collection<ConfigAttribute>attributes);
object是用户要访问的资源,ConfigAttribute则是访问object要满足的条件,通常payload是字符串,比如ROLE_ADMIN。所以我们来看下RoleVoter的实现,其核心就是从authentication提取出GrantedAuthority,然后和ConfigAttribute比较是否满足条件。
publicbooleansupports(ConfigAttributeattribute){
if((attribute.getAttribute()!=null)
&&attribute.getAttribute().startsWith(getRolePrefix())){
returntrue;
}
else{
returnfalse;
}
}
publicbooleansupports(Class<?>clazz){
returntrue;
}
publicintvote(Authenticationauthentication,Objectobject,
Collection<ConfigAttribute>attributes){
if(authentication==null){
returnACCESS_DENIED;
}
intresult=ACCESS_ABSTAIN;
//获取GrantedAuthority信息
Collection<?extendsGrantedAuthority>authorities=extractAuthorities(authentication);
for(ConfigAttributeattribute:attributes){
if(this.supports(attribute)){
//默认拒绝访问
result=ACCESS_DENIED;
//Attempttofindamatchinggrantedauthority
for(GrantedAuthorityauthority:authorities){
//判断是否有匹配的authority
if(attribute.getAttribute().equals(authority.getAuthority())){
//可访问
returnACCESS_GRANTED;
}
}
}
}
returnresult;
}
这里要疑问,ConfigAttribute哪来的?其实就是上面ApplicationSecurity的configure里的。
websecurity如何实现
Web层中的SpringSecurity(用于UI和HTTP后端)基于ServletFilters
,下图显示了单个HTTP请求的处理程序的典型分层。
SpringSecurity通过FilterChainProxy
作为单一的Filter注册到web层,Proxy内部的Filter。
FilterChainProxy相当于一个filter的容器,通过VirtualFilterChain来依次调用各个内部filter
publicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,
FilterChainchain)throwsIOException,ServletException{
booleanclearContext=request.getAttribute(FILTER_APPLIED)==null;
if(clearContext){
try{
request.setAttribute(FILTER_APPLIED,Boolean.TRUE);
doFilterInternal(request,response,chain);
}
finally{
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
else{
doFilterInternal(request,response,chain);
}
}
privatevoiddoFilterInternal(ServletRequestrequest,ServletResponseresponse,
FilterChainchain)throwsIOException,ServletException{
FirewalledRequestfwRequest=firewall
.getFirewalledRequest((HttpServletRequest)request);
HttpServletResponsefwResponse=firewall
.getFirewalledResponse((HttpServletResponse)response);
List<Filter>filters=getFilters(fwRequest);
if(filters==null||filters.size()==0){
if(logger.isDebugEnabled()){
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+(filters==null?"hasnomatchingfilters"
:"hasanemptyfilterlist"));
}
fwRequest.reset();
chain.doFilter(fwRequest,fwResponse);
return;
}
VirtualFilterChainvfc=newVirtualFilterChain(fwRequest,chain,filters);
vfc.doFilter(fwRequest,fwResponse);
}
privatestaticclassVirtualFilterChainimplementsFilterChain{
privatefinalFilterChainoriginalChain;
privatefinalList<Filter>additionalFilters;
privatefinalFirewalledRequestfirewalledRequest;
privatefinalintsize;
privateintcurrentPosition=0;
privateVirtualFilterChain(FirewalledRequestfirewalledRequest,
FilterChainchain,List<Filter>additionalFilters){
this.originalChain=chain;
this.additionalFilters=additionalFilters;
this.size=additionalFilters.size();
this.firewalledRequest=firewalledRequest;
}
publicvoiddoFilter(ServletRequestrequest,ServletResponseresponse)
throwsIOException,ServletException{
if(currentPosition==size){
if(logger.isDebugEnabled()){
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+"reachedendofadditionalfilterchain;proceedingwithoriginalchain");
}
//Deactivatepathstrippingasweexitthesecurityfilterchain
this.firewalledRequest.reset();
originalChain.doFilter(request,response);
}
else{
currentPosition++;
FilternextFilter=additionalFilters.get(currentPosition-1);
if(logger.isDebugEnabled()){
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+"atposition"+currentPosition+"of"+size
+"inadditionalfilterchain;firingFilter:'"
+nextFilter.getClass().getSimpleName()+"'");
}
nextFilter.doFilter(request,response,this);
}
}
}
参考
- https://spring.io/guides/topicals/spring-security-architecture/
- https://docs.spring.io/spring-security/site/docs/5.0.5.RELEASE/reference/htmlsingle/#overall-architecture
作者:Jadepeng
出处:jqpeng的技术记事本--http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
本文内容总结:核心对象,SecurityContextHolder,SecurityContext和Authentication,UserDetails与UserDetailsService,GrantedAuthority,小结,Authentication认证,AuthenticationManager,AuthenticationProvider,定制AuthenticationManagers,授权与访问控制,websecurity如何实现,参考,
原文链接:https://www.cnblogs.com/xiaoqi/p/spring-security.html