一种垂直越权的解决方案

垂直越权是一种非常常见且非常严重的权限漏洞,具体表现就是,低权限的用户可以不受控制的访问高权限用户的资源。

其实业界有现成的权限框架可以解决这个问题,比如Shiro、SpringSecurity,但是框架一般都比较重,如果我们的系统对权限校验的要求比较简单,那么就可以考虑自己来实现一套防止垂直越权的体系。

在开始如下方案的介绍前,需要拥有Spring拦截器、自定义注解、JavaConfig的相关知识。

方案一、

基于资源限定的角色来进行垂直越权控制

我们先构建一个基础的RestDemo应用,采用Spring Boot构建,然后新建一个RestController类如下:

@RestController
public class AuthTest {

    @RolePermitted(roleList = {"admin","leader"})
    @GetMapping(value = "/getMoney")
    public String getMoney(){
        return "1000";
    }

    @RolePermitted(roleList = {"admin","leader","employee"})
    @GetMapping(value = "/storeMoney/{num}")
    public String storeMoney(@PathVariable("num") String num){
        return num + " money stored";
    }

}

然后定义一个自定义注解@RolePermitted,用来表示哪些角色可以访问当前这个资源。

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface RolePermitted {
    String[] roleList();
}

以上,我们已经完成了基本内容的开发,剩下的就是创建一个拦截器并进行配置的注册了。

@Slf4j
public class RoleBasedAuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod)handler;
        Method method = handlerMethod.getMethod();

        // 对标注了RolePermitted注解的方法访问进行权限校验
        if(method.isAnnotationPresent(RolePermitted.class)){
            checkRolePermission(method);
        }

        // 只有当权限校验通过,不抛出异常的时候才能通过
        return true;
    }

    private void checkRolePermission(Method method) throws Exception {
        // 获取当前方法允许访问的角色列表
        String[] roleList = method.getAnnotation(RolePermitted.class).roleList();

        // 模拟从数据库/redis/缓存中获取用户的实际角色是employee
        String currentUserRole = "employee";

        for (String role : roleList) {
            if(role.equals(currentUserRole)){
                log.info("{}权限校验通过, 允许访问的角色列表是{},当前用户角色是{}", method.getName(), roleList ,currentUserRole);
                return;
            }
        }

        log.warn("{}权限校验不通过, 允许访问的角色列表是{},当前用户角色是{}", method.getName(), roleList ,currentUserRole);
        throw new Exception("权限校验不通过");
    }
}
@Configuration
@EnableWebMvc
public class AuthConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RoleBasedAuthInterceptor());
    }
}

此时,我们启动应用后,对目前仅有的全部两个URL的访问就都会走我们的RoleBasedAuthInterceptor拦截器进行角色的垂直越权校验,因为此时我们模拟的登录用户角色是employee,所以当访问/storeMoney/{num}时没有问题,但是访问/getMoney时就抛出了异常。

这种方案的优点是,实现起来非常简单,便于后续的维护,我们只需要对新增的URL添加对应的允许访问角色即可。但是缺点也非常的明显:

  • 对于角色很多的系统,我们不得不在注解上加上很多允许访问的角色,会显得很冗长;
  • 如果后续需要新增一个角色,那么就需要找到所有该角色允许访问的URL,挨个增加角色信息,费时费力,还容易遗漏;
  • 万一在系统运行期间需要灵活调配不同角色允许访问的URL,几乎是不可能的,需要重新修改代码部署;

方案二、

基于资源和角色的配置关系来进行垂直越权控制

@RestController
public class AuthTest {

    @ResourceCode(resourceCode = "getMoney")
    @GetMapping(value = "/getMoney")
    public String getMoney() {
        return "1000";
    }

    @ResourceCode(resourceCode = "storeMoney")
    @GetMapping(value = "/storeMoney/{num}")
    public String storeMoney(@PathVariable("num") String num) {
        return num + " money stored";
    }

}
@Slf4j
public class ResourceCodeBasedAuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod)handler;
        Method method = handlerMethod.getMethod();

        // 对标注了ResourceCode注解的方法访问进行权限校验
        if(method.isAnnotationPresent(ResourceCode.class)){
            checkRolePermission(method);
        }

        // 只有当权限校验通过,不抛出异常的时候才能通过
        return true;
    }

    private void checkRolePermission(Method method) throws Exception {
        // 获取当前方法的资源Code
        String resourceCode = method.getAnnotation(ResourceCode.class).resourceCode();

        // 模拟从数据库/redis/缓存中获取用户的可访问资源code列表
        List<String> allowedResourceCodeList = new ArrayList<>();
        allowedResourceCodeList.add("storeMoney");
        allowedResourceCodeList.add("transferMoney");

        if(allowedResourceCodeList.contains(resourceCode)){
            log.info("{}权限校验通过, 当前用户的资源列表为{},当前资源的code为{}", method.getName(), allowedResourceCodeList ,resourceCode);
            return;
        }

        log.warn("{}权限校验不通过, 当前用户的资源列表为{},当前资源的code为{}", method.getName(), allowedResourceCodeList ,resourceCode);
        throw new Exception("权限校验不通过");
    }
}
@Configuration
@EnableWebMvc
public class AuthConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new ResourceCodeBasedAuthInterceptor());
    }
}
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ResourceCode {

    String resourceCode();

}

在这个例子中,我们每个URL的Rest方法定义一个resourceCode来唯一标识它,然后在拦截器中,先加载出当前登录者所允许的全部resourceCode,只要当前方法的resourceCode被包含在所允许的全部resourceCode中,那么就证明当前用户可以访问。

至于如何获取当前登录者的所有resourceCode,我们可以通过如下的表设计来完成:

  • auth_user,存储所有用户信息
    • user_id
    • user_name
  • auth_role,存储所有角色信息
    • role_id
    • role_name
  • auth_user_role_relation,存储用户和角色的关系,一个用户可以拥有多个角色(多条记录)
    • relation_id
    • uder_id
    • role_id
  • auth_resource_code,存储资源信息
    • resource_id
    • resource_code
  • auth_role_resource_code_relation,存储角色和资源之间的关系,一个角色可以拥有多个资源(多条记录)
    • relation_id
    • role_id
    • resource_id

我们只要执行如下的SQL语句就能得到当前登录者所有的resourceCode:

select rc.resource_code 
from auth_user u 
left join auth_user_role_relation urr on u.user_id = urr.user_id 
left join auth_role_resource_code_relation rrcr on urr.role_id = rrcr.role_id
left join auth_resource_code rc on rrcr.resource_id = rc.resource_id
where u.user_name = ''

这种方案的优势是:

  • 对于角色很多的系统,我们不需要在代码中标注;
  • 如果后续需要新增一个角色,那么只需要在auth_role、auth_user_role_relation、auth_role_resource_code_relation中添加记录即可,不用去修改代码,改完立马就能生效;
  • 可以很方便地在系统运行期间调配不同人员、不同角色的权限信息。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容