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

SpringSecurity

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

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

SpringSecurity的核心业务其实就两个

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

1. 认证

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

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

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

在这里插入图片描述

基于表单的认证流程:

  1. UsernamePasswordAuthenticationFilter

拦截登录请求(/login),并在attemptAuthentication方法进行认证,从请求中提取用户名和密码。创建一个未认证的UsernamePasswordAuthenticationToken对象,调用 AuthenticationManager接口(实际调用实现类的ProviderManager) 的 authenticate 方法进行身份验证,如果认证成功就放入到securityContextHolder(上下文)中。

关键源码:

@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("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接口的实现类,所以可以调用
		return this.getAuthenticationManager().authenticate(authRequest);
	}
  1. ProviderManager

ProviderManager.authenticate()方法流程如下:
ProviderManager内部管理了多个AuthenticationProvider(接口)来实现灵活的认证机制。每个AuthenticationProvider都负责一种特定类型的认证。如下:

  • DaoAuthenticationProvider:用于表单登录认证。从数据库或其他持久化存储中加载用户信息并进行认证。
  • LdapAuthenticationProvider:用于 LDAP 认证。
  • OAuth2AuthenticationProvider:用于 OAuth2 认证

因为AuthenticationProvider是一个接口,所以具体调用的是实现类来进行认证。
这里使用的是DaoAuthenticationProvider实现类。但是因为DaoAuthenticationProvider实现类没有

ProviderManager.authenticate源码解析:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // 获取待测试的认证对象的类类型
    Class<? extends Authentication> 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 instanceof CredentialsContainer)) {
            ((CredentialsContainer) result).eraseCredentials();
        }
        
        // 如果父级认证管理器未发布成功事件,则发布认证成功事件
        if (parentResult == null) {
            this.eventPublisher.publishAuthenticationSuccess(result);
        }

        return result;
    }

    // 如果没有任何提供者认证成功
    if (lastException == null) {
        // 创建并记录提供者未找到异常
        lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
                new Object[] { 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()源码:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // 确认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;
            }
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
        // 确保retrieveUser返回的用户信息不为null
        Assert.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对象
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

retrieveUser方法是抽象方法,所以是在DaoAuthenticationProvider实现类中实现的。
具体作用是调用UserDetailsService.loadUserByUsername()方法,获取真实的用户名、密码
DaoAuthenticationProvider.retrieveUser源码说明:

@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {
    // 准备防御时间攻击的保护措施
    prepareTimingAttackProtection();
    try {
        // 调用UserDetailsService的loadUserByUsername方法加载用户信息
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        // 如果返回的用户信息为null,则抛出InternalAuthenticationServiceException异常
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                    "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) {
        // 捕获其他异常,并抛出InternalAuthenticationServiceException
        throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
    }
}

该方法主要是调用UserDetailsService接口的loadUserByUsername方法加载用户信息(获取数据库/自定义的用户名、密码),同时返回用户信息,交给authenticate方法验证,验证当前登录用户的信息(用户名、密码)和加载的(数据库)信息(用户名、密码)是否一致。

密码比对也是在DaoAuthenticationProvider.authenticate方法中。上面源代码可以看到,调用了additionalAuthenticationChecks抽象方法,由DaoAuthenticationProvider实现。
DaoAuthenticationProvider.additionalAuthenticationChecks()源码说明:

@Override
	@SuppressWarnings("deprecation")
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
			// 判断登录用户的密码是否为空
		if (authentication.getCredentials() == null) {
			this.logger.debug("Failed to authenticate since no credentials provided");
			throw new BadCredentialsException(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");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}

authenticate方法中 密码比对成功后,会将用户信息放入缓存中,其中包括了用户的权限信息。方便后期鉴权。

UserDetailsService

UserDetailsService是一个接口,所以获取真实的用户名、密码需要实现类提供。
SpringSecurity在项目开始时,是有给默认的用户名、密码的,存储在InMemoryUserDetailsManager实现的users属性中。
所以这里会调用InMemoryUserDetailsManager实现类的loadUserByUsername方法。从而获取真实的用户名和密码。
InMemoryUserDetailsManager源码说明:

public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
// 默认的用户名、密码
private final Map<String, MutableUserDetails> users = new HashMap<>();

@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {		
		// 根据登录用户名获取真实的用户名、密码
		UserDetails user = this.users.get(username.toLowerCase());
		// 为空则抛出异常
		if (user == null) {
			throw new UsernameNotFoundException(username);
		}

		// 将真实的用户名、密码、权限封装到UserDetails对象中。
		return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
				user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
	}
}

这样DaoAuthenticationProvider.authenticate方法就拿到了真实的用户名和密码。

但是通常我们的真实用户名、密码是数据库提供的,而不是使用默认的。
所以这里我们可以自己实现UserDetailsService接口,提供用户信息。

自定义提供真实用户信息

配置信息如下:
自己实现UserDetailsService接口;

@Service
public class MyUserDetailsService implements UserDetailsService {
	@Autowrite
	private UserMapper usermapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 这里你可以从数据库或其他数据源加载用户信息
        User user = usermapper.getByUsername(username);
        if(user == null){
        	throw new UsernameNotFoundException("用户不存在")
		}
		// 获取用户权限
		String authority = userService.getUserAuthorityInfo(userName);  // ROLE_admin,ROLE_normal,sys:user:list,....
		
		// 返回真实信息
		return new AccountUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), getUserAuthority(username));
    }
}

