SpringSecurity主要实现了Authentication(认证,解决whoareyou?)和AccessControl(访问控制,也就是whatareyouallowedtodo?,也称为Authorization)。SpringSecurity在架构上将认证与授权分离,并提供了扩展点。
主要代码在spring-security-core包下面。要了解SpringSecurity,需要先关注里面的核心对象。
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是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; }在UserDetails里说了,GrantedAuthority可以理解为角色,例如ROLE_ADMINISTRATORorROLE_HR_SUPERVISOR。
实现认证主要是通过AuthenticationManager接口,它只包含了一个方法:
publicinterfaceAuthenticationManager{ Authenticationauthenticate(Authenticationauthentication) throwsAuthenticationException; }authenticate()方法主要做三件事:
如果验证通过,返回Authentication(通常带上authenticated=true)。 认证失败抛出AuthenticationException 如果无法确定,则返回nullAuthenticationException是运行时异常,它通常由应用程序按通用方式处理,用户代码通常不用特意被捕获和处理这个异常。
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,ordidntauthenticate(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有多种实现,大家最关注的通常是DaoAuthenticationProvider,继承于AbstractUserDetailsAuthenticationProvider,核心是通过UserDetails来实现认证,DaoAuthenticationProvider默认会自动加载,不用手动配。
先来看AbstractUserDetailsAuthenticationProvider,看最核心的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 //wereusinglatestdata(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就可以了。
SpringSecurity提供了一个Builder类AuthenticationManagerBuilder,借助它可以快速实现自定义认证。
看官方源码说明:
SecurityBuilderusedtocreateanAuthenticationManager.Allowsforeasilybuildinginmemoryauthentication,LDAPauthentication,JDBCbasedauthentication,addingUserDetailsService,andaddingAuthenticationProviders.
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里的。
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); } } }作者: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