乐趣区

关于java:Spring-Security-中想在权限中使用通配符怎么做

小伙伴们晓得,在 Shiro 中,默认是反对权限通配符的,例如零碎用户有如下一些权限:

  • system:user:add
  • system:user:delete
  • system:user:select
  • system:user:update

当初给用户受权的时候,咱们能够像下面这样,一个权限一个权限的配置,也能够间接用通配符:

  • system:user:*

这个通配符就示意领有针对用户的所有权限。

当然这是 Shiro 里边的,对 Shiro 不相熟的小伙伴,能够在公众号后盾回复 shiro,查看松哥之前录的视频教程。

明天咱们来聊聊 Spring Security 中对此如何解决,也顺便来看看 TienChin 我的项目中,这块该如何改良。

1. SpEL

要搞明确基于注解的权限治理,那么得首先了解 SpEL,不须要理解多深刻,我这里就简略介绍下。

Spring Expression Language(简称 SpEL)是一个反对查问和操作运行时对象导航图性能的弱小的表达式语言。它的语法相似于传统 EL,但提供额定的性能,最出色的就是函数调用和简略字符串的模板函数。

SpEL 给 Spring 社区提供一种简略而高效的表达式语言,一种可贯通整个 Spring 产品组的语言。这种语言的个性基于 Spring 产品的需要而设计,这是它呈现的一大特色。

在咱们离不开 Spring 框架的同时,其实咱们也曾经离不开 SpEL 了,因为它太好用、太强大了,SpEL 在整个 Spring 家族中也处于一个十分重要的地位。然而很多时候,咱们对它的只理解一个大略,其实如果你零碎的学习过 SpEL,那么下面 Spring Security 那个注解其实很好了解。

我先通过一个简略的例子来和大家捋一捋 SpEL。

为了省事,我就创立一个 Spring Boot 工程来和大家演示,创立的时候不必加任何额定的依赖,就最最根底的依赖即可。

代码如下:

String expressionStr = "1 + 2";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expressionStr);

expressionStr 是咱们自定义的一个表达式字符串,这个字符串通过一个 ExpressionParser 对象将之解析为一个 Expression,接下来就能够执行这个 exp 了。

执行的时候有两种形式,对于咱们下面这种不带任何额定变量的,咱们能够间接执行,间接执行的形式如下:

Object value = exp.getValue();
System.out.println(value.toString());

这个打印后果为 3。

我记得之前有个小伙伴在群里问想执行一个字符串表达式,然而不晓得怎么办,js 中有 eval 函数很不便,咱们 Java 中也有 SpEL,一样也很不便。

不过很多时候,咱们要执行的表达式可能比较复杂,这时候下面这种调用形式就不太够用了。

此时咱们能够为要调用的表达式设置一个上下文环境,这个时候就会用到 EvaluationContext 或者它的子类,如下:

StandardEvaluationContext context = new StandardEvaluationContext();
System.out.println(exp.getValue(context));

当然下面这个表达式不须要设置上下文环境,我举一个须要设置上下文环境的例子。

例如我当初有一个 User 类,如下:

public class User {
    private Integer id;
    private String username;
    private String address;
    // 省略 getter/setter
}

当初我的表达式是这样:

String expression = "#user.username";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
StandardEvaluationContext ctx = new StandardEvaluationContext();
User user = new User();
user.setAddress("广州");
user.setUsername("javaboy");
user.setId(99);
ctx.setVariable("user", user);
String value = exp.getValue(ctx, String.class);
System.out.println("value =" + value);

这个表达式就示意获取 user 对象的 username 属性。未来创立一个 user 对象,放到 StandardEvaluationContext 中,并基于此对象执行表达式,就能够打印进去想要的后果。

如果咱们将 user 对象设置为 rootObject,那么表达式中就不须要 user 了,如下:

String expression = "username";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
StandardEvaluationContext ctx = new StandardEvaluationContext();
User user = new User();
user.setAddress("广州");
user.setUsername("javaboy");
user.setId(99);
ctx.setRootObject(user);
String value = exp.getValue(ctx, String.class);
System.out.println("value =" + value);

表达式就一个 username 字符串,未来执行的时候,会主动从 user 中找到 username 的值并返回。

当然表达式也能够是办法,例如我在 User 类中增加如下两个办法:

public String sayHello(Integer age) {return "hello" + username + ";age=" + age;}
public String sayHello() {return "hello" + username;}

咱们就能够通过表达式调用这两个办法,如下:

调用有参的 sayHello:

String expression = "sayHello(99)";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
StandardEvaluationContext ctx = new StandardEvaluationContext();
User user = new User();
user.setAddress("广州");
user.setUsername("javaboy");
user.setId(99);
ctx.setRootObject(user);
String value = exp.getValue(ctx, String.class);
System.out.println("value =" + value);