需要将自定义实现类,注入到AuthenticationManagerBuilder中,这样SpringSecurity在认证时就知道使用这个实现类获取真实用户信息了。

java配置



@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin().permitAll()
                .and()
                .logout().permitAll();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

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类就会调用我们自定义的认证实现类。

在这里插入图片描述

自定义实现类:


@Component
public class MyAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Autowired
    BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        //获取用户输入的用户名和密码
        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();
            throw new BadCredentialsException("用户名或密码错误!");
        }

        //获取封装用户
        UserDetails user = myUserDetailsService.loadUserByUsername(username);

        //进行密码比对
        if (bCryptPasswordEncoder.matches(password,user.getPassword())){
            //验证成功
         return new UsernamePasswordAuthenticationToken(username,user.getPassword(),user.getAuthorities());
        }
        throw new BadCredentialsException("用户名或密码错误!");
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }

这里需要注意的是:
myUserDetailsService是之前我们自己实现的自定义实现的提供真实用户信息类
而自定义实现的提供真实用户信息类,它实现了UserDetailsService接口。是DaoAuthenticationProvider实现类的一个流程。这是源码自己实现的一个流程。如下
在这里插入图片描述
那在自定义认证实现类中,我也可以自定义的流程, 那我就直接与数据库交互。不依靠UserDetailsService接口。也就是说我们可以直接注入xxMapper接口,获取真实的用户信息。

