Skip to content

Spring Security 6 权限校验介绍

本文主要介绍Spring Security中的权限校验框架以及入门案例,本文是Spring Security系列文章第六篇,前五篇如下:

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请求有权限:

image-20250111150137570

/num请求没有权限:

image-20250111150225897

1.4 请求权限校验流程

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

authorizationfilter

图片来源:https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html

  1. 当请求到达AuthorizationFilter时,该过滤器会从SecurityContextHolder中获取用户信息Authentication
  2. 然后将获取到的Authentication以及请求Request交给AuthorizationManager(具体实现是RequestMatcherDelegatingAuthorizationManager),执行authorize()方法;
  3. 如果权限校验失败,则抛出AcessDeniedException异常;
  4. 如果权限校验成功,则放行请求,正常执行;

2. 方法级别的权限校验

2.1 开启配置

方法级别的权限校验默认不生效,需要我们手动开启,在Spring Security的配置类上增加注解@EnableMethodSecurity

java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
  // ... 省略代码
}

然后,我们就可以在组件中的方法上使用 PreAuthorize, PostAuthorize, PreFilterPostFilter注解来增加权限规则,例如:

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 方法权限校验流程

methodsecurity

图片来源:https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html

  1. Spring AOP 为 readCustomer 调用其代理方法。在代理的其他通知器中,它调用了一个与 @PreAuthorize 切点匹配的 AuthorizationManagerBeforeMethodInterceptor

  2. 拦截器调用 PreAuthorizeAuthorizationManager#check

  3. 授权管理器使用 MethodSecurityExpressionHandler 来解析注解的 SpEL 表达式,并从包含 Supplier<Authentication>MethodInvocationMethodSecurityExpressionRoot 构建相应的 EvaluationContext(评估上下文)。

  4. 拦截器使用此上下文评估表达式;具体来说,它从 Supplier 中读取 Authentication(认证信息),并检查其权限集合中是否具有指定权限。

  5. 如果评估通过,则 Spring AOP 继续调用该方法。

  6. 如果未通过,则拦截器发布一个 AuthorizationDeniedEvent(授权拒绝事件),并抛出一个 AccessDeniedException(访问拒绝异常),该异常会被 ExceptionTranslationFilter 捕获,并向响应返回 403 状态码。

  7. 方法返回后,Spring AOP 调用一个与 @PostAuthorize 切点匹配的 AuthorizationManagerAfterMethodInterceptor,其操作方式与上述相同,但使用的是 PostAuthorizeAuthorizationManager

  8. 如果评估通过,则处理正常继续。

  9. 如果未通过,则拦截器发布一个 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_ADMINROLE_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接口配合使用。