SpringSecurity
是Spring提供的一个权限管理框架。提供多种身份验证机制(表单登录、HTTP Basic、JWT无状态身份验证),提供细粒度的权限验证机制。提供内置的安全防护机制,保证服务的安全,防止服务遭受恶意攻击。如CSRF(跨站请求伪造),内部使用CORS机制来解决此问题。
同时可以对用户的密码进行加强管理,对用户密码进行加密,同时这个加密是不可逆的。这样即使数据库泄露,也不会暴露用户密码。保护用户的身份信息。
SpringSecurity的核心业务其实就两个
- 认证: 验证当前用户是否为本系统用户
- 授权:经过认证后判断当前用户是否有权限进行某个操作
1. 认证
登录校验流程:
SpringSecurity底层其实就是一个过滤器链,内部提供了各种功能的过滤器,核心过滤器如下
- UsernamePasswordAuthtocationFilter:用户名密码认证过滤器。用于用户认证;
- FilterSecurityInterceptor:负责权限校验的过滤器
- ExceptionTranslationFilter:异常转换过滤器;在用户认证或者权限校验时出现异常,会捕获异常并进行相应的处理。
基于表单的认证流程:
- UsernamePasswordAuthenticationFilter
拦截登录请求(/login),并在attemptAuthentication方法进行认证,从请求中提取用户名和密码。创建一个未认证的UsernamePasswordAuthenticationToken对象,调用 AuthenticationManager接口(实际调用实现类的ProviderManager) 的 authenticate 方法进行身份验证,如果认证成功就放入到securityContextHolder(上下文)中。
关键源码:
@OverridepublicAuthenticationattemptAuthentication(HttpServletRequest request,HttpServletResponse response)throwsAuthenticationException{if(this.postOnly &&!request.getMethod().equals("POST")){thrownewAuthenticationServiceException("Authentication method not supported: "+ request.getMethod());}// 获取用户名String username =obtainUsername(request);
username =(username !=null)? username.trim():"";// 获取密码String password =obtainPassword(request);
password =(password !=null)? password :"";// 创建一个未认证的UsernamePasswordAuthenticationToken对象UsernamePasswordAuthenticationToken authRequest =UsernamePasswordAuthenticationToken.unauthenticated(username,
password);// Allow subclasses to set the "details" property// 将请求的其它信息提取到UsernamePasswordAuthenticationToken对象中setDetails(request, authRequest);// 调用authenticationManager接口.authenticate()。因为是接口,所以具体调用的是ProviderManager实现类。// authenticationManager接口是注入在AbstractAuthenticationProcessingFilter接口// 但是UsernamePasswordAuthenticationFilter是AbstractAuthenticationProcessingFilter接口的实现类,所以可以调用returnthis.getAuthenticationManager().authenticate(authRequest);}
- ProviderManager
ProviderManager.authenticate()方法流程如下:
ProviderManager内部管理了多个AuthenticationProvider(接口)来实现灵活的认证机制。每个AuthenticationProvider都负责一种特定类型的认证。如下:
- DaoAuthenticationProvider:用于表单登录认证。从数据库或其他持久化存储中加载用户信息并进行认证。
- LdapAuthenticationProvider:用于 LDAP 认证。
- OAuth2AuthenticationProvider:用于 OAuth2 认证
因为AuthenticationProvider是一个接口,所以具体调用的是实现类来进行认证。
这里使用的是DaoAuthenticationProvider实现类。但是因为DaoAuthenticationProvider实现类没有
ProviderManager.authenticate源码解析:
publicAuthenticationauthenticate(Authentication authentication)throwsAuthenticationException{// 获取待测试的认证对象的类类型Class<?extendsAuthentication> toTest = authentication.getClass();// 定义变量用于存储可能抛出的认证异常AuthenticationException lastException =null;AuthenticationException parentException =null;// 定义变量用于存储认证结果Authentication result =null;Authentication parentResult =null;// 当前处理的提供者位置和总提供者数量int currentPosition =0;int size =this.providers.size();// 遍历所有已配置的认证提供者for(AuthenticationProvider provider :getProviders()){// 如果当前提供者不支持该类型的认证请求,则跳过if(!provider.supports(toTest)){continue;}// 如果日志级别为TRACE,记录当前正在使用的认证提供者if(logger.isTraceEnabled()){
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(),++currentPosition, size));}try{// 使用当前提供者进行认证
result = provider.authenticate(authentication);// 如果认证成功,复制认证详细信息并退出循环if(result !=null){copyDetails(authentication, result);break;}}catch(AccountStatusException|InternalAuthenticationServiceException ex){// 处理账户状态异常和内部认证服务异常,准备异常信息并抛出prepareException(ex, authentication);throw ex;}catch(AuthenticationException ex){// 捕获其他认证异常,记录最后一次异常
lastException = ex;}}// 如果没有提供者成功认证且有父级认证管理器,尝试使用父级认证管理器if(result ==null&&this.parent !=null){try{
parentResult =this.parent.authenticate(authentication);
result = parentResult;}catch(ProviderNotFoundException ex){// 忽略提供者未找到异常}catch(AuthenticationException ex){
parentException = ex;
lastException = ex;}}// 如果认证成功if(result !=null){// 如果配置了认证后擦除凭据且认证结果实现了CredentialsContainer接口,擦除凭据if(this.eraseCredentialsAfterAuthentication &&(result instanceofCredentialsContainer)){((CredentialsContainer) result).eraseCredentials();}// 如果父级认证管理器未发布成功事件,则发布认证成功事件if(parentResult ==null){this.eventPublisher.publishAuthenticationSuccess(result);}return result;}// 如果没有任何提供者认证成功if(lastException ==null){// 创建并记录提供者未找到异常
lastException =newProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",newObject[]{ toTest.getName()},"No AuthenticationProvider found for {0}"));}// 如果父级认证管理器未发布失败事件,则准备认证失败异常if(parentException ==null){prepareException(lastException, authentication);}// 抛出最后一次认证异常throw lastException;}
- DaoAuthenticationProvider
需要知道的是DaoAuthenticationProvider实现类中,并没有认证方法authenticate,而认证方法是在父类(AbstractUserDetailsAuthenticationProvider抽象类)中的。但是实例化之后,子类是能够继承父类的方法的,所以具体说,DaoAuthenticationProvider实例是有认证方法(authenticate)的
可以看到AbstractUserDetailsAuthenticationProvider抽象类是实现了AuthenticationProvider接口的,也就说它肯定有认证方法authenticate的,抽象类为了复用,就直接写在本抽象类中,子类直接使用即可,避免重复代码。
AbstractUserDetailsAuthenticationProvider.authenticate()源码:
publicAuthenticationauthenticate(Authentication authentication)throwsAuthenticationException{// 确认authentication是UsernamePasswordAuthenticationToken类型,否则抛出异常Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,()->this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports","Only UsernamePasswordAuthenticationToken is supported"));// 从authentication中提取用户名String username =determineUsername(authentication);// 标志是否使用了缓存boolean cacheWasUsed =true;// 从缓存中获取用户信息UserDetails user =this.userCache.getUserFromCache(username);// 如果缓存中没有找到用户信息if(user ==null){
cacheWasUsed =false;try{// 从数据源中检索用户信息
user =retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);}catch(UsernameNotFoundException ex){// 记录日志并处理用户未找到异常this.logger.debug("Failed to find user '"+ username +"'");if(!this.hideUserNotFoundExceptions){throw ex;}thrownewBadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}// 确保retrieveUser返回的用户信息不为nullAssert.notNull(user,"retrieveUser returned null - a violation of the interface contract");}try{// 执行预认证检查this.preAuthenticationChecks.check(user);// 执行额外的认证检查additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);}catch(AuthenticationException ex){// 如果第一次检查失败,且用户信息来自缓存,则重新从数据源中检索用户信息并重试if(!cacheWasUsed){throw ex;}
cacheWasUsed =false;
user =retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);this.preAuthenticationChecks.check(user);// 进行密码比对additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);}// 执行后认证检查this.postAuthenticationChecks.check(user);// 如果用户信息不是从缓存中获取的,则将其放入缓存if(!cacheWasUsed){this.userCache.putUserInCache(user);}// 返回的principal对象Object principalToReturn = user;if(this.forcePrincipalAsString){
principalToReturn = user.getUsername();}// 创建并返回认证成功的Authentication对象returncreateSuccessAuthentication(principalToReturn, authentication, user);}
retrieveUser方法是抽象方法,所以是在DaoAuthenticationProvider实现类中实现的。
具体作用是调用UserDetailsService.loadUserByUsername()方法,获取真实的用户名、密码
DaoAuthenticationProvider.retrieveUser源码说明:
@OverrideprotectedfinalUserDetailsretrieveUser(String username,UsernamePasswordAuthenticationToken authentication)throwsAuthenticationException{// 准备防御时间攻击的保护措施prepareTimingAttackProtection();try{// 调用UserDetailsService的loadUserByUsername方法加载用户信息UserDetails loadedUser =this.getUserDetailsService().loadUserByUsername(username);// 如果返回的用户信息为null,则抛出InternalAuthenticationServiceException异常if(loadedUser ==null){thrownewInternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");}// 返回加载到的用户信息return loadedUser;}catch(UsernameNotFoundException ex){// 如果捕获到UsernameNotFoundException,进行时间攻击保护措施,并重新抛出异常mitigateAgainstTimingAttack(authentication);throw ex;}catch(InternalAuthenticationServiceException ex){// 如果捕获到InternalAuthenticationServiceException,直接重新抛出异常throw ex;}catch(Exception ex){// 捕获其他异常,并抛出InternalAuthenticationServiceExceptionthrownewInternalAuthenticationServiceException(ex.getMessage(), ex);}}
该方法主要是调用UserDetailsService接口的loadUserByUsername方法加载用户信息(获取数据库/自定义的用户名、密码),同时返回用户信息,交给authenticate方法验证,验证当前登录用户的信息(用户名、密码)和加载的(数据库)信息(用户名、密码)是否一致。
密码比对也是在DaoAuthenticationProvider.authenticate方法中。上面源代码可以看到,调用了additionalAuthenticationChecks抽象方法,由DaoAuthenticationProvider实现。
DaoAuthenticationProvider.additionalAuthenticationChecks()源码说明:
@Override@SuppressWarnings("deprecation")protectedvoidadditionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)throwsAuthenticationException{// 判断登录用户的密码是否为空if(authentication.getCredentials()==null){this.logger.debug("Failed to authenticate since no credentials provided");thrownewBadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}String presentedPassword = authentication.getCredentials().toString();// 密码比对,不一致则抛出异常if(!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())){this.logger.debug("Failed to authenticate since password does not match stored value");thrownewBadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}}
authenticate方法中 密码比对成功后,会将用户信息放入缓存中,其中包括了用户的权限信息。方便后期鉴权。
UserDetailsService
UserDetailsService是一个接口,所以获取真实的用户名、密码需要实现类提供。
SpringSecurity在项目开始时,是有给默认的用户名、密码的,存储在InMemoryUserDetailsManager实现的users属性中。
所以这里会调用InMemoryUserDetailsManager实现类的loadUserByUsername方法。从而获取真实的用户名和密码。
InMemoryUserDetailsManager源码说明:
publicclassInMemoryUserDetailsManagerimplementsUserDetailsManager,UserDetailsPasswordService{// 默认的用户名、密码privatefinalMap<String,MutableUserDetails> users =newHashMap<>();@OverridepublicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{// 根据登录用户名获取真实的用户名、密码UserDetails user =this.users.get(username.toLowerCase());// 为空则抛出异常if(user ==null){thrownewUsernameNotFoundException(username);}// 将真实的用户名、密码、权限封装到UserDetails对象中。returnnewUser(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());}}
这样DaoAuthenticationProvider.authenticate方法就拿到了真实的用户名和密码。
但是通常我们的真实用户名、密码是数据库提供的,而不是使用默认的。
所以这里我们可以自己实现UserDetailsService接口,提供用户信息。
自定义提供真实用户信息
配置信息如下:
自己实现UserDetailsService接口;
@ServicepublicclassMyUserDetailsServiceimplementsUserDetailsService{@AutowriteprivateUserMapper usermapper;@OverridepublicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{// 这里你可以从数据库或其他数据源加载用户信息User user = usermapper.getByUsername(username);if(user ==null){thrownewUsernameNotFoundException("用户不存在")}// 获取用户权限String authority = userService.getUserAuthorityInfo(userName);// ROLE_admin,ROLE_normal,sys:user:list,....// 返回真实信息returnnewAccountUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(),getUserAuthority(username));}}
需要将自定义实现类,注入到AuthenticationManagerBuilder中,这样SpringSecurity在认证时就知道使用这个实现类获取真实用户信息了。
java配置
@Configuration@EnableWebSecuritypublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateMyUserDetailsService myUserDetailsService;@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());}@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
http.authorizeRequests().anyRequest().authenticated().and().formLogin().permitAll().and().logout().permitAll();}@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}}
WebSecurityConfigurerAdapter是什么?
WebSecurityConfigurerAdapter是 Spring Security 提供的一个适配器类,Spring Security也是通过这个类中的信息,去灵活处理认证流程。所以我们继承该类并覆盖其中方法,就可以实现自定义的安全策略(如自定义认证)。
总结:
第一步:
UsernamePasswordAuthenticationFilte过滤器
拦截login登录请求,从请求头中获取用户名、密码,创建一个未认证UsernamePasswordAuthenticationToken对象
调用真正认证接口AuthenticationManager.authenticate方法
第二步:
AuthenticationManager真正的认证接口
有多个认证实现类,使用默认实现类ProviderManager进行投票
第三步:
ProviderManager认证实现类
内部管理了多个AuthenticationProvider身份认证提供者接口,实现灵活的认证机制
第四步:
AuthenticationProvider身份认证提供者接口
- DaoAuthenticationProvider:用于表单登录认证。从数据库或其他持久化存储中加载用户信息并进行认证。
- LdapAuthenticationProvider:用于 LDAP 认证。
- OAuth2AuthenticationProvider:用于 OAuth2 认证 因为是表单提交,所以使用DaoAuthenticationProvider身份认证提供者实现类进行认证。
第五步:
DaoAuthenticationProvider身份认证提供者实现类
执行authenticate方法,从UserDetailsService接口的loadUserByUsername方法加载用户信息(获取数据库/自定义的用户名、密码)
同时跟当前登录用户进行密码比对,进行认证,认证成功则放入到缓存中,方便后期鉴权。
第六步:
UserDetailsService接口
SpringSecurity启动会默认的用户名和密码,放入在InMemoryUserDetailsManager实现类中,所以获取真实的用户信息是由InMemoryUserDetailsManager实现类的。
同时我们也可以自己实现UserDetailsService接口,自己提供数据库中的用户名、密码
**所以真正的认证过滤器是
AuthenticationManager
,但是它会委托给不同AuthenticationProvider身份认证提供者接口进行认证!!!**
1.1 自定义实现登录认证
我们知道在SpringSecurity中,usernamePassword会拦截登录请求,同时调用ProviderManager。
ProviderManager内部管理了多个AuthenticationProvider(接口)来实现灵活的认证机制内部管理了多个AuthenticationProvider(接口)来实现灵活的认证机制
也就是说,ProviderManager会决定调用具体AuthenticationProvider实现类来进行认证。
那我们就有思路了,我们自己实现AuthenticationProvider接口不就好了。后续ProviderManager类就会调用我们自定义的认证实现类。
自定义实现类:
@ComponentpublicclassMyAuthenticationProviderimplementsAuthenticationProvider{@AutowiredprivateMyUserDetailsService myUserDetailsService;@AutowiredBCryptPasswordEncoder bCryptPasswordEncoder;@OverridepublicAuthenticationauthenticate(Authentication authentication)throwsAuthenticationException{//获取用户输入的用户名和密码String username = authentication.getName();String password = authentication.getCredentials().toString();try{//解密密码// BASE64Decoder base64Decoder = new BASE64Decoder();// byte[] passByte = Base64.getMimeDecoder().decode(password);// byte[] passByte = base64Decoder.decodeBuffer(password);
password =AESUtils.decrypt(password);}catch(Exception e){
e.printStackTrace();thrownewBadCredentialsException("用户名或密码错误!");}//获取封装用户UserDetails user = myUserDetailsService.loadUserByUsername(username);//进行密码比对if(bCryptPasswordEncoder.matches(password,user.getPassword())){//验证成功returnnewUsernamePasswordAuthenticationToken(username,user.getPassword(),user.getAuthorities());}thrownewBadCredentialsException("用户名或密码错误!");}@Overridepublicbooleansupports(Class<?> authentication){returntrue;}
这里需要注意的是:
myUserDetailsService是之前我们自己实现的自定义实现的提供真实用户信息类。
而自定义实现的提供真实用户信息类,它实现了UserDetailsService接口。是DaoAuthenticationProvider实现类的一个流程。这是源码自己实现的一个流程。如下
那在自定义认证实现类中,我也可以自定义的流程, 那我就直接与数据库交互。不依靠UserDetailsService接口。也就是说我们可以直接注入xxMapper接口,获取真实的用户信息。
配置适配器类。
@Configuration@EnableWebSecuritypublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateMyAuthenticationProvider myAuthenticationProvider;@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{// 注册自定义的认证实现类 AuthenticationProvider
auth.authenticationProvider(myAuthenticationProvider);}@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
http.authorizeRequests().anyRequest().authenticated().and().formLogin().permitAll().and().logout().permitAll();}@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}@Bean@OverridepublicAuthenticationManagerauthenticationManagerBean()throwsException{returnsuper.authenticationManagerBean();}}
注意:**configure(AuthenticationManagerBuilder auth)**方法,只注入了自定义认证实现类,而自定义实现的提供真实用户信息类并没有注入到
AuthenticationManagerBuilder
中,刚才也说到了,自定义认证实现类都可以直接与数据库交互拿到真实信息了,那还注入 自定义实现的提供真实用户信息类 有啥用。
参考文章:SpringSecurity认证流程(超级详细)
这里需要注意的是;在SpringSecurity认证流程中,
AbstractUserDetailsAuthenticationProvider.authenticate()方法中,如果认证成功了就会将用户放入缓存中。
同时会将用户信息放入HttpSession中,避免重复认证。
部分源码如下:
publicAuthenticationauthenticate(Authentication authentication)throwsAuthenticationException{...// 标志是否使用了缓存boolean cacheWasUsed =true;// 从缓存中获取用户信息UserDetails user =this.userCache.getUserFromCache(username);// 如果缓存中没有找到用户信息if(user ==null){
cacheWasUsed =false;try{// 从数据源中检索用户信息
user =retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);}catch(UsernameNotFoundException ex){// 记录日志并处理用户未找到异常....}try{// 执行预认证检查this.preAuthenticationChecks.check(user);// 执行额外的认证检查additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);}catch(AuthenticationException ex){...}// 执行后认证检查this.postAuthenticationChecks.check(user);// 如果用户信息不是从缓存中获取的,则将其放入缓存if(!cacheWasUsed){this.userCache.putUserInCache(user);}
但是我们需要知道:目前都是前后端分离项目,已经不使用
HttpSession
了,而是使用
JWT
,更何况HttpSession会出现CSRF(跨站请求伪造)问题。
1.2 自定义JWT认证
这里需要注意的是;自定义JWT认证跟自定义登录认证是不同的认证。
UsernamePasswordAuthenticationFilter过滤器会拦截/login的请求,同时调用authenticationManager接口(ProviderManager类->AuthenticationProvider类->DaoAuthenticationProvider类)的authenticate(),进行登录认证
而实现JWT认证,是依靠BasicAuthenticationFilter过滤器进行认证。
BasicAuthenticationFilter与UsernamePasswordAuthenticationFilter的区别及共同点
区别:
是两个不同的过滤器,处理不同的请求。
- UsernamePasswordAuthenticationFilter会处理请求地址为login的请求,并调用authenticationManager进行认证
- BasicAuthenticationFilter会处理请求头‘Authorization’的请求,同时格式正确的话,就会直接进行认证。否则会放行请求,不做认证处理。
共同点:
都间接继承了GenericFilterBean抽象类
UsernamePasswordAuthenticationFilter:
BasicAuthenticationFilter:
这里可以看到BasicAuthenticationFilter是继承OncePerRequestFilter抽象类。
这个OncePerRequestFilter抽象类,会保证
子类的过滤逻辑
在每次调用链中都
执行一次
,请求都会被OncePerRequestFilter拦截。
而BasicAuthenticationFilter继承了它,就说明请求都会进入到BasicAuthenticationFilter过滤器中
但是BasicAuthenticationFilter过滤器只会认证带有请求头‘Authorization’的请求,同时需要格式正确
也就知道了login请求也会进入BasicAuthenticationFilter过滤器,但是因为不带请求头‘Authorization’,所以不会进行认证处理。而是交给UsernamePasswordAuthenticationFilter进行认证处理。
BasicAuthenticationFilter源码说明:
@OverrideprotectedvoiddoFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain)throwsIOException,ServletException{try{// 尝试从请求中提取用户名和密码,生成一个 UsernamePasswordAuthenticationToken 对象UsernamePasswordAuthenticationToken authRequest =this.authenticationConverter.convert(request);// 如果提取失败(即未找到 Authorization 头中的用户名和密码)if(authRequest ==null){this.logger.trace("Did not process authentication request since failed to find "+"username and password in Basic Authorization header");// 继续过滤器链的下一个过滤器
chain.doFilter(request, response);return;}// 获取提取到的用户名String username = authRequest.getName();this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username));// 检查是否需要进行认证if(authenticationIsRequired(username)){// 调用 AuthenticationManager 进行认证Authentication authResult =this.authenticationManager.authenticate(authRequest);// 创建一个空的 SecurityContextSecurityContext context =this.securityContextHolderStrategy.createEmptyContext();// 将认证结果放入 SecurityContext
context.setAuthentication(authResult);// 将 SecurityContext 放入 SecurityContextHolderthis.securityContextHolderStrategy.setContext(context);if(this.logger.isDebugEnabled()){this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));}// 调用 RememberMeServices 登录成功的方法this.rememberMeServices.loginSuccess(request, response, authResult);// 保存 SecurityContextthis.securityContextRepository.saveContext(context, request, response);// 处理成功认证后的逻辑onSuccessfulAuthentication(request, response, authResult);}}catch(AuthenticationException ex){// 认证失败,清空 SecurityContextthis.securityContextHolderStrategy.clearContext();this.logger.debug("Failed to process authentication request", ex);// 调用 RememberMeServices 登录失败的方法this.rememberMeServices.loginFail(request, response);// 处理认证失败的逻辑onUnsuccessfulAuthentication(request, response, ex);if(this.ignoreFailure){// 如果配置了忽略失败,继续过滤器链的下一个过滤器
chain.doFilter(request, response);}else{// 否则,调用 AuthenticationEntryPoint 来处理认证失败的情况(如返回 401 错误)this.authenticationEntryPoint.commence(request, response, ex);}return;}// 在没有异常的情况下,继续过滤器链的下一个过滤器
chain.doFilter(request, response);}
我们可以看到BasicAuthenticationFilter.doFilterInternal方法也会调用 AuthenticationManager 进行认证处理。同时放入缓存中。方便后期鉴权
那这时我们是不是可以通过继承OncePerRequestFilter类,并重写doFilterInternal方法,在方法里拦截指定请求并进行认证,跟BasicAuthenticationFilter的doFilterInternal方法大同小异。
与BasicAuthenticationFilter不同的是,它调用AuthenticationManager进行认证处理,而我们的JWT实现类是验证JWT,因为JWT是登录成功后返回给用户的,证明之前已经认证过了,也就不用再次访问数据库进行验证。最后根据JWT获取的用户名生成一个UsernamePasswordAuthenticationToken,并放入到缓存中,方便后期鉴权。
注:以下是用户信息在登录时使用redis中间件进行缓存的,所以在这里可以直接获取,最后也会将用户的权限信息放入到SpringSecurity上下文中,方便后期SpringSecurity根据上下文信息自己鉴权。
但是需要特别注意的是:
每次请求都会认证一次。
SpringSecurity是使用自己实现的缓存,防止重复调用数据库认证。
而我们是使用redis+JWT,其实效果也差不多。
JWT实现类:
@ComponentpublicclassJwtAuthenticationTokenFilterextendsOncePerRequestFilter{privatefinalRedisUtil redisUtil;privatefinalAuthManager authManager;@AutowiredpublicJwtAuthenticationTokenFilter(RedisUtil redisUtil,AuthManager authManager){this.redisUtil = redisUtil;this.authManager = authManager;}@OverrideprotectedvoiddoFilterInternal(HttpServletRequest request,@NotNullHttpServletResponse response,@NotNullFilterChain filterChain)throwsServletException,IOException{String authorizationInfo = request.getHeader("Authorization");if(ObjectUtil.isEmpty(authorizationInfo)){
filterChain.doFilter(request, response);return;}String token = authorizationInfo.substring("Bearer".length());// Bearer tokenString sysUserId;try{Claims claims =JwtUtil.parseJWT(token);
sysUserId = claims.getSubject();}catch(Exception e){thrownewRuntimeException("非法token");}// 从redis获取用户信息String redisKey = authManager.getRedisUserKey(sysUserId);Object accountUserObject = redisUtil.getCacheObject(redisKey);if(ObjectUtil.isNull(accountUserObject)){thrownewRuntimeException("用户未登录");}AccountUser accountUser =JSON.parseObject(accountUserObject.toString(),AccountUser.class);// 存入SecurityContextHolderUsernamePasswordAuthenticationToken authenticationToken =newUsernamePasswordAuthenticationToken(accountUser,null,accountUser.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request,response);}}
SpringSecurity适配器类相关配置
@Configuration@EnableWebSecuritypublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateMyUserDetailsService myUserDetailsService;@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());}@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
http.authorizeRequests().anyRequest().authenticated().and().formLogin().permitAll().and().logout().permitAll().and()// 注入过滤器,执行顺序在UsernamePasswordAuthenticationFilter前面.addFilterBefore(jwtAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);}@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}}
2. 鉴权
如何在应用中拿到security上下文(用户信息)
之前的源码中,都能看到在认证成功后,都会将用户信息放入SpringSecurity上下文中。也就是SecurityContext,
而SecurityContextHolder是管理SecurityContext上下文的,所以我们可以通过它更新/获取用户信息。
// 更新上下文信息SecurityContextHolder.getContext().setAuthentication(Authentication authentication);// 获取当前用户的信息SecurityContextHolder.getContext().getAuthentication();
2.1 流程
从上图可以看到,认证成功之后就进入到我们的鉴权环节了,也就是进行了鉴权过滤器FilterSecurityInterceptor。
- FilterSecurityInterceptor类
首先根据源码看看它具体做了什么事情。
FilterSecurityInterceptor关键源码:
@OverridepublicvoiddoFilter(ServletRequest request,ServletResponse response,FilterChain chain)throwsIOException,ServletException{invoke(newFilterInvocation(request, response, chain));}
可以看到是将
request
、
response
、
chain
都封装成了一个FilterInvocation对象。之后调用本类的invoke方法。
ServletRequest、ServletResponse大家都熟悉,但是这个FilterChain对象是什么?
FilterChain封装了当前整个过滤器链,鉴权过滤器可以在 doFilter 方法中执行鉴权逻辑。如果鉴权失败,它可以选择不调用 FilterChain.doFilter 方法,从而阻止请求继续传递到后续的过滤器或目标资源。
接下看看invoke方法做了什么事情?
publicvoidinvoke(FilterInvocation filterInvocation)throwsIOException,ServletException{// 1. 检查是否已经应用过滤器,并且用户要求我们观察每个请求只处理一次if(isApplied(filterInvocation)&&this.observeOncePerRequest){// 2. 如果已经应用过滤器且要求每个请求只处理一次,则直接传递请求给下一个过滤器链
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());return;}// 3. 如果是第一次应用此请求的过滤器,则设置一个标记,以指示此过滤器已应用于请求if(filterInvocation.getRequest()!=null&&this.observeOncePerRequest){
filterInvocation.getRequest().setAttribute(FILTER_APPLIED,Boolean.TRUE);}// 4. 执行拦截器之前的操作,可能会抛出 AccessDeniedExceptionInterceptorStatusToken token =super.beforeInvocation(filterInvocation);try{// 5. 执行请求链,传递请求给下一个过滤器或目标资源
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());}finally{// 6. 执行拦截器之后的操作,用于清理资源或状态super.finallyInvocation(token);}// 在请求处理完成后,执行拦截器的最终操作super.afterInvocation(token,null);}
主要看第4步及之后步骤。
调用父类的beforeInvocation方法,方法完成后,将请求传递给下一个过滤器,同时进行一些后置处理。
FilterSecurityInterceptor是继承了AbstractSecurityInterceptor抽象类的。
那看看super.beforeInvocation方法做了什么?
这里要重点看看,因为进行了鉴权!!
AbstractSecurityInterceptor.beforeInvocation源码:
protectedInterceptorStatusTokenbeforeInvocation(Object object){// 确保传入的对象不为空Assert.notNull(object,"Object was null");// 检查传入的对象是否是安全对象if(!getSecureObjectClass().isAssignableFrom(object.getClass())){thrownewIllegalArgumentException("Security invocation attempted for object "+ object.getClass().getName()+" but AbstractSecurityInterceptor only configured to support secure objects of type: "+getSecureObjectClass());}// 根据request的url获取对应Controller的安全属性 ,如获取@PreAuthorize注解Collection<ConfigAttribute> attributes =this.obtainSecurityMetadataSource().getAttributes(object);// 如果安全属性为空,表示该对象为公共对象,不需要进行进一步的安全检查if(CollectionUtils.isEmpty(attributes)){Assert.isTrue(!this.rejectPublicInvocations,()->"Secure object invocation "+ object
+" was denied as public invocations are not allowed via this interceptor. "+"This indicates a configuration error because the "+"rejectPublicInvocations property is set to 'true'");if(this.logger.isDebugEnabled()){this.logger.debug(LogMessage.format("Authorized public object %s", object));}// 发布公共对象事件publishEvent(newPublicInvocationEvent(object));returnnull;// 不需要进一步的工作}// 检查当前安全上下文中是否存在认证对象if(this.securityContextHolderStrategy.getContext().getAuthentication()==null){credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound","An Authentication object was not found in the SecurityContext"), object, attributes);}// 如果需要认证,则执行认证Authentication authenticated =authenticateIfRequired();if(this.logger.isTraceEnabled()){this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));}// 尝试授权attemptAuthorization(object, attributes, authenticated);if(this.logger.isDebugEnabled()){this.logger.debug(LogMessage.format("Authorized %s with attributes %s", object, attributes));}// 如果需要运行作为不同的用户,则构建运行作为认证对象Authentication runAs =this.runAsManager.buildRunAs(authenticated, object, attributes);if(runAs !=null){// 切换为运行作为认证对象SecurityContext origCtx =this.securityContextHolderStrategy.getContext();SecurityContext newCtx =this.securityContextHolderStrategy.createEmptyContext();
newCtx.setAuthentication(runAs);this.securityContextHolderStrategy.setContext(newCtx);if(this.logger.isDebugEnabled()){this.logger.debug(LogMessage.format("Switched to RunAs authentication %s", runAs));}// 需要在调用后恢复到原始认证对象returnnewInterceptorStatusToken(origCtx,true, attributes, object);}this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");// 不需要进一步的工作returnnewInterceptorStatusToken(this.securityContextHolderStrategy.getContext(),false, attributes, object);}
1、this.obtainSecurityMetadataSource().getAttributes(object);
根据request的url获取对应Controller的安全属性(ROLE_USER),不管是在controller接口上使用注解还是使用配置类配置的都能获取到
// 配置类@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
http
.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN").antMatchers("/user/**").hasRole("USER").anyRequest().authenticated();}@GetMapping("/user/profile")@PreAuthorize("hasRole('USER')")publicStringuserProfile(){return"User Profile";}
2、authenticateIfRequired(); 获取当前用户信息 返回Authentication
3、attemptAuthorization(object, attributes, authenticated);
开始鉴权
,同时将目标接口的安全属性、用户信息、FilterInvocation注入到方法中。
AbstractSecurityInterceptor.attemptAuthorization方法源码:
privateAccessDecisionManager accessDecisionManager;privatevoidattemptAuthorization(Object object,Collection<ConfigAttribute> attributes,Authentication authenticated){try{// 其实就做了一件事,调用accessDecisionManager.decide()进行鉴权this.accessDecisionManager.decide(authenticated, object, attributes);}catch(AccessDeniedException ex){if(this.logger.isTraceEnabled()){this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object,
attributes,this.accessDecisionManager));}elseif(this.logger.isDebugEnabled()){this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));}publishEvent(newAuthorizationFailureEvent(object, attributes, authenticated, ex));throw ex;}}
2.2 AccessDecisionManager
那这时我们就知道了,鉴权的真正处理者是:
AccessDecisionManager接口
AccessDecisionManager接口源码:
publicinterfaceAccessDecisionManager{// 主要鉴权方法voiddecide(Authentication authentication,Object object,Collection<ConfigAttribute> configAttributes)throwsAccessDeniedException,InsufficientAuthenticationException;booleansupports(ConfigAttribute attribute);booleansupports(Class<?> clazz);}
那AccessDecisionManager既然是接口,肯定有实现类。看看接口的结构树
从图中我们可以看到它主要有三个实现类,分别代表了三种不同的鉴权逻辑:
- AffirmativeBased:一票通过,只要有一票通过就算通过,默认是它。
- UnanimousBased:一票反对,只要有一票反对就不能通过。
- ConsensusBased:少数票服从多数票。
2.3 AffirmativeBased实现类
默认调用该类进行投票;
一票通过,只要有一票通过就算通过,默认是它。
publicvoiddecide(Authentication authentication,Object object,Collection<ConfigAttribute> configAttributes)throwsAccessDeniedException{// 初始化拒绝计数器int deny =0;// 遍历所有的决策投票者for(AccessDecisionVoter voter :getDecisionVoters()){// 每个投票者对当前请求进行投票int result = voter.vote(authentication, object, configAttributes);// 根据投票结果执行相应的逻辑switch(result){// 如果有投票者授予了访问权限,则立即返回,表示访问被授予caseAccessDecisionVoter.ACCESS_GRANTED:return;// 如果有投票者拒绝了访问权限,则增加拒绝计数器caseAccessDecisionVoter.ACCESS_DENIED:
deny++;// 结束当前选择器(switch),进行下一个循环break;// 如果投票者弃权,则不进行任何操作default:break;}}// 如果有投票者拒绝了访问权限,抛出 AccessDeniedException 异常,表示访问被拒绝if(deny >0){thrownewAccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied","Access is denied"));}// 如果所有投票者都弃权,检查是否允许访问checkAllowIfAllAbstainDecisions();}
getDecisionVoters是父类AbstractAccessDecisionManager抽象类的方法,投票器也是注入父类属性中的,如下:
publicabstractclassAbstractAccessDecisionManagerimplementsAccessDecisionManager,InitializingBean,MessageSourceAware{privateList<AccessDecisionVoter<?>> decisionVoters;publicList<AccessDecisionVoter<?>>getDecisionVoters(){returnthis.decisionVoters;}}
在源码中可以看到,AffirmativeBased将鉴权委托给了各个投票器
AccessDecisionVoter
,每个投票器根据自身逻辑来进行投票。
其中只要有一个投票器通过就立马返回,表示有该接口权限。可以访问
2.3 AccessDecisionVoter接口
它是一个接口,所以需要看看它的实现类。
- RoleVoter:根据用户所需的角色进行来投票。角色信息从ConfigAttribute中获取
- AuthenticatedVoter:根据用户的认证状态投票(例如:匿名用户、已认证用户)。如ConfigAttribute 要求特定的认证状态(如 “IS_AUTHENTICATED_FULLY”),则根据用户的认证状态进行投票。
- Jsr250Voter:检查方法或类上是否有 @RolesAllowed 注解,并根据注解中的角色信息进行投票。
- WebExpressionVoter:使用 SpEL 表达式来进行复杂的访问控制决策。
- PreInvocationAuthorizationAdviceVoter:检查方法或类上是否有@PreAuthorize 和 @PostAuthorize 注解,根据注解信息进行投票
这里需要注意:
AffirmativeBased会将decisionVoters属性的投票器都拿出来进行投票,而不是调用指定的投票器,源码中也写出来了使用for循环
而AbstractAccessDecisionManager.decisionVoters属性;默认注入的投票器有
- RoleVoter
- AuthenticatedVoter
- WebExpressionVoter
但是我们一般是在方法上使用@PreAuthorize(方法调用之前)和@PostAuthorize(方法调用之后)进行权限控制的
@PreAuthorize("hasRole('ROLE_ADMIN')")publicvoiddeleteUser(Long userId){// 只有具有 ROLE_ADMIN 角色的用户才能执行此方法
userRepository.deleteById(userId);}@PostAuthorize("returnObject.owner == authentication.name")publicDocumentgetDocument(Long documentId){// 方法执行后,会检查返回的 Document 的 owner 是否与当前用户匹配return documentRepository.findById(documentId).orElse(null);}
而扫描这两个注解的投票器PreInvocationAuthorizationAdviceVoter,并没有注入到decisionVoters属性中,那投票的时候岂不是直接拒绝了?那这种时候应该怎么办呢?
其实可以使用注解@EnableGlobalMethodSecurity
@Configuration@EnableGlobalMethodSecurity(prePostEnabled =true, jsr250Enabled =true)publicclassMethodSecurityConfigextendsWebSecurityConfigurerAdapter{// 这里可以进行额外的配置}
- prePostEnabled = true:将PreInvocationAuthorizationAdviceVoter投票器注入到decisionVoters属性中参与投票。解析@PreAuthorize和@PostAuthorize注解信息
- jsr250Enabled = true:将Jsr250Voter注入到decisionVoters属性中参与投票:解析@RolesAllowed 注解信息。
这样后续投票时就会扫描对应投票器并参与投票!!!
需要注意的是:
我们在之前讲到FilterSecurityInterceptor.beforeInvocation方法中获取到@PreAuthorize注解信息;
如配置了@PreAuthorize,就会返回一个 PreInvocationAttribute实例,包含hasRole(‘ROLE_ADMIN’)的表达式
@PreAuthorize("hasRole('ROLE_ADMIN')")publicvoidadminMethod(){System.out.println("Admin method");}
此时又衍生出一个问题FilterSecurityInterceptor.beforeInvocation方法怎么知道要扫描哪些配置(如具体注解、configure配置)?
还记得@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)注解嘛?
它的作用其实就是FilterSecurityInterceptor.beforeInvocation方法就是让知道扫描什么配置,同时会将具体投票器注入到AbstractAccessDecisionManager.decisionVoters属性,后期参与投票
而FilterSecurityInterceptor.beforeInvocation方法是通过SecurityMetadataSource进行扫描的。
但是SecurityMetadataSource是一个接口,而MethodSecurityMetadataSource是它的具体实现类。
所以实际的扫描工作是MethodSecurityMetadataSource完成的。
所以这里就可以得出结论了
配置了@EnableGlobalMethodSecurity注解,会让MethodSecurityMetadataSource类知道扫描哪些配置
同时将PreInvocationAuthorizationAdviceVoter投票器注入到AbstractAccessDecisionManager.decisionVoters属性.
后期PreInvocationAuthorizationAdviceVoter投票器就会根据自身业务逻辑进行判断投票,如下:
@Overridepublicintvote(Authentication authentication,MethodInvocation method,Collection<ConfigAttribute> attributes){// Find prefilter and preauth (or combined) attributes// if both null, abstain else call advice with them// 判断当前实例是否属于该类PreInvocationAttribute preAttr =findPreInvocationAttribute(attributes);if(preAttr ==null){// No expression based metadata, so abstainreturnACCESS_ABSTAIN;}// 调用PreInvocationAuthorizationAdvice进行解析判断returnthis.preAdvice.before(authentication, method, preAttr)?ACCESS_GRANTED:ACCESS_DENIED;}privatePreInvocationAttributefindPreInvocationAttribute(Collection<ConfigAttribute> config){for(ConfigAttribute attribute : config){if(attribute instanceofPreInvocationAttribute){return(PreInvocationAttribute) attribute;}}returnnull;}
可以判断会先进行判断,判断之前通过 FilterSecurityInterceptor.beforeInvocation 拿到的安全属性实例,是否属于PreInvocationAttribute类。
后面调用PreInvocationAuthorizationAdvice进行解析判断
PreInvocationAuthorizationAdvice.before源码
@Overridepublicbooleanbefore(Authentication authentication,MethodInvocation mi,PreInvocationAttribute attr){// 将 PreInvocationAttribute 强制转换为 PreInvocationExpressionAttributePreInvocationExpressionAttribute preAttr =(PreInvocationExpressionAttribute) attr;// 获取用户权限信息 使用表达式处理器创建一个 EvaluationContext(评估上下文)EvaluationContext ctx =this.expressionHandler.createEvaluationContext(authentication, mi);// 获取 PreInvocationExpressionAttribute 中的过滤表达式 Expression preFilter = preAttr.getFilterExpression();// 获取 PreInvocationExpressionAttribute 中的授权表达式Expression preAuthorize = preAttr.getAuthorizeExpression();// 如果有预过滤表达式if(preFilter !=null){// 查找需要过滤的目标Object filterTarget =findFilterTarget(preAttr.getFilterTarget(), ctx, mi);// 通过表达式处理器对过滤目标应用过滤表达式this.expressionHandler.filter(filterTarget, preFilter, ctx);}// 如果有预授权表达式,则评估该表达式,并将其结果作为方法返回值// 否则,默认返回 true,表示授权通过return(preAuthorize !=null)?ExpressionUtils.evaluateAsBoolean(preAuthorize, ctx):true;}
过滤表达式可以忽略 基本不用 格式如下:
// 授权表达式@PreAuthorize("hasRole('ROLE_ADMIN')")// 过滤表达式@PreFilter("filterObject.owner == authentication.name")
this.expressionHandler.createEvaluationContext(authentication, mi); 获取用户上下文,并创建一个EvaluationContext(评估上下文)
(preAuthorize != null) ? ExpressionUtils.evaluateAsBoolean(preAuthorize, ctx) : true; 与安全属性进行判断,评估
ExpressionUtils.evaluateAsBoolean(preAuthorize, ctx)源码
publicstaticbooleanevaluateAsBoolean(Expression expr,EvaluationContext ctx){try{return expr.getValue(ctx,Boolean.class);}catch(EvaluationException ex){thrownewIllegalArgumentException("Failed to evaluate expression '"+ expr.getExpressionString()+"'",
ex);}}
其实就是判断当前用户权限信息包含安全属性(授权表达式)
所以鉴权流程为:
第一步:
FilterSecurityInterceptor类
- 执行doFilter方法;方法主要将ServletRequest、ServletResponse、FilterChain封装成FilterInvocation对象并传入到invoke方法
执行invoke方法
;内部调用super.beforeInvocation方法- 执行beforeInvocation方法;request的url
获取安全属性
(进入接口的权限信息), 同时调用attemptAuthorization方法 - 执行attemptAuthorization方法;调用真正的
鉴权过滤器accessDecisionManager.decide()
执行dofilter方法,获取请求的目标接口的安全信息(权限信息),调用真正的鉴权过滤器方法accessDecisionManager.decide
第二步:
AccessDecisionManager鉴权接口
有多个鉴权实现类
- AffirmativeBased:一票通过,只要有一票通过就算通过,默认是它。
- UnanimousBased:一票反对,只要有一票反对就不能通过。
- ConsensusBased:少数票服从多数票。
默认使用AffirmativeBased鉴权实现类(一票通过)
第三步:AccessDecisionManager鉴权实现类
内部使用投票机制,同时将
自身鉴权能力委托各投票器实现
。根据内部的List
decisionVoters
投票器列表属性,投票器列表在项目启动时就注入到该属性中了,循环调用投票器进行投票,只要有一票通过即可,即调用AccessDecisionVoter.vote方法。进行鉴权
第四步:
AccessDecisionVoter投票接口
- RoleVoter:根据用户所需的角色进行来投票。角色信息从ConfigAttribute中获取
- AuthenticatedVoter:根据用户的认证状态投票(例如:匿名用户、已认证用户)。如ConfigAttribute 要求特定的认证状态(如 “IS_AUTHENTICATED_FULLY”),则根据用户的认证状态进行投票。
- Jsr250Voter:检查方法或类上是否有 @RolesAllowed 注解,并根据注解中的角色信息进行投票。
- WebExpressionVoter:使用 SpEL 表达式来进行复杂的访问控制决策。
- PreInvocationAuthorizationAdviceVoter:检查方法或类上是否有@PreAuthorize 和 @PostAuthorize 注解,根据注解信息进行投票
有多个投票器实现类,每个投票器有不同的投票逻辑。如RoleVoter投票实现类启动时就注入到AffirmativeBased鉴权实现类中了
第五步:
RoleVoter投票实现类
根据目标接口的安全属性(权限信息),查看用户是否有当前权限。角色信息从ConfigAttribute中获取
但是我们一般是使用
@PreAuthorize注解
来配置接口安全属性(权限),所以需要声明
注解@EnableGlobalMethodSecurity
这样后期
FilterSecurityInterceptor类
使用
MethodSecurityMetadataSource
就知道
扫描@PreAuthorize
注解并封装成为
PreInvocationAttribute实例
。
同时将
PreInvocationAuthorizationAdviceVoter投票器实现类
注入到AffirmativeBased实现类的decisionVoters属性。后期可调用该投票器进行投票,投票时会解析PreInvocationAttribute实例,并进行权限判断,为true则通过。
**所以真正的鉴权过滤器是
AccessDecisionManager
,但是它会委托给不同的AccessDecisionVoter投票器实现!!!**
至此完结!!
参考文章:
Spring Security 鉴权流程
SpringSecurity动态鉴权流程解析
版权归原作者 蚂蚁牙黑147 所有, 如有侵权,请联系我们删除。