配置适配器类。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyAuthenticationProvider myAuthenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 注册自定义的认证实现类 AuthenticationProvider
        auth.authenticationProvider(myAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin().permitAll()
            .and()
            .logout().permitAll();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

注意:**configure(AuthenticationManagerBuilder auth)**方法,只注入了自定义认证实现类,而自定义实现的提供真实用户信息类并没有注入到AuthenticationManagerBuilder中,刚才也说到了,自定义认证实现类都可以直接与数据库交互拿到真实信息了,那还注入 自定义实现的提供真实用户信息类 有啥用。

参考文章:SpringSecurity认证流程(超级详细)

这里需要注意的是;在SpringSecurity认证流程中,
AbstractUserDetailsAuthenticationProvider.authenticate()方法中,如果认证成功了就会将用户放入缓存中。
同时会将用户信息放入HttpSession中,避免重复认证。

部分源码如下:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  
  	...  
    // 标志是否使用了缓存
    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源码说明:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, 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);
            
            // 创建一个空的 SecurityContext
            SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
            // 将认证结果放入 SecurityContext
            context.setAuthentication(authResult);
            // 将 SecurityContext 放入 SecurityContextHolder
            this.securityContextHolderStrategy.setContext(context);
            
            if (this.logger.isDebugEnabled()) {
                this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
            }
            
            // 调用 RememberMeServices 登录成功的方法
            this.rememberMeServices.loginSuccess(request, response, authResult);
            // 保存 SecurityContext
            this.securityContextRepository.saveContext(context, request, response);
            // 处理成功认证后的逻辑
            onSuccessfulAuthentication(request, response, authResult);
        }
    }
    catch (AuthenticationException ex) {
        // 认证失败,清空 SecurityContext
        this.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实现类:


@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private final RedisUtil redisUtil;
    private final AuthManager authManager;

    @Autowired
    public JwtAuthenticationTokenFilter(RedisUtil redisUtil, AuthManager authManager) {
        this.redisUtil = redisUtil;
        this.authManager = authManager;
    }


    @Override
    protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
        String authorizationInfo = request.getHeader("Authorization");
        if (ObjectUtil.isEmpty(authorizationInfo)) {
            filterChain.doFilter(request, response);
            return;
        }
        String token = authorizationInfo.substring("Bearer".length()); // Bearer token
        String sysUserId;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            sysUserId = claims.getSubject();
        } catch (Exception e) {
            throw new RuntimeException("非法token");
        }

        // 从redis获取用户信息
        String redisKey = authManager.getRedisUserKey(sysUserId);
        Object accountUserObject = redisUtil.getCacheObject(redisKey);
        if(ObjectUtil.isNull(accountUserObject)){
            throw new RuntimeException("用户未登录");
        }
        AccountUser accountUser = JSON.parseObject(accountUserObject.toString(), AccountUser.class);

        // 存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(accountUser,null,accountUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request,response);
    }
}


SpringSecurity适配器类相关配置


@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin().permitAll()
                .and()
                .logout().permitAll()
                .and()
                // 注入过滤器,执行顺序在UsernamePasswordAuthenticationFilter前面
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
              
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

2. 鉴权

如何在应用中拿到security上下文(用户信息)

之前的源码中,都能看到在认证成功后,都会将用户信息放入SpringSecurity上下文中。也就是SecurityContext,
而SecurityContextHolder是管理SecurityContext上下文的,所以我们可以通过它更新/获取用户信息。

// 更新上下文信息
SecurityContextHolder.getContext().setAuthentication(Authentication authentication);

// 获取当前用户的信息
SecurityContextHolder.getContext().getAuthentication();       
2.1 流程

在这里插入图片描述
从上图可以看到,认证成功之后就进入到我们的鉴权环节了,也就是进行了鉴权过滤器FilterSecurityInterceptor。

  1. FilterSecurityInterceptor类

首先根据源码看看它具体做了什么事情。

FilterSecurityInterceptor关键源码:

@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		invoke(new FilterInvocation(request, response, chain));
	}

可以看到是将 requestresponsechain都封装成了一个FilterInvocation对象。之后调用本类的invoke方法。

ServletRequest、ServletResponse大家都熟悉,但是这个FilterChain对象是什么?

FilterChain封装了当前整个过滤器链,鉴权过滤器可以在 doFilter 方法中执行鉴权逻辑。如果鉴权失败,它可以选择不调用 FilterChain.doFilter 方法,从而阻止请求继续传递到后续的过滤器或目标资源。

接下看看invoke方法做了什么事情?

public void invoke(FilterInvocation filterInvocation) throws IOException, 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. 执行拦截器之前的操作,可能会抛出 AccessDeniedException
    InterceptorStatusToken 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源码:

protected InterceptorStatusToken beforeInvocation(Object object) {
    // 确保传入的对象不为空
    Assert.notNull(object, "Object was null");
    
    // 检查传入的对象是否是安全对象
    if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
        throw new IllegalArgumentException("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(new PublicInvocationEvent(object));
        return null; // 不需要进一步的工作
    }
    
    // 检查当前安全上下文中是否存在认证对象
    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));
        }
        // 需要在调用后恢复到原始认证对象
        return new InterceptorStatusToken(origCtx, true, attributes, object);
    }
    
    this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");
    // 不需要进一步的工作
    return new InterceptorStatusToken(this.securityContextHolderStrategy.getContext(), false, attributes, object);
}

