如何优雅的使用切面和注解实现权限验证

2,799 阅读3分钟

背景

权限验证在我们系统中是一个与业务逻辑无关但是又与业务息息相关的一个功能。
设想我们开发了一款为中小型企业定制的会员系统。这款系统可以为企业A、企业B等多种企业提供服务。数据库中的表结构往往是这样的(以下只是一个demo,实际情况中字段一定会更多、更复杂):

id memberCardCode userName card_status business
1 a564456578 zhangsan 0 business-a
2 b678688643 lisi 1 businsss-b
3 a775445667 wangwu 0 businsss-a
4 b943578978 zhaoliu 1 businsss-b
5 c657688799 sunqi 1 businsss-c

基于上表,我们删除id = 1的会员往往是这样操作的(假设是物理删除):
controller层:

@RestController("/member")
public class MemberController {
    @PostMapping("/delete")
    public void deleteById(int id) {
        // 此处省略删除代码
    }
}

最终在controller中调用的SQL语句是这样的:

delete from member where id = 1;

乍一看,就这样一条简单sql语句能有什么问题呢?其实越是简单的问题,越不能放过。
通过上表我们看到id = 1的会员信息是属于business-a的。所以理应是business-a的账号才能删除id = 1的会员信息。那此时如果business-b在删除会员的时候将参数id改为1,此时就会出现business-b删除了business-a的会员。此时business-a的心情是崩溃的。

所以权限验证是非常必要。那权限验证怎么做呢?

不太优雅:侵入业务代码的方案

在controller层加入逻辑判断:判断删除的id是否属于当前账号,如果属于则删除;否则直接返回。代码如下:

@RestController("/member")
public class MemberController {
    @PostMapping("/delete")
    public void deleteById(int id) {
        // 第一步:权限验证
        Integer id = selectByIdAndBusiness(id, "business-a");
        if (id == null) {
            // 说明id不属于business-a不能删除
            return;
        }
        // 第二步:调用删除逻辑
    }
}

上面的代码确实解决了越权的问题,但是会将一些业务无关的代码侵入到我们的业务逻辑,这样的实现逻辑不太优雅。那我们来选择一个更优雅的方式来完成权限验证。

优雅:注解 + 切面的方案

首先我们需要明确关注的字段信息:id和business。其中business可以在filter中存入一个ThreadLocal,所以我们只需要关注字段id即可。
第一步:创建自定义注解(作用于方法)

@Documented  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.METHOD)  
public @interface Auth {
    // 方法的参数名称,以防参数名称不是id,所以提供paramName
    String paramName() default "id";
}

第二步:在方法上使用注解

package com.demo.controller;
@RestController("/member")
public class MemberController {
    @PostMapping("/delete")
    @Auth(paramId = "deleteId")
    public void deleteById(int deleteId) {
        // 调用删除逻辑
    }
}

第三步:实现切面

// 通过注解可以看到,我们该方法切的是controller层带有Auth注解的方法
@Before(value = "execution(public * com.demo.controller..*.*(..))"
      + " && @annotation(auth)", argNames = "pjp, auth")
public void before4Auth(JoinPoint pjp, Auth auth) {
    // 1、通过ThreadLocal获取business
    String business = context.get();
    // 2、通过注解解析id
    // 2.1 获取参数值
    Object[] args = pjp.getArgs();
    // 2.2 获取参数名
    MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
    String[] parameterNames = methodSignature.getParameterNames();
    // 2.3 获取注解中paramName的下标
    int index = ArrayUtils.indexOf(parameterNames, auth.paramName());
    // 2.4 根据下标获取id对应的值
    int val = (int) args[index];
    // 2.5 鉴权逻辑
    Integer id = selectByIdAndBusiness(val, business);
    if (id == null) {
        // 抛异常提示越权
    }
    // 否则的正常执行下面的业务逻辑
}

通过这种方式,我们只需在controller层的方法加上@Auth注解即可。没有非业务代码的侵入,实现方式可算优雅。
如果您有更优雅的解决方案,欢迎提供思路。