Appearance
Spring Security 6 权限校验介绍
本文主要介绍Spring Security中的权限校验框架以及入门案例,本文是Spring Security系列文章第六篇,前五篇如下:
- Spring Security - 1 初认识
- Spring Security - 2 身份验证(Authentication)
- Spring Security - 3 身份验证之数据库
- Spring Security - 4 JWT
- Spring Security - 5 使用JWT登录
Spring Security中的权限校验分为两个级别:请求级别与方法级别。
1. 请求级别的权限校验
1.1 配置请求的权限
我们可以在Security过滤器链配置中增加特定接口的权限校验规则,例如:
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.csrf(csrfConfigurer -> csrfConfigurer.disable())
.formLogin(config -> config.disable())
.addFilterAt(new JwtAuthFilter(userDetailsService, requestMatcher()), UsernamePasswordAuthenticationFilter.class)
.sessionManagement(session->session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorizeHttpRequestsConfigurer ->
authorizeHttpRequestsConfigurer
.requestMatchers(requestMatcher()).permitAll()
.requestMatchers("/num").hasAuthority("NUM")
.requestMatchers("/hello").hasRole("USER")
.anyRequest().authenticated())
.build();
}- 第10行表示发起请求
/num时,用户必须要有NUM权限; - 第11行表示发起请求
/hello时,用户必须要有USER角色(其实角色就是权限的合集,在这里USER角色表示ROLE_USER权限);
1.2 问题:用户的权限哪里来?
还记得接口UserDetails吗,其中有一个方法:
java
Collection<? extends GrantedAuthority> getAuthorities();该方法返回权限集合。所以我们在实现该接口时,应该实现该方法以返回该用户的权限。
java
public class UserPricipal implements UserDetails {
private User user;
private List<String> authorityList;
public UserPricipal(User user, List<String> authorityList){
this.user = user;
this.authorityList = authorityList;
}
// 获取权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorityList == null || authorityList.size() == 0){
return Collections.emptyList();
}
return authorityList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
}在获取用户信息的方法中,应该同时获取用户权限:
java
@Service
public class DBUserDetailsService implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.getUserByUsername(username);
if(user == null){
throw new UsernameNotFoundException("User not found");
}
//TODO: 获取用户权限,此处用做测试
List<String> authorityList = new ArrayList<>(Arrays.asList("ROLE_USER"));
return new UserPricipal(user, authorityList);
}
}1.3 测试截图
/hello请求有权限:

/num请求没有权限:

1.4 请求权限校验流程
在Spring Security中,权限校验是由过滤器链中最后一个过滤器AuthorizationFilter完成的:

图片来源:https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html
- 当请求到达
AuthorizationFilter时,该过滤器会从SecurityContextHolder中获取用户信息Authentication; - 然后将获取到的
Authentication以及请求Request交给AuthorizationManager(具体实现是RequestMatcherDelegatingAuthorizationManager),执行authorize()方法; - 如果权限校验失败,则抛出
AcessDeniedException异常; - 如果权限校验成功,则放行请求,正常执行;
2. 方法级别的权限校验
2.1 开启配置
方法级别的权限校验默认不生效,需要我们手动开启,在Spring Security的配置类上增加注解@EnableMethodSecurity:
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
// ... 省略代码
}然后,我们就可以在组件中的方法上使用 PreAuthorize, PostAuthorize, PreFilter和 PostFilter注解来增加权限规则,例如:
java
@RestController
public class HelloController {
@GetMapping("/hello")
@PreAuthorize("hasRole('USER')")
public String hello(){
return "hello";
}
@GetMapping("/num")
@PreAuthorize("hasAuthority('NUM')")
public int num(){
return new Random().nextInt();
}
}hello()方法调用要求有USER角色(即ROLE_USER权限)num()方法调用要求有NUM权限
2.2 方法权限校验流程

图片来源:https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html
Spring AOP 为
readCustomer调用其代理方法。在代理的其他通知器中,它调用了一个与@PreAuthorize切点匹配的AuthorizationManagerBeforeMethodInterceptor。拦截器调用
PreAuthorizeAuthorizationManager#check。授权管理器使用
MethodSecurityExpressionHandler来解析注解的 SpEL 表达式,并从包含Supplier<Authentication>和MethodInvocation的MethodSecurityExpressionRoot构建相应的EvaluationContext(评估上下文)。拦截器使用此上下文评估表达式;具体来说,它从
Supplier中读取Authentication(认证信息),并检查其权限集合中是否具有指定权限。如果评估通过,则 Spring AOP 继续调用该方法。
如果未通过,则拦截器发布一个
AuthorizationDeniedEvent(授权拒绝事件),并抛出一个AccessDeniedException(访问拒绝异常),该异常会被ExceptionTranslationFilter捕获,并向响应返回 403 状态码。方法返回后,Spring AOP 调用一个与
@PostAuthorize切点匹配的AuthorizationManagerAfterMethodInterceptor,其操作方式与上述相同,但使用的是PostAuthorizeAuthorizationManager。如果评估通过,则处理正常继续。
如果未通过,则拦截器发布一个
AuthorizationDeniedEvent,并抛出一个AccessDeniedException,该异常会被ExceptionTranslationFilter捕获,并向响应返回 403 状态码。
可见对于方法权限的校验,Spring Security使用的是AOP和方法拦截器。
3. 权限校验规则
此处只说明方法级别的权限校验。
3.1 hasAuthority()
java
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public void adminMethod() {
// ...
}表示调用adminMethod方法需要有ROLE_ADMIN权限。
3.2 hasAnyAuthority()
java
@PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')")
public void userOrAdminMethod() {
// ...
}表示调用userOrAdminMethod方法需要有ROLE_ADMIN或ROLE_USER权限。
3.3 hasRole()
java
@PreAuthorize("hasRole('USER')")
public void userMethod(){
// ...
}表示调用userMethod方法需要有USER角色,即ROLE_USER权限。
3.4 hasAnyRole()
java
@PreAuthorize("hasAnyRole('USER','ADMIN')")
public void userOrAdminMethod(){
// ...
}表示调用userOrAdminMethod方法需要有USER角色(即ROLE_USER权限)或ADMIN角色(即ROLE_ADMIN权限)。
3.5 SpEL表达式
我们可以在@PreAuthorize中使用SpEL表达式,其中有两个变量可以使用:
authentication对象访问当前用户的认证信息,例如用户名、权限列表等;principal对象,即Authentication#getPrincipal,表示当前用户;
一些案例:
java
@PreAuthorize("authentication.name == 'admin'")
public void adminMethod() {
// ...
}使用SpEL表达式指定权限校验规则,表示登录用户名为admin才可以访问该方法;
java
@PreAuthorize("hasAuthority('ROLE_ADMIN') && !hasAuthority('ROLE_TEMP')")
public void adminNotTempMethod() {
// ...
}我们也可以在SpEL表达式中使用逻辑运算符&&、||和!。
java
@PreAuthorize("#id == authentication.principal.id or hasAuthority('ROLE_ADMIN')")
public void getResource(@PathVariable Long id) {
// ...
}也可以在SpEL中获取方法参数。
3.6 hasPermission()
自定义权限校验规则,需要与PermissionEvaluator接口配合使用。