1、this.obtainSecurityMetadataSource().getAttributes(object);
根据request的url获取对应Controller的安全属性(ROLE_USER),不管是在controller接口上使用注解还是使用配置类配置的都能获取到

// 配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
 http
     .authorizeRequests()
     .antMatchers("/admin/**").hasRole("ADMIN")
     .antMatchers("/user/**").hasRole("USER")
     .anyRequest().authenticated();
}



 @GetMapping("/user/profile")
 @PreAuthorize("hasRole('USER')")
 public String userProfile() {
     return "User Profile";
 }

2、authenticateIfRequired(); 获取当前用户信息 返回Authentication

3、attemptAuthorization(object, attributes, authenticated); 开始鉴权,同时将目标接口的安全属性、用户信息、FilterInvocation注入到方法中。

AbstractSecurityInterceptor.attemptAuthorization方法源码:


private AccessDecisionManager accessDecisionManager;

private void attemptAuthorization(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));
			}
			else if (this.logger.isDebugEnabled()) {
				this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));
			}
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
			throw ex;
		}
	}

2.2 AccessDecisionManager

那这时我们就知道了,鉴权的真正处理者是:AccessDecisionManager接口
AccessDecisionManager接口源码:

public interface AccessDecisionManager {
 
    // 主要鉴权方法
    void decide(Authentication authentication, Object object,
                Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
            InsufficientAuthenticationException;
 
    boolean supports(ConfigAttribute attribute);
 
    boolean supports(Class<?> clazz);
}

那AccessDecisionManager既然是接口,肯定有实现类。看看接口的结构树
在这里插入图片描述

从图中我们可以看到它主要有三个实现类,分别代表了三种不同的鉴权逻辑:

