首页
学习
活动
专区
圈层
工具
发布
30 篇文章
1
我又发现 Spring Security 中一个小秘密!
2
聊一个 GitHub 上开源的 RBAC 权限管理系统,很6!
3
Spring Security 中最流行的权限管理模型!
4
一个案例演示 Spring Security 中粒度超细的权限控制!
5
Spring Security 中如何细化权限粒度?
6
Spring Security 中的 hasRole 和 hasAuthority 有区别吗?
7
Spring Security 权限管理的投票器与表决机制
8
Spring Security 中如何让上级拥有下级的所有权限?
9
什么是计时攻击?Spring Boot 中该如何防御?
10
一个诡异的登录问题
11
为什么你使用的 Spring Security OAuth 过期了?松哥来和大家捋一捋!
12
深入理解 WebSecurityConfigurerAdapter【源码篇】
13
Spring Security 初始化流程梳理
14
花式玩 Spring Security ,这样的用户定义方式你可能没见过!
15
深入理解 AuthenticationManagerBuilder 【源码篇】
16
深入理解 SecurityConfigurer 【源码篇】
17
深入理解 FilterChainProxy【源码篇】
18
在 Spring Security 中,我就想从子线程获取用户登录信息,怎么办?
19
Spring Security 可以同时对接多个用户表?
20
Spring Security 竟然可以同时存在多个过滤器链?
21
一文搞定 Spring Security 异常处理机制!
22
Spring Security 配置中的 and 到底该怎么理解?
23
Spring Security 多种加密方案共存,老破旧系统整合利器!
24
神奇!自己 new 出来的对象一样也可以被 Spring 容器管理!
25
Spring Security 中的四种权限控制方式
26
Spring Boot+CAS 默认登录页面太丑了,怎么办?
27
Spring Boot+CAS 单点登录,如何对接数据库?
28
Spring Boot 实现单点登录的第三种方案!
29
松哥手把手教你入门 Spring Boot + CAS 单点登录
30
来一个简单的,微服务项目中如何管理依赖版本号?

我又发现 Spring Security 中一个小秘密!

松哥原创的 Spring Boot 视频教程已经杀青,感兴趣的小伙伴戳这里-->Spring Boot+Vue+微人事视频教程


说来惭愧,Spring Security 系列前前后后写了 60 多篇文章了,竟然漏掉了如此重要的一块。

平时在公司项目中,都是能用则用,先把需求整出来,有的时候也不去深究它的原理,这无形中就给自己挖了大坑。

松哥这次就是,不过也因此又发现了 Spring Security 的一个用法,今天就和小伙伴们分享下。

1.缘起

事情是这样的,看过 vhr 项目(https://github.com/lenve/vhr)的小伙伴都知道 vhr 里边有一个动态权限管理功能,实现的思路就是重写了 FilterInvocationSecurityMetadataSource 以及决策管理器 AccessDecisionManager,代码就类似下面这样(小伙伴们可以在 GitHub 上查看完整代码):

代码语言:javascript
代码运行次数:0
复制
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                    object.setAccessDecisionManager(customUrlDecisionManager);
                    object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                    return object;
                }
            })
            .and()
            .formLogin()
            .permitAll()
            .and()
            .csrf().disable();
}

昨天我想再写一个类似的功能,本想着很简单,三下五除二就搞定,大家看下:

代码语言:javascript
代码运行次数:0
复制
http
        .authorizeRequests()
        .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
            @Override
            public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                object.setSecurityMetadataSource(customSecurityMetadataSource);
                object.setAccessDecisionManager(accessDecisionManager());
                return object;
            }
        })
        .and()
        .formLogin()
        .and()
        .csrf().disable();

小伙伴们能看出这两段代码的差别吗?

写完之后,启动项目,一启动就报错了!

我就有点懵。之前的 vhr 启动是没问题的,但是这次新的项目启动就有问题。

在 IDEA 中,通过 Ctrl+Shift+F 全局搜索,找到了异常抛出的位置:

如果 requestMap 变量为空,就会抛出异常。requestMap 就是我们在 configure 方法中配置的请求和权限的映射,不过在上面的案例中,我是想像 vhr 那样做动态权限管理,所以请求和角色的映射关系我是保存在数据库中,没有必要在代码中配置。

但是根据异常提示,我就先随便加一个映射,果然启动就不报错了:

代码语言:javascript
代码运行次数:0
复制
http
        .authorizeRequests()
        .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
            @Override
            public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                object.setSecurityMetadataSource(customSecurityMetadataSource);
                object.setAccessDecisionManager(accessDecisionManager());
                return object;
            }
        })
        .antMatchers("/hello").hasRole("admin")
        .and()
        .formLogin()
        .and()
        .csrf().disable();

现在让我有两个郁闷的地方:

  1. vhr 为什么可以启动而不报错
  2. 这里代码中多出来的一行映射明显是累赘,最好能够去掉

2.问题分析

第一个问题很好办,仔细对比最开始贴出来的代码就能找到端倪,vhr 中的代码多了一个 permitAll 方法,这就是一个表达式,有了该方法,就意味着里边多了一个映射关系,/login -> permitAll,这样 requestMap 就不会为空,所以启动时就不报错了。

按照这个思路,我去修改了自己的代码,去除冗余的映射,加上 permitAll,这次果然可以了。