就间接写办法名而后执行就行了。

调用无参的 sayHello:

String expression = "sayHello";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
StandardEvaluationContext ctx = new StandardEvaluationContext();
User user = new User();
user.setAddress("广州");
user.setUsername("javaboy");
user.setId(99);
ctx.setRootObject(user);
String value = exp.getValue(ctx, String.class);
System.out.println("value =" + value);

这些就都好懂了。

甚至,咱们的表达式也能够波及到 Spring 中的一个 Bean,例如咱们向 Spring 中注册如下 Bean:

@Service("us")
public class UserService {public String sayHello(String name) {return "hello" + name;}
}

而后通过 SpEL 表达式来调用这个名为 us 的 bean 中的 sayHello 办法,如下:

@Autowired
BeanFactory beanFactory;
@Test
void contextLoads() {String expression = "@us.sayHello('javaboy')";
    ExpressionParser parser = new SpelExpressionParser();
    Expression exp = parser.parseExpression(expression);
    StandardEvaluationContext ctx = new StandardEvaluationContext();
    ctx.setBeanResolver(new BeanFactoryResolver(beanFactory));
    String value = exp.getValue(ctx, String.class);
    System.out.println("value =" + value);
}

给配置的上下文环境设置一个 bean 解析器,这个 bean 解析器会主动跟进名字从 Spring 容器中找打响应的 bean 并执行对应的办法。

当然,对于 SpEL 的玩法还有很多,我就不一一列举了。这里次要是想让小伙伴们晓得,有这么个技术,不便大家了解 @PreAuthorize 注解的原理。

总结一下:

  1. 在应用 SpEL 的时候,如果表达式间接写的就是办法名,那是因为在构建 SpEL 上下文的时候,曾经设置了 RootObject 了,咱们所调用的办法,实际上就是 RootObject 对象中的办法。
  2. 在应用 SpEL 对象的时候,如果像调用非 RootObject 对象中的办法,那么表达式须要加上 @对象名 作为前缀,例如后面案例的 @us

2. 自定义权限该如何写

那么自定义权限到底该如何写呢?首先咱们来看下在 Spring Security 中,不波及到通配符的权限该怎么解决。

松哥举一个简略的例子,咱们创立一个 Spring Boot 工程,引入 Web 和 Security 依赖,为了不便,这里的用户我间接创立在内存中,配置如下:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Bean
    UserDetailsService userDetailsService() {InMemoryUserDetailsManager m = new InMemoryUserDetailsManager();
        m.createUser(User.withUsername("javaboy").password("{noop}123").authorities("system:user:add","system:user:delete").build());
        return m;
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.csrf().disable();
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                .permitAll();
        return http.build();}

}

都是惯例配置,没啥好说的。留神后面的注解,开启基于注解的权限管制。

这里我多啰嗦一句,大家看创立用户的时候,调用的是 authorities 办法去设置权限的,这个跟 roles 办法其实没啥大的区别,调用 roles 办法会主动为你设置的字符串增加一个 ROLE_ 前缀,其余的其实都一样。在 Spring Security 中,role 和 permission 仅仅只是人为划分进去的货色,底层的实现包含判断逻辑基本上都是没有区别的。

接下来咱们定义四个测试接口,如下:

@RestController
public class UserController {@GetMapping("/add")
    @PreAuthorize("hasPermission('/add','system:user:add')")
    public String addUser() {return "add";}
    @GetMapping("/delete")
    @PreAuthorize("hasPermission('/delete','system:user:delete')")
    public String deleteUser() {return "delete";}
    @GetMapping("/update")
    @PreAuthorize("hasPermission('/update','system:user:update')")
    public String updateUser() {return "update";}
    @GetMapping("/select")
    @PreAuthorize("hasPermission('/select','system:user:select')")
    public String selectUser() {return "select";}
}

接口拜访都须要不同的权限。

此时如果大家启动我的项目去此时,零碎会提醒你四个接口通通都不具备权限,这是啥起因呢?咱们来持续剖析。

小伙伴们看这里,调用的时候 @PreAuthorize 注解中执行写办法名,不必写对象名,阐明调用的办法是 RootObject 中的办法,这里的 RootObject 实际上就是 SecurityExpressionRoot,咱们来看看这个对象中的 hasPermission 办法:

@Override
public boolean hasPermission(Object target, Object permission) {return this.permissionEvaluator.hasPermission(this.authentication, target, permission);
}
@Override
public boolean hasPermission(Object targetId, String targetType, Object permission) {return this.permissionEvaluator.hasPermission(this.authentication, (Serializable) targetId, targetType,
            permission);
}