  • AffirmativeBased:一票通过,只要有一票通过就算通过,默认是它。
  • UnanimousBased:一票反对,只要有一票反对就不能通过。
  • ConsensusBased:少数票服从多数票。

2.3 AffirmativeBased实现类

默认调用该类进行投票;
一票通过,只要有一票通过就算通过,默认是它。

public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
        throws AccessDeniedException {
    
    // 初始化拒绝计数器
    int deny = 0;

    // 遍历所有的决策投票者
    for (AccessDecisionVoter voter : getDecisionVoters()) {
        // 每个投票者对当前请求进行投票
        int result = voter.vote(authentication, object, configAttributes);
        
        // 根据投票结果执行相应的逻辑
        switch (result) {
            // 如果有投票者授予了访问权限,则立即返回,表示访问被授予
            case AccessDecisionVoter.ACCESS_GRANTED:
                return;
                
            // 如果有投票者拒绝了访问权限,则增加拒绝计数器
            case AccessDecisionVoter.ACCESS_DENIED:
                deny++;
                // 结束当前选择器(switch),进行下一个循环
                break;
                
            // 如果投票者弃权,则不进行任何操作
            default:
                break;
        }
    }

    // 如果有投票者拒绝了访问权限,抛出 AccessDeniedException 异常,表示访问被拒绝
    if (deny > 0) {
        throw new AccessDeniedException(
                this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
    }

    // 如果所有投票者都弃权,检查是否允许访问
    checkAllowIfAllAbstainDecisions();
}

getDecisionVoters是父类AbstractAccessDecisionManager抽象类的方法,投票器也是注入父类属性中的,如下:

public abstract class AbstractAccessDecisionManager
		implements AccessDecisionManager, InitializingBean, MessageSourceAware {
	private List<AccessDecisionVoter<?>> decisionVoters;

	public List<AccessDecisionVoter<?>> getDecisionVoters() {
		return this.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')")
public void deleteUser(Long userId) {
    // 只有具有 ROLE_ADMIN 角色的用户才能执行此方法
    userRepository.deleteById(userId);
}
@PostAuthorize("returnObject.owner == authentication.name")
public Document getDocument(Long documentId) {
    // 方法执行后,会检查返回的 Document 的 owner 是否与当前用户匹配
    return documentRepository.findById(documentId).orElse(null);
}

而扫描这两个注解的投票器PreInvocationAuthorizationAdviceVoter,并没有注入到decisionVoters属性中,那投票的时候岂不是直接拒绝了?那这种时候应该怎么办呢?

其实可以使用注解@EnableGlobalMethodSecurity



@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
public class MethodSecurityConfig extends WebSecurityConfigurerAdapter {
    // 这里可以进行额外的配置
}

  • prePostEnabled = true:将PreInvocationAuthorizationAdviceVoter投票器注入到decisionVoters属性中参与投票。解析@PreAuthorize和@PostAuthorize注解信息
  • jsr250Enabled = true:将Jsr250Voter注入到decisionVoters属性中参与投票:解析@RolesAllowed 注解信息。

这样后续投票时就会扫描对应投票器并参与投票!!!

需要注意的是:
我们在之前讲到FilterSecurityInterceptor.beforeInvocation方法中获取到@PreAuthorize注解信息;
如配置了@PreAuthorize,就会返回一个 PreInvocationAttribute实例,包含hasRole(‘ROLE_ADMIN’)的表达式

 @PreAuthorize("hasRole('ROLE_ADMIN')")
    public void adminMethod() {
        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投票器就会根据自身业务逻辑进行判断投票,如下:

@Override
	public int vote(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 abstain
			return ACCESS_ABSTAIN;
		}
		// 调用PreInvocationAuthorizationAdvice进行解析判断
		return this.preAdvice.before(authentication, method, preAttr) ? ACCESS_GRANTED : ACCESS_DENIED;
	}

	private PreInvocationAttribute findPreInvocationAttribute(Collection<ConfigAttribute> config) {
		for (ConfigAttribute attribute : config) {
			if (attribute instanceof PreInvocationAttribute) {
				return (PreInvocationAttribute) attribute;
			}
		}
		return null;
	}

可以判断会先进行判断,判断之前通过 FilterSecurityInterceptor.beforeInvocation 拿到的安全属性实例,是否属于PreInvocationAttribute类。
后面调用PreInvocationAuthorizationAdvice进行解析判断

PreInvocationAuthorizationAdvice.before源码

@Override
public boolean before(Authentication authentication, MethodInvocation mi, PreInvocationAttribute attr) {
    // 将 PreInvocationAttribute 强制转换为 PreInvocationExpressionAttribute
    PreInvocationExpressionAttribute 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)源码

public static boolean evaluateAsBoolean(Expression expr, EvaluationContext ctx) {
		try {
			return expr.getValue(ctx, Boolean.class);
		}
		catch (EvaluationException ex) {
			throw new IllegalArgumentException("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动态鉴权流程解析

  • 31
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Security是一个基于Spring框架的安全框架,它提供了一套完整的安全认证和授权机制。下面是Spring Security认证流程: 1. 用户访问需要认证的资源,Spring Security会拦截请求并重定向到登录页面。 2. 用户输入用户名和密码,提交表单。 3. Spring Security会将表单提交的用户名和密码封装成一个Authentication对象。 4. AuthenticationManager接口会根据Authentication对象中的用户名和密码去调用UserDetailsService接口的实现类获取用户信息。 5. 如果获取到用户信息,则将用户信息封装成一个包含权限信息的Authentication对象返回给AuthenticationManager。 6. AuthenticationManager会将Authentication对象交给AuthenticationProvider接口的实现类进行认证。 7. 如果认证成功,则将认证成功的Authentication对象返回给Spring Security。 8. Spring Security会将认证成功的Authentication对象存储到SecurityContextHolder中,供后续的访问授权使用。 下面是一个简单的Spring Security认证流程的代码示例: ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").hasAnyRole("ADMIN", "USER") .anyRequest().authenticated() .and() .formLogin() .and() .logout().logoutSuccessUrl("/login").permitAll(); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值