【Spring Security】 自定义权限注解

Metadata

title: 【Spring Security】 自定义权限注解
date: 2023-02-02 22:35
tags:
  - 行动阶段/完成
  - 主题场景/组件
  - 笔记空间/KnowladgeSpace/ProgramSpace/ModuleSpace
  - 细化主题/Module/SpringSecurity
categories:
  - SpringSecurity
keywords:
  - SpringSecurity
description: 【Spring Security】 自定义权限注解

【Spring Security】 自定义权限注解

在之前的案例中,我们可以使用 Security 提供的几大注解,轻松实现访问控制。

@PreAuthorize("hasRole('ROLE_ROOT')")
@PreAuthorize("hasAuthority('add:user')")

但是只提供了固定的几个表达式,内置表达式如下图所示:

表达式 说明
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’)

以上表达式能满足大部分应用场景,但是某些特殊条件下,如何进行自定义扩展呢?

分析

AccessDecisionVoter

AccessDecisionVoter 是一个投票器,负责对授权决策进行表决。然后,最终由唱票者 AccessDecisionManager 统计所有的投票器表决后,来做最终的授权决策。

AccessDecisionVoter 有众多的实现类,对不同的权限配置进行投票,比如 RoleVoter、WebExpressionVoter 分别负责对角色、配置类中配置的授权表达式进行处理。

其中 PreInvocationAuthorizationAdviceVoter 用于对 @PreFilter 和 @PreAuthorize 进行投票处理。

SecurityExpressionRoot

SecurityExpressionRoot 是用 Security 表达式评估的根对象,也就是进行表达式计算的时候,调用的是这个对象中的方法。

决策流程

  1. 首先对配置类配置的访问策略进行投票,比如默认配置所有的访问都必须经过认证,会调用 WebExpressionVoter 对当前请求进行决策,如果已认证则放行。
http.authorizeRequests((requests) -> {
            ((AuthorizedUrl)requests.anyRequest()).authenticated();
        });
  1. 接着再次进入过滤器,对注解形式的授权表达式进行处理,这里有三个选民。

  1. 首先 PreInvocationAuthorizationAdviceVoter 进行投票,最终进入的是 ExpressionBasedPreInvocationAdvice 的 before 方法进行处理。
public boolean before(Authentication authentication, MethodInvocation mi, PreInvocationAttribute attr) {
        // 获取注解配置项,转为PreInvocationExpressionAttribute对象,[authorize: 'hasAuthority('add:user')', filter: 'null', filterTarget: 'null']
        PreInvocationExpressionAttribute preAttr = (PreInvocationExpressionAttribute) attr;
        // 使用表达式处理器,获取评估上下文
        EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, mi);
        // 获取@PreFilter注解上的表达式,此处为null
        Expression preFilter = preAttr.getFilterExpression();
        // 获取@PreAuthorize注解上的表达式,hasAuthority('add:user')
        Expression preAuthorize = preAttr.getAuthorizeExpression();
        if (preFilter != null) {
            Object filterTarget = findFilterTarget(preAttr.getFilterTarget(), ctx, mi);
            this.expressionHandler.filter(filterTarget, preFilter, ctx);
        }
        // 使用表达式,进行计算,获取返回投票结果
        return (preAuthorize != null) ? ExpressionUtils.evaluateAsBoolean(preAuthorize, ctx) : true;
    }
  1. PreInvocationAuthorizationAdviceVoter 投票为 1,直接认为投票通过,放行请求。

分析 ExpressionBasedPreInvocationAdvice

由上可知,security 对注解进行授权判断的主要选民为 PreInvocationAuthorizationAdviceVoter,当它不通过后,才会调用其他的选民进行投票。而它进行投票的逻辑在 ExpressionBasedPreInvocationAdvice 类中。

ExpressionBasedPreInvocationAdvice 就是基于方法表达式的授权处理的核心类了,它包含了一个 MethodSecurityExpressionHandler 处理器,用来创建 SpEL 评估上下文。(题外话:可以看到 Security 基本上每个类都会有接口,想到了设计模式的依赖倒置,面相接口编程。。。)

接下来我们看下这个处理器是怎么创建的,可以看到有一个默认的处理器,但是也提供了 set 方法,那是否可以通过 set 方法,这是自定义的处理器,来创建自定义的评估上下文,在上下文中,添加我们自己的表达式方法,这样就可以自定义表达式咯?

public class ExpressionBasedPreInvocationAdvice implements PreInvocationAuthorizationAdvice {
    // 默认处理器
    private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
    // 设置处理器
    public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
        this.expressionHandler = expressionHandler;
    }
}

案例演示

演示需求: 创建一个表达式 hasUser,表示只有哪些用户可以访问资源。

1. 创建根对象

在源码中,发现有一个 MethodSecurityExpressionRoot 类,从注释上看可知,这是一个扩展根对象,并添加自定义表达式的类。但是这个类,没有修饰符,只能当前包访问。那只能仿照这个类,添加我们自定义的根对象即可。

自定义扩展根对象

@Slf4j
public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {


    public CustomMethodSecurityExpressionRoot(Authentication authentication) {
        super(authentication);
    }

    private Object filterObject;

    private Object returnObject;

    private Object target;


    /**
     * 自定义表达式
     *
     * @param username 具有权限的用户账号
     * @return 投票结果
     */
    public boolean hasUser(String... username) {
        log.info("进入自定义表达式");
        String name = this.getAuthentication().getName();
        log.info("当前登陆用户:" + name);
        String[] names = username;
        // 循环表达式配置的值
        for (String nameStr : names) {
            if (name.equals(nameStr)) {
                return true;
            }
        }
        log.info("当前登陆用户:" + name + ",没有权限");
        return false;
    }


    @Override
    public void setFilterObject(Object filterObject) {
        this.filterObject = filterObject;
    }

    @Override
    public Object getFilterObject() {
        return this.filterObject;
    }

    @Override
    public void setReturnObject(Object returnObject) {
        this.returnObject = returnObject;
    }

    @Override
    public Object getReturnObject() {
        return this.returnObject;
    }
    void setThis(Object target) {
        this.target = target;
    }
    @Override
    public Object getThis() {
        return this.target;
    }
}

2. 创建处理器

创建自定义处理器,主要是重写创建根对象的方法。

public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {

    @Override
    protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, MethodInvocation invocation) {
        CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot(authentication);
        root.setThis(invocation.getThis());
        root.setPermissionEvaluator(getPermissionEvaluator());
        root.setTrustResolver(getTrustResolver());
        root.setRoleHierarchy(getRoleHierarchy());
        root.setDefaultRolePrefix(getDefaultRolePrefix());
        return root;
    }
}

3. 配置 GlobalMethodSecurityConfiguration

之前我们使用 @EnableGlobalMethodSecurity 开启全局方法安全,而这些全局方法级别的安全配置就在 GlobalMethodSecurityConfiguration 配置类中。

可以扩展这个类来自定义默认值,但必须确保在类上指定 @EnableGlobalMethodSecurity 注解,否则会 bean 冲突报错。

@Configuration
// 将EnableGlobalMethodSecurity注解移到这里
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    /**
     * 重写创建处理器方法
     *
     * @return ExpressionHandler
     */
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return new CustomMethodSecurityExpressionHandler();
    }
}

4. 测试

创建一个访问接口,@PreAuthorize 注解添加自定义表达式 hasUser,表示只有 user,bbb 用户能访问这个接口。

    @GetMapping("/403")
    @PreAuthorize("hasUser('user','bbb')")
    public String code403() {
        return "templates/403";
    }

使用 test 登录访问,发现没有权限。

使用 user 登录访问,权限校验通过。