最终的调用又指向了 permissionEvaluator 对象。

在 Spring Security 中,permissionEvaluator 有一个对立的接口就是 PermissionEvaluator,然而这个接口只有一个实现类,就是 DenyAllPermissionEvaluator,看名字就晓得,这是回绝所有。

public class DenyAllPermissionEvaluator implements PermissionEvaluator {private final Log logger = LogFactory.getLog(getClass());

    /**
     * @return false always
     */
    @Override
    public boolean hasPermission(Authentication authentication, Object target, Object permission) {return false;}

    /**
     * @return false always
     */
    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType,
            Object permission) {return false;}

}

这两个办法里啥都没干,间接返回了 false,这下就破案了!

所以,在 Spring Security 中,如果想判断权限,须要本人提供一个 PermissionEvaluator 的实例,咱们来看下:

@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        for (GrantedAuthority authority : authorities) {if (authority.getAuthority().equals(permission)) {return true;}
        }
        return false;
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {return false;}
}

我这里的判断逻辑比较简单,所以只须要实现第一个办法就行了,这个办法三个参数,第一个参数就是以后登录胜利的用户对象,前面两个参数则是咱们在 @PreAuthorize("hasPermission('/select','system:user:select')") 注解中的两个参数,当初该有的货色都有了,咱们只须要判断须要的权限以后用户是否有就行了。

这个自定义的权限评估器写好之后,注册到 Spring 容器就行了,其余什么事件都不必做。

接下来咱们就能够对方才的四个接口进行测试了,测试过程我就不演示了,小伙伴们自行用 postman 测试就行了。

3. 权限通配符

看明确了下面的逻辑,当初不必我说,大家也晓得权限通配符在 Spring Security 中是不反对的(无论你在 @PreAuthorize 注解中写的 SpEL 是哪个,调用的是哪个办法,都是不反对权限通配符的)。

例如我当初这样形容我的用户权限:

@Bean
UserDetailsService userDetailsService() {InMemoryUserDetailsManager m = new InMemoryUserDetailsManager();
    m.createUser(User.withUsername("javaboy").password("{noop}123").authorities("system:user:*").build());
    return m;
}

我想用 system:user:* 字符串示意 javaboy 具备针对用户的所有权限。

间接这样写必定是不行的,最终字符串比拟肯定是不会通过的。

那么怎么办呢?用正则仿佛也不太行,因为 * 在正则中不代表所有字符,如果拆解字符串去比拟,性能尽管也行得通,然而比拟麻烦。

想来想去,想到一个方法,不晓得小伙伴们是否还记得咱们之前在 vhr 中用过的 AntPathMatcher,用这个不就行了!

批改后的 CustomPermissionEvaluator 如下:

@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        for (GrantedAuthority authority : authorities) {if (antPathMatcher.match(authority.getAuthority(), (String) permission)) {return true;}
        }
        return false;
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {return false;}
}

批改之后,当初只有用户具备 system:user:* 权限,就四个接口都能拜访了。

4. TienChin 我的项目怎么做的?

TienChin 我的项目用的是 RuoYi-Vue 脚手架,咱们来看下这个脚手架的实现形式:

@PreAuthorize("@ss.hasPermi('tienchin:channel:query')")
@GetMapping("/list")
public TableDataInfo getChannelList() {startPage();
    List<Channel> list = channelService.list();
    return getDataTable(list);
}

看了后面的解说,当初 @ss.hasPermi('tienchin:channel:query') 应该很好懂了:

  • ss 是一个注册在 Spring 容器中的 bean,对应的类位于 org.javaboy.tienchin.framework.web.service.PermissionService 中。
  • 很显著,hasPermi 就是这个类中的办法。

这个 hasPermi 办法的逻辑其实很简略:

public boolean hasPermi(String permission) {if (StringUtils.isEmpty(permission)) {return false;}
    LoginUser loginUser = SecurityUtils.getLoginUser();
    if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {return false;}
    return hasPermissions(loginUser.getPermissions(), permission);
}
private boolean hasPermissions(Set<String> permissions, String permission) {return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}

这个判断逻辑很简略,就是获取到以后登录的用户,判断以后登录用户的权限汇合中是否具备以后申请所须要的权限。具体的判断逻辑没啥好说的,就是看汇合中是否存在某个字符串,从判断的逻辑中咱们也能够看进去,这个权限也是不反对通配符的。

不过我还是感觉官网的计划更好一些,接下来在视频中,我会率领小伙伴们对 RuoYi-Vue 脚手架进行一个小革新,把这个依照 Spring Security 官网的思路来定制一下,这个咱们视频中见,对视频感兴趣的小伙伴,戳戳戳这里:TienChin 我的项目配套视频来啦。

退出移动版