Spring Security 优化鉴权注解,使用自定义鉴权注解替代@PreAuthorize

引言

众所周知,在SpringSecurity的鉴权体系中,最常用的就是使用 @PreAuthorize 进行鉴权:

java 复制代码
@Service
public class MyCustomerService {
    @PreAuthorize("hasAuthority('permission:read')")
    @PostAuthorize("returnObject.owner == authentication.name")
    public Customer readCustomer(String id) { ... }
}

@PreAuthorize 注解本身使用SpEL表达式进行解析,其中hasAuthority 方式是位于#rootSpEL Context 中 rootObject 对象的方法 即SecurityExpressionRoot 继承自SecurityExpressionOperations接口的方法

尽管SpEL表达式相当灵活,可以调用Bean,可以调用静态方法,但是对于常见的判断当前用户有没有指定权限的写法而言相对啰嗦: 通常来说,权限字符串应该是静态定声明在常量类中,但如果要在SpEL表达式中引用,就需要

java 复制代码
@Service
public class MyCustomerService {
    @PreAuthorize("hasAnyAuthority('" + PermissionCode.PROJECT + "')")
    public Customer readCustomer(String id) { ... }
    //或者
    @PreAuthorize("hasAnyAuthority(T(com.xxx.constant.PermissionCode).PROJECT)")
    public Customer readCustomer2(String id) { ... }
}

特别是,如果需要引用多个权限字符串,编写起来相当麻烦

java 复制代码
@Service
public class MyCustomerService {
    @PreAuthorize("hasAnyAuthority('" + PermissionCode.DATA_FILLING + "','" + PermissionCode.ASSEMBLY + "')")
    public Customer readCustomer(String id) { ... }
}
    @PreAuthorize("hasAnyAuthority(T(com.xxx.constant.PermissionCode).DATA_FILLING,T(com.xxx.constant.PermissionCode).ASSEMBLY)")
    public Customer readCustomer2(String id) { ... }
}

可谓相当啰嗦且容易出错,而且这只是最基础的权限,如果包含租户/组织架构的权限判定,参数就会更加复杂,这就需要一个自定义的鉴权注解来替代@PreAuthorize 简化鉴权编码

自定义注解

java 复制代码
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@PreAuthorize("@authorizeCheck.checkAuthority(#root)")
public @interface Authorize {
    /**
     * 权限编码集合
     */
    String[] value();

    /**
     * 租户ID的SpEL表达式
     */
    @Language("SpEL") String tenantId() default "";

    /**
     * 多个权限校验码之间是否只要有一个满足即可还是必须全部满足
     */
    boolean anyMatch() default true;
}

其中最重要的就是@PreAuthorize("@authorizeCheck.checkAuthority(#root)")@PreAuthorize本身是具有元注解功能的,但是其所提供的的功能不够强大,我们不能通过@PreAuthorize在元注解模式下在被调用方法中提取到我们的自定义注解信息,我们需要拓展以达到AOP切面的效果,而不直接使用AOP的原因是,融入SpringSecurity的体系中可以帮助我们减少很多麻烦和处理工作

拓展 @PreAuthorize 的元注解能力

元注解@PreAuthorize("@authorizeCheck.checkAuthority(#root)")中,@authorizeCheck.checkAuthority 是调用名为authorizeCheck的Bean的checkAuthority方法,方法参数为#root对象。

但是默认的#root对象MethodSecurityExpressionRoot并不包含方法信息,target字段也只是方法所在类实例,即上述示例中的 MyCustomerService 对象

java 复制代码
package org.springframework.security.access.expression.method;
class MethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
    private Object filterObject;
    private Object returnObject;
    private Object target;
    //...
}
package org.springframework.security.access.expression;
public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {
	private final Supplier<Authentication> authentication;
	private AuthenticationTrustResolver trustResolver;
	private RoleHierarchy roleHierarchy;
	private Set<String> roles;
	private String defaultRolePrefix = "ROLE_";
	public final boolean permitAll = true;
	public final boolean denyAll = false;
	private PermissionEvaluator permissionEvaluator;
	public final String read = "read";
	public final String write = "write";
	public final String create = "create";
	public final String delete = "delete";
	public final String admin = "administration";
        //...
}

我们通过注册一个子类化的DefaultMethodSecurityExpressionHandler的来替换rootObject,使得其包含当前被鉴权的方法信息

我们先创建一个RootObject类

java 复制代码
@Getter
@Setter
public class RootObject extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
    private Object filterObject;
    private Object returnObject;
    private Object target;
    private MethodInvocation methodInvocation;//重点对象,很可惜默认的root并不传递该对象
    
    public RootObject(Authentication authentication) {//SpringSecurity5写法
        super(authentication);
    }
    public RootObject(Supplier<Authentication> authentication) {//SpringSecurity6写法
        super(authentication);
    }

    public void setThis(Object target) {
        this.target = target;
    }

    @Override
    public Object getThis() {
        return this.target;
    }
}

然后是子类化DefaultMethodSecurityExpressionHandler,如下是SpringSecurity6中的写法,SpringSecurity6中新增了Supplier<Authentication> authentication参数形式的系列方法

