0


Springsecurity的认证流程及鉴权流程(流程绝对清晰)

SpringSecurity

是Spring提供的一个权限管理框架。提供多种身份验证机制(表单登录、HTTP Basic、JWT无状态身份验证),提供细粒度的权限验证机制。提供内置的安全防护机制,保证服务的安全,防止服务遭受恶意攻击。如CSRF(跨站请求伪造),内部使用CORS机制来解决此问题。

同时可以对用户的密码进行加强管理,对用户密码进行加密,同时这个加密是不可逆的。这样即使数据库泄露,也不会暴露用户密码。保护用户的身份信息。

SpringSecurity的核心业务其实就两个

  • 认证: 验证当前用户是否为本系统用户
  • 授权:经过认证后判断当前用户是否有权限进行某个操作

1. 认证

登录校验流程:
在这里插入图片描述

SpringSecurity底层其实就是一个过滤器链,内部提供了各种功能的过滤器,核心过滤器如下

  • UsernamePasswordAuthtocationFilter:用户名密码认证过滤器。用于用户认证;
  • FilterSecurityInterceptor:负责权限校验的过滤器
  • ExceptionTranslationFilter:异常转换过滤器;在用户认证或者权限校验时出现异常,会捕获异常并进行相应的处理。

在这里插入图片描述

基于表单的认证流程:

  1. 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);}
  1. 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;}
  1. 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。

  1. 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接口

它是一个接口,所以需要看看它的实现类。

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类
  1. 执行doFilter方法;方法主要将ServletRequest、ServletResponse、FilterChain封装成FilterInvocation对象并传入到invoke方法
  2. 执行invoke方法;内部调用super.beforeInvocation方法
  3. 执行beforeInvocation方法;request的url获取安全属性(进入接口的权限信息), 同时调用attemptAuthorization方法
  4. 执行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动态鉴权流程解析

标签: spring

本文转载自: https://blog.csdn.net/qq_60264381/article/details/123071739
版权归原作者 蚂蚁牙黑147 所有, 如有侵权,请联系我们删除。

“Springsecurity的认证流程及鉴权流程(流程绝对清晰)”的评论:

还没有评论