SpringSecurity
是Spring提供的一个权限管理框架。提供多种身份验证机制(表单登录、HTTP Basic、JWT无状态身份验证),提供细粒度的权限验证机制。提供内置的安全防护机制,保证服务的安全,防止服务遭受恶意攻击。如CSRF(跨站请求伪造),内部使用CORS机制来解决此问题。
同时可以对用户的密码进行加强管理,对用户密码进行加密,同时这个加密是不可逆的。这样即使数据库泄露,也不会暴露用户密码。保护用户的身份信息。
SpringSecurity的核心业务其实就两个
- 认证: 验证当前用户是否为本系统用户
- 授权:经过认证后判断当前用户是否有权限进行某个操作
1. 认证
登录校验流程:
SpringSecurity底层其实就是一个过滤器链,内部提供了各种功能的过滤器,核心过滤器如下
- UsernamePasswordAuthtocationFilter:用户名密码认证过滤器。用于用户认证;
- FilterSecurityInterceptor:负责权限校验的过滤器
- ExceptionTranslationFilter:异常转换过滤器;在用户认证或者权限校验时出现异常,会捕获异常并进行相应的处理。
基于表单的认证流程:
- 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);
}
- 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;
}
- 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认证流程中,
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。
- FilterSecurityInterceptor类
首先根据源码看看它具体做了什么事情。
FilterSecurityInterceptor关键源码:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
invoke(new FilterInvocation(request, response, chain));
}
可以看到是将 request
、response
、chain
都封装成了一个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接口
它是一个接口,所以需要看看它的实现类。
- 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类
- 执行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投票器实现!!!
至此完结!!