java 复制代码
    /**
     * <a href="https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#subclass-defaultmethodsecurityexpressionhandler">春季安全/Servlet 应用程序/授权/方法安全性/子类化DefaultMethodSecurityExpressionHandler</a>
     */
   public class AuthorizeExpressionHandler extends DefaultMethodSecurityExpressionHandler {
        @Override
        public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication, MethodInvocation mi) {
            // 替换为自定义 ROOT OBJECT
            MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(createSecurityExpressionRoot(authentication, mi)
                    , AopUtils.getMostSpecificMethod(mi.getMethod(), AopProxyUtils.ultimateTargetClass(Objects.requireNonNull(mi.getThis()))), mi.getArguments(), getParameterNameDiscoverer());
            context.setBeanResolver(getBeanResolver());
            return context;
        }

        @Override
        protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, MethodInvocation invocation) {
            return createSecurityExpressionRoot(() -> authentication, invocation);
        }
        //创建自定义 ROOT OBJECT
        private MethodSecurityExpressionOperations createSecurityExpressionRoot(Supplier<Authentication> authentication,
                                                                                MethodInvocation invocation) {
            RootObject root = new RootObject(authentication);
            root.setMethodInvocation(invocation);
            root.setThis(invocation.getThis());
            root.setPermissionEvaluator(getPermissionEvaluator());
            root.setTrustResolver(getTrustResolver());
            root.setRoleHierarchy(getRoleHierarchy());
            root.setDefaultRolePrefix(getDefaultRolePrefix());
            return root;
        }
    }

然后将该Handler注册为Bean即可

java 复制代码
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(ApplicationContext context) {
    AuthorizeExpressionHandler expressionHandler = new AuthorizeExpressionHandler();
    expressionHandler.setApplicationContext(context);
    return expressionHandler;
}

这样一来,@PreAuthorize("@authorizeCheck.checkAuthority(#root)")#root对象就是携带方法信息的我们自定义的rootObject

自定义鉴权方法读取自定义注解

创建一个类用于执行校验逻辑的Service

java 复制代码
@Service("authorizeCheck") // Bean名称要和@PreAuthorize("@authorizeCheck.checkAuthority(#root)")中一样
@RequiredArgsConstructor
public class AuthorizeCheckSerivce {
    private final UserService userService;

    /**
     * 通过自定义注解{@link Authorize}判断权限<br>
     *
     * @param root SpEL 中的#root
     * @return 是否有权限
     */
    public boolean checkAuthority(RootObject root) {
        MethodInvocation methodInvocation = root.getMethodInvocation();
        if (methodInvocation == null) {
            return true;
        }
        Authorize annotation;
        if (methodInvocation.getMethod().isAnnotationPresent(Authorize.class)) {
            annotation = methodInvocation.getMethod().getAnnotation(Authorize.class);
        } else if (methodInvocation.getMethod().getDeclaringClass().isAnnotationPresent(Authorize.class)) {
            annotation = methodInvocation.getMethod().getDeclaringClass().getAnnotation(Authorize.class);
        } else {
            return true;
        }
        String name = root.getAuthentication().getName();
        UserEntity userEntity = userService.loadUserByUsername(name);//自己系统的用户对象
        if (userEntity == null) {
            return false;
        }
        List<String> authorities;
        // 自定义注解中如果有一些自己系统的特别参数,就在这里处理
        if (StringUtils.hasText(annotation.tenantId())) {
             // 使用 SpEL 表达式工具读取当前调用方法参数中的租户ID
            @Nullable Long tenantId = SpELUtil.getExpression(annotation.tenantId(), root.getMethodInvocation()).getValue(Long.class);
            // 提取对应租户权限
            authorities = userEntity.getAuthorities().stream()
                        .filter(e -> e instanceof TenantAuthority authority
                                     && authority.tenantId().equals(tenantId))
                        .map(Authority::getAuthority).distinct().toList();
        } else {
            authorities = userEntity.getAuthorities().stream().map(Authority::getAuthority).toList();
        }
        String[] needAuthorities = annotation.value();
        if (annotation.anyMatch()) {
            for (String needAuthority : needAuthorities) {
                if (authorities.contains(needAuthority)) {
                    return true;
                }
            }
            return false;
        }
        for (String needAuthority : needAuthorities) {
            if (!authorities.contains(needAuthority)) {
                return false;
            }
        }
        return true;
    }
}

使用自定义注解

这样我们可以使用灵活的方式使用自定义注解,而不是编写冗长的SpEL了

java 复制代码
@Service
public class MyCustomerService {
    @Authorize(PermissionCode.PROJECT)
    public Customer readCustomer(String id) { ... }

    @Authorize({PermissionCode.PROJECT, PermissionCode.DATA_FILLING})
    public Customer readCustomer2(String id) { ... }
    
    @Authorize(value = {PermissionCode.PROJECT,PermissionCode.DATA_FILLING}, anyMatch = false)
    public Customer readCustomer3(String id) { ... }
    
    @Authorize(value = PermissionCode.PROJECT,tenantId = "#p0")
    public Customer readCustomer4(Long tenantId, String id) { ... }
}
相关推荐
Estar.Lee2 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
2401_857610034 小时前
SpringBoot社团管理:安全与维护
spring boot·后端·安全
凌冰_5 小时前
IDEA2023 SpringBoot整合MyBatis(三)
spring boot·后端·mybatis
码农飞飞5 小时前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货5 小时前
Rust 的简介
开发语言·后端·rust
monkey_meng5 小时前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
Estar.Lee5 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
新知图书6 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
盛夏绽放7 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
Ares-Wang7 小时前
Asp.net Core Hosted Service(托管服务) Timer (定时任务)
后端·asp.net