【Spring Security】 基于表达式的访问控制

Metadata

title: 【Spring Security】 基于表达式的访问控制
date: 2023-02-02 14:15
tags:
  - 行动阶段/完成
  - 主题场景/组件
  - 笔记空间/KnowladgeSpace/ProgramSpace/ModuleSpace
  - 细化主题/Module/SpringSecurity
categories:
  - SpringSecurity
keywords:
  - SpringSecurity
description: 【Spring Security】 基于表达式的访问控制

【Spring Security】 基于表达式的访问控制

Spring Security 3.0 引入了使用 Spring EL 表达式作为授权机制的能力。

内置表达式

表达式 说明
hasRole(String role) 返回 true 当前主体是否具有指定的角色。例如, hasRole(‘admin’)默认情况下,如果提供的角色不以 “ROLE_” 开头,则会添加它。这可以通过修改 defaultRolePrefixon 来定制 DefaultWebSecurityExpressionHandler。
hasAnyRole(String…​ roles) 返回 true 当前主体是否具有任何提供的角色(以逗号分隔的字符串列表形式给出)。例如, hasAnyRole(‘admin’, ‘user’)
hasAuthority(String authority) 返回 true 当前主体是否具有指定的权限。例如, hasAuthority(‘read’)
hasAnyAuthority(String…​ authorities) 返回 true 当前主体是否具有任何提供的权限(以逗号分隔的字符串列表形式给出)例如, hasAnyAuthority(‘read’, ‘write’)
principal 允许直接访问代表当前用户的主体对象
authentication 允许直接访问 Authentication 从 SecurityContext
permitAll 总是评估为 tru
denyAll 总是评估为 false
isAnonymous() 如果当前主体是匿名用户,则返回 true
isRememberMe() true 如果当前主体是记得,我的用户, 则返回 true
isAuthenticated() 如果用户不是匿名的则返回 true
iisFullyAuthenticated() 如果用户不是匿名或记得,我的用户, 则返回 true
hasPermission(Object target, Object permission) 返回 true 用户是否有权访问给定权限的提供目标。例如,hasPermission(domainObject, ‘read’)
hasPermission(Object targetId, String targetType, Object permission) 返回 true 用户是否有权访问给定权限的提供目标。例如,hasPermission(1, ‘com.example.domain.Message’, ‘read’)

表达式注解

pringsecurity3.0 引入了一些新的注释,以便全面支持表达式的使用。

有四个注释支持表达式属性,以允许调用前和调用后授权检查,并支持过滤提交的集合参数或返回值。它们是 @PreAuthorize,@PreFilter,@PostAuthorize 和 @PostFilter。

相关注解

@EnableGlobalMethodSecurity

要开启注解授权,还需要开启 EnableGlobalMethodSecurity 注解。

EnableGlobalMethodSecurity 源码:

// 开启全局方法级别权限控制,类似于XML :global-method-security
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ GlobalMethodSecuritySelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableGlobalMethodSecurity {
    // 开启 @PreAuthorize 和 @PostAuthorize注解支持,默认false
    boolean prePostEnabled() default false;
    //  确定是否应启用 Spring Security 的 {@link @Secured}注解
    boolean securedEnabled() default false;
    // 确定是否应启用 JSR-250 注解。默认为false。
    boolean jsr250Enabled() default false;
    // 指示是否要创建基于子类 (CGLIB) 的代理,而不是基于标准 Java 接口的代理默认值为 {@code false}
    boolean proxyTargetClass() default false;
    // 建议模式
    AdviceMode mode() default AdviceMode.PROXY;
    // 指示在特定连接点应用多个建议时安全顾问的执行顺序
    int order() default Ordered.LOWEST_PRECEDENCE;
}

我们在 MyWebSecurityConfiguration 加上此注解:

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)

@Secured

@Secured 用于判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀 “ROLE_“。

我们在 controller 方法上添加 @Secured 注解,并添加多个角色值,表示当前登陆用户必须有其中一个角色,否则无法访问。

    @GetMapping("/test")
    @Secured({"ROLE_root","ROLE_manager"})
    public Object test() {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        MyUser principal = (MyUser)authentication.getPrincipal();
        return principal;
    }

@PreAuthorize 和 @PostAuthorize

@PreAuthorize 和 @PostAuthorize 权限注解,可以作用于方法或类,可以结合 EL 表达式进行访问控制,区别是 @PreAuthorize 是方法执行前,@PostAuthorize 是执行后,当表达式结果为 true 时,才能进入。

常用表达式示例:

// 表示有ROLE_ROOT角色才能访问
@PreAuthorize("hasRole('ROLE_ROOT')")
// 表示有ROLE_root或者ROLE_manager角色即可访问
@PreAuthorize("hasAnyRole('ROLE_root','ROLE_manager')")
// 表示有add:user这个权限值即可访问,不区分角色或者权限
@PreAuthorize("hasAuthority('add:user')")
// 有其中一个权限值即可访问
@PreAuthorize("hasAnyAuthority('add:user','user:update')")
// 表示只要登录都可以访问
@PreAuthorize("permitAll()")
// 拒绝所有访问
@PreAuthorize("denyAll()")

@PreFilter 和 @PostFilter

使用 @PreFilter 和 @PostFilter 可以对集合类型的参数或返回值进行过滤。

使用 @PostFilter 注解时,Spring Security 会遍历返回的集合或映射,并删除提供的表达式为 false 的任何元素。使用 @PreFilter,尽管这是一个不太常见的要求。语法是一样的,但是如果有多个参数是集合类型,那么您必须使用 filterTarget 此注释的属性按名称选择一个。

也就是可以过滤参数或者返回值,但是一般也不会这么做,效率比较低。

比如以下案例,会过滤掉返回值中 userName 不为 test 的元素。

@PostFilter(value = "filterObject.userName == 'test'")
    public Object test() {
        List<User> userList = userService.list();
        return userList;
    }

所以最后查询,虽然数据库查询了所有数据,但是只输出了 userName 为 test 的单条数据。