本章代码已分享至Gitee:https://gitee.com/lengcz/security-spring-boot文章目录工作原理结构总览认证流程授权流程AuthenticationProviderUserDetailsServicePasswordEncoder如何使用BCryptPasswordEncoder授权流程授权流程授权决策工作原理结构总览\qquadSpring Security 所解决的问题就是安全访问控制而安全访问控制功能其实就是所有进入系统的请求进行拦截校验每个请求是否能够访问它所期望的资源。Spring Security 对Web资源的保护是通过Filter入手的所以从这个Filter入手逐步深入Spring Security原理。$\qquad%当初始化Spring Security时会创建一个名为SpringSecurityFilterChain的Servlet过滤器类型为org.springframework.security.web.FilterChainProxy它实现了javax.servlet.Filter因此外部的请求会经过此类下面是Spring Security过滤器链结构图。FilterChainProxy是一个代理真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter同时这些Filter作为Bean被管理它们是Spring Security核心各有各的职责但他们并不直接处理用户的认证也不直接处理用户的授权而是把它们交给认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理下图是FilterChainProxy相关类的UML图示。spring security功能的实现主要是由一系列过滤器链相互配合完成。、下面价绍过滤器链中主要的几个过滤器和其作用SecurityContextPersistenceFilter这个Filter是整个拦截国车过的入口和出口会在请求开始时从配置好的SecurityContextRepository中获取SecurityContext然后把它设置给SecurityContextHolder。在请求完成后将SecurityContextHolder持有的SecurityContext再保存到配置好的SecurityContextRepository同时清除SecurityContextHolder所持有的SecurityContextUsernamePasswordAuthenticationFilter用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码其内部还有登录成功或失败后进行处理的AuthenticationSuccessHandler和AuthenticationFailureHandler这些都是可以根据需求做出相关改变FilterSecurityInterceptor用于保护Web资源的使用AccessDecisionManager对当前用户进行授权访问。ExceptionTranslationFilter能够捕获来自FilterChain所有的异常并进行处理。但是它只会处理两类异常AuthenticationException和AccessDeniedException其它的异常它会继续抛出。认证流程认证过程用户提交用户名、密码被SecurityFilterChain中的UsernamePassowordAuthenticationFilter过滤器获取到封装为请求的Authentication通常情况下是UsernamePasswordAuthenticationToken这个实现类。然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证认证成功后AuthenticationManager身份管理器返回一个被填满信了信息的Authentication(包含了全新信息身份信息细节信息但是密码通常会被移除)实例。SecurityContextHolder 安全上下文容器将第3步填充了信息的Authentication通过SecurityContextHolder.getContext().setAuthentication()方法设置到其中。可以看到AuthenticationManager接口(认证管理器)是认证相关的核心接口也是发起认证的出发点它的实现类为ProviderManager。而Spring Security支持多种认证方式因此ProviderManager维护着一个List列表存放多种认证方式最终实际的认证工作是由AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider它的内部又维护着一个UserDetailService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication。授权流程Spring Security 可以通过http.authorizeRequests()对web请求进行授权保护。Spring Security 使用标准Filter建立了对web请求的拦截最终实现对资源的授权访问。AuthenticationProvider\qquad通过前面的Spring Security认证流程可以知认证管理器(AuthenticationManager)委托AuthenticationProvider完成认证工作。AuthenticationProvider是一个接口定义如下publicinterfaceAuthenticationProvider{Authenticationauthenticate(Authenticationvar1)throwsAuthenticationException;booleansupports(Class?var1);}authenticate()方法定义了认证的实现过程它的参数一个Authentication里面包含了登录用户所提交的用户、密码等。而返回值也是一个Authentication这个Authentication则是在认证成功后将用户的权限及其他信息重新组装后生成。\qquadSpring Security中维护着一个List列表存放着多种认证方式不同的认证方式使用不同的AuthenticationProvider。如使用用户名密码登录时使用AuthenticationProvider1短信登陆时使用AuthentcationProvider2等这样的例子。\qquad每个AuthenticationProvider需要实现 supports()方法来表明自己支持的认证方法如我们使用表单方式认证在提交请求时Spring Security会生成UsernamePasswordAuthenticationToekn它是一个Authentication,里面封装着用户提交的用户名、密码信息。而对应着哪个AuthenticationProvider来处理它我们在DaoAuthenticationProvider的基类AbstractUserDetailsAuthenticationProvider发现一下代码publicbooleansupports(Class?authentication){returnUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);}也就是说当web表单提交用户名密码时Spring Security由DaoAuthenticationProvider处理。\qquad最后我们来看一下Authentication(认证信息)的结构它是一个接口我们之前提到的UsernamePasswordAuthenticationToken就是它的实现之一publicinterfaceAuthenticationextendsPrincipal,Serializable{Collection?extendsGrantedAuthoritygetAuthorities();//权限列表ObjectgetCredentials();//凭证ObjectgetDetails();//用户信息ObjectgetPrincipal();//用户身份booleanisAuthenticated();//是否用户认证通过voidsetAuthenticated(booleanvar1)throwsIllegalArgumentException;}UserDetailsService实现UserDetailsService接口publicinterfaceUserDetailsService{UserDetailsloadUserByUsername(Stringvar1)throwsUsernameNotFoundException;}很多人把DaoAuthenticationProvider和UserDetailsService的职责搞混淆其实UserDetailsService只负责从特定的地方(数据库)加载用户信息仅此而已。而DaoAuthenticationProvider的职责更大它完成完整的整个认证流程同时会把UserDetails填充Authentication。UserDetails是什么publicinterfaceUserDetailsextendsSerializable{Collection?extendsGrantedAuthoritygetAuthorities();StringgetPassword();StringgetUsername();booleanisAccountNonExpired();booleanisAccountNonLocked();booleanisCredentialsNonExpired();booleanisEnabled();}我们知道了它的结构实现自己的实现类ServicepublicclassSpringDataUserDetailsServiceimplementsUserDetailsService{//根据用户名获取用户的信息OverridepublicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{//从数据库中查用户信息System.out.println(查询用户usernameusername);UserDetailsuserDetailsUser.withUsername(xiaowang).password(111).authorities(p1).build();returnuserDetails;}}同时屏蔽掉内存定义的用户启动服务器测试登录可以看到它实际调用了我们自定义的UserDetailsServicePasswordEncoder自定义解析器。\qquadDaoAuthenticationProvider认证处理器通过UserDetailsService获取到UserDetails后它是如何与请求Authentication中的密码做对比的\qquad这里Spring Security为了适应多种多样的加密类型又做了抽象DaoAuthenticationProvider通过PasswordEncoder接口的matches方法进行密码的对比而具体的密码对比细节取决于实现publicinterfacePasswordEncoder{Stringencode(CharSequencevar1);booleanmatches(CharSequencevar1,Stringvar2);defaultbooleanupgradeEncoding(StringencodedPassword){returnfalse;}}而Spring Securiy提供很多内置的PasswordEncoder能够开箱即用使用某种PasswordEncoder只需要进行如下声明即可如下//密码编码器BeanpublicPasswordEncoderpasswordEncoder(){//原文密码比较returnNoOpPasswordEncoder.getInstance();}NoOpPasswordEncoder 采用字符串匹配方法不对密码进行加密比较处理密码比较流程如下用户输入密码明文DaoAuthenticationProvider获取UserDetails(其中存储了用户的正确密码)DaoAuthenticationProvider使用PasswordEncoder对输入的密码和正确的密码进行校验密码一致则通过校验否则校验失败。\qquadNoPasswordEncoder的校验规则拿输入的密码和UserDetails中的正确密码进行字符串比较字符串内容一致则校验成功否则校验失败。实际项目中推荐使用BCryptPasswordEncoderPkbdf2PasswordEncoderScrypePasswordEncoder等。如何使用BCryptPasswordEncoderBCryptPasswordEncoder并不需要引入新的依赖在安全类中定义BCryptPasswordEncoder//使用BCryptPasswordEncoder密码编码器BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}为了方便测试这里需要使用BCrypt生成一个密码由于加盐所以每次生成的都是不同的密码但是并不妨碍它的校验。RunWith(SpringRunner.class)publicclassBcryptTest{TestpublicvoidtestBcyrpt(){//BCrypt.gensalt() 生成盐StringhashpwBCrypt.hashpw(123,BCrypt.gensalt());System.out.println(hashpw);//校验booleancheckpwBCrypt.checkpw(123,$2a$10$NcYCXQUjgeCzc2NWWop6s.pz6KCW9QMaLkBYKu34Co38KTJ3ef2jW);System.out.println(checkpw);}}把用户的密码替换再次启动测试登录授权流程授权流程\qquad通过前面我们知道Spring Security 可以通过http.authorizeRequests()对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截最终实现对资源的授权访问。分析授权流程拦截请求已认证用户访问受保护的web资源将被SecurityFilterChain中的FilterSecurityInterceptor的子类拦截。获取资源访问策略FilterSecurityInterceptor会从SecurityMetadataSource的子类DefaultFilterInvocationSecurityMetadataSource获取要访问当前资源所需要的权限CollectionConfigAttribue。SecurityMetadataSource其实就是读取访问策略的抽象而读取的内容其实就是我们配置的访问规则读取访问策略如http.authorizeRequests().antMatchers(/r/r1).hasAnyAuthority(p1).antMatchers(/r/r2).hasAnyAuthority(p2)最后FilterSecurityInterceptor会调用AccessDecisionManager进行授权决策如果决策通过则允许访问资源否则将禁止访问。AccessDecisionManager(访问决策管理器)的核心接口如下publicinterfaceAccessDecisionManager{voiddecide(Authenticationvar1,Objectvar2,CollectionConfigAttributevar3)throwsAccessDeniedException,InsufficientAuthenticationException;booleansupports(ConfigAttributevar1);booleansupports(Class?var1);}decide参数解释authentication: 要访问资源的访问者的身份object: 要访问的受保护资源web请求对应FilterInvocationconfigAttributes: 是受保护资源的访问策略通过SecurityMetadataSource获取授权决策AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源。\qquad通过上面可以看出AccessionDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication是否有权访问受保护对象进行投票AccessDecisionManager根据投票结果做出最终决策。AccessionDecisonVoter是一个接口其中定义有三个方法具体结构如下所示。publicinterfaceAccessDecisionVoterS{intACCESS_GRANTED1;intACCESS_ABSTAIN0;intACCESS_DENIED-1;booleansupports(ConfigAttributevar1);booleansupports(Class?var1);intvote(Authenticationvar1,Svar2,CollectionConfigAttributevar3);}vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。ACCESS_GRANTED表示同意ACCESS_DENIED表示拒绝ACCESS_ABSTAIN表示弃权。如果一个AccessDecisionVoter不能判定当前Authentication是否拥有访问对应受保护对象的权限则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN 。\qquadSpring Security内置了三个基于投票的AccessDecisionManager实现类如下它们分别是AffirmativeBased、ConsensusBased和UnanimousBased。\qquadAffirmativeBased的逻辑是1只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问2如果全部弃权也表示通过(3) 如果没有一个投赞成票但是有人投反对票则将抛出AccessDeniedException。Spring Security默认使用的是AffirmativeBased。\qquadConsensusBased的逻辑是(1)如果赞成票多于反对票则表示通过。(2)反过来如果反对票多于赞成票则将抛出AccessDeniedException(3)如果赞成票与反对票相同且不等于0并且属性allowIfEqualsGrantedDeniedDecisions的值为true,则表示通过否则将抛出异常AccessDeniedException。参数allowIfEqualsGrantedDeniedDecisions的值默认为true。(4)如果所有的AccessDecisionVoter都弃权了则将视参数allowIfAllAbstainDecisions的值而定如果该值为true则表示通过否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false。\qquadUnanimousBased的逻辑与另外两种实现有些不一样另外两种都会一次性把保护对象的配置属性全部传递给AccessDecisionVoter进行投票而UnanimousBased会一次值传递一个ConfigAttribute给AccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的ConfigAttribute中有一个能够匹配则投赞成票但是放到UnanimousdBased中其投票结果就不一定是赞成了。UnanimousBased的逻辑具体来说是这样的:(1)如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了则将抛出AccessDeniedException。(2)如果没有反对票但是有赞成票则表示通过。(3)如果全部弃权了则将视参数allowIfAllAbstainDecisions的值而定true则表示通过false则抛出AccessDeniedException。\qquadSpring Security内置了一些投票者实现类如RoleVoter、AuthenticatedVoter、WebExpressionVoter等。