问题虽然解决了,但是心里还是有点膈应。

为什么呢?因为用户-角色-资源的映射关系我都保存在数据库中了,权限数据库类似下面这样:

既然所有的权限管理都保存在数据库中了,再在代码中配置似乎就不太合适!但是不配置,项目启动又会出错,看来还是得从源码入手。

于是松哥花了点时间,把这里涉及到的相关源码仔细梳理了一遍。

3.源码梳理

首先大家知道,Spring Security 中的权限控制有两种不同的方式:

  1. 通过 URL 请求地址进行控制。
  2. 通过方法进行控制。

如果通过 URL 请求地址进行控制,负责控制类配置的是 AbstractInterceptUrlConfigurer,我们来看下它的子类:

可以看到它有两个子类:

  • ExpressionUrlAuthorizationConfigurer
  • UrlAuthorizationConfigurer

两个都可以处理基于 URL 请求地址的权限控制。不同的是,第一个 ExpressionUrlAuthorizationConfigurer 支持权限表达式,第二个不支持。

什么是权限表达式?其实大家都有用,只是可能没注意过这些概念,下图就是 Spring Security 中提供的内置通用权限表达式:

图片源自网络

ExpressionUrlAuthorizationConfigurer 支持权限表达式的原因是因为它使用的投票器是 WebExpressionVoter,这个投票器就是用来处理权限表达式的。

而 UrlAuthorizationConfigurer 不支持权限表达式,是因为它使用的投票器是 RoleVoter 和 AuthenticatedVoter,这两者可以用来处理角色或者权限,但是没法处理权限表达式。

关于投票器,小伙伴们可以参考松哥之前的文章:Spring Security 权限管理的投票器与表决机制

上面说的都是默认行为,我们也可以通过修改配置,让 UrlAuthorizationConfigurer 支持权限表达式,不过一般来说没必要这样做,如果需要支持权限表达式,直接用 ExpressionUrlAuthorizationConfigurer 即可。

当我们调用如下这行代码时:

代码语言:javascript
代码运行次数:0
复制
http.authorizeRequests()

实际上就是通过 ExpressionUrlAuthorizationConfigurer 去配置基于 URL 请求地址的权限控制,所以它是支持权限表达式的。例如下面这段大家再熟悉不过的代码:

代码语言:javascript
代码运行次数:0
复制
http.authorizeRequests()
        .antMatchers("/admin/**").hasRole("ADMIN")
        .antMatchers("/user/**").access("hasRole('USER')")

在 ExpressionUrlAuthorizationConfigurer 中创建 SecurityMetadataSource 时,就会检查映射关系,如果 requestMap 为空就会抛出异常:

代码语言:javascript
代码运行次数:0
复制
@Override
ExpressionBasedFilterInvocationSecurityMetadataSource createMetadataSource(
  H http) {
 LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = REGISTRY
   .createRequestMap();
 if (requestMap.isEmpty()) {
  throw new IllegalStateException(
    "At least one mapping is required (i.e. authorizeRequests().anyRequest().authenticated())");
 }
 return new ExpressionBasedFilterInvocationSecurityMetadataSource(requestMap,
   getExpressionHandler(http));
}

UrlAuthorizationConfigurer 中也有 createMetadataSource 方法,但是却是另外一套实现方案:

代码语言:javascript
代码运行次数:0
复制
@Override
FilterInvocationSecurityMetadataSource createMetadataSource(H http) {
 return new DefaultFilterInvocationSecurityMetadataSource(
   REGISTRY.createRequestMap());
}

UrlAuthorizationConfigurer 并不会检查 requestMap 是否为空,但是它会在 createRequestMap 方法中检查一下映射关系是否完整,例如下面这样:

代码语言:javascript
代码运行次数:0
复制
.antMatchers("/admin/**").access("ROLE_ADMIN")
.mvcMatchers("/user/**").access("ROLE_USER")
.antMatchers("/getinfo");

最后的 /getinfo 没有指定需要的权限,这种就是不完整,就会抛出异常。

现在大家应该大致明白 ExpressionUrlAuthorizationConfigurer 和 UrlAuthorizationConfigurer 的区别了吧。

4.问题解决

ExpressionUrlAuthorizationConfigurer 会要求至少配置一个映射关系,UrlAuthorizationConfigurer 则无此要求。

当我们想要动态配置权限拦截时,一般来说也不会使用权限表达式,数据库中保存的就是普通的权限或者角色,所以这个时候我们可以选择 UrlAuthorizationConfigurer 而不是 ExpressionUrlAuthorizationConfigurer。

反映到代码上,就是下面这样:

代码语言:javascript
代码运行次数:0
复制
@Override
protected void configure(HttpSecurity http) throws Exception {
    ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
    http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                    object.setSecurityMetadataSource(customSecurityMetadataSource);
                    return object;
                }
            });
    http.formLogin()
            .and()
            .csrf().disable();
}

这样看起来就更合理一些,不用额外配置一条映射关系。

今天这篇文章可能对没了解过动态权限控制的小伙伴来说略难,大家可以参考松哥的 vhr(https://github.com/lenve/vhr)项目去了解下动态权限控制。当然,也可以参考松哥的Spring Boot+Vue+微人事视频教程,里边也有讲动态权限控制问题。

下一篇
举报
领券