spring security用法,spring security 接口权限控制

  spring security用法,spring security 接口权限控制

  00-1010前言1。SpEL2。如何编写自定义权限3?权限通配符4。天琴计划做了什么?

  00-1010小伙伴们都知道,在Shiro中,默认支持权限通配符。例如,系统用户拥有以下权限:

  系统:用户3360添加系统3360用户3360删除系统3360用户3360选择系统3360用户:更新…现在授权用户时,我们可以如上配置一个权限一个权限,或者直接使用通配符:

  系统:用户:*

  这个通配符意味着您拥有该用户的所有权限。

  今天就来说说春安怎么处理这个,也来看看天琴项目这一块怎么改进。

  00-1010要了解基于标注的权限管理,首先要了解SpEL,不需要了解太多。这里简单介绍一下。

  Spring表达式语言(简称SpEL)是一种功能强大的表达式语言,支持运行时对象导航图的查询和操作。它的语法类似于传统EL,但它提供了额外的功能,其中最好的是简单字符串的函数调用和模板函数。

  SpEL为Spring社区提供了一种简单高效的表达式语言,可以贯穿整个Spring产品组。这种语言的特点是基于Spring产品的需求而设计的,这是其外观的一大特点。

  虽然我们离不开Spring框架,但实际上我们离不开SpEL,因为它是如此的好用和强大,而且SpEL在整个Spring家族中也处于非常重要的地位。但是很多时候,我们对它只知道一个大概。其实如果你系统的学习过SpEL,上面Spring Security的注解其实很好理解。

  首先,让我给你一个简单的例子来平滑SpEL。

  为了省事,我将创建一个Spring Boot项目来向您演示。在创建它的时候,我不需要添加任何额外的依赖项,只需要最基本的依赖项。

  代码如下:

  String expressionStr= 1 2expression parser parser=new SpelExpressionParser();expression exp=parser . parse expression(expression str);ExpressionStr是我们定义的表达式字符串。这个字符串被ExpressionParser对象解析成一个表达式,然后就可以执行exp了。

  有两种执行方式。对于上面那个没有任何额外变量的,我们可以直接执行。直接执行的方式如下:

  object value=exp . getvalue();system . out . println(value . tostring());这个打印结果是3。

  记得之前群里有个小伙伴让我执行一个字符串表达式,我不知道该怎么做。js里有eval函数很方便,Java里我们也有SpEL,一样方便。

  但是很多时候我们要执行的表达式可能比较复杂,这个时候上面的调用方法是不够的。

  此时,我们可以为要调用的表达式设置一个上下文,然后我们将使用EvaluationContext或它的子类,如下所示:

  standardyevaluationcontext context=new standardyevaluationcontext();system . out . println(exp . getvalue(context));当然,上面的表达式不需要设置上下文。我举一个设定背景的例子。

  例如,我现在有一个用户类,如下所示:

  公共类用户{私有整数id;私有字符串用户名;私有字符串地址;//省略getter/setter}现在我的表达式是这样的:

  字符串表达式= # user.usernameexpression parser parser=new SpelExpressionParser();expression exp=parser . parse expression(表达式);standardyevaluationcontext CTX=new standardyevaluationcontext();用户用户=新用户

  er();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 方法,如下:

 

  

@AutowiredBeanFactory beanFactory;@Testvoid 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 仅仅只是人为划分出来的东西,底层的实现包括判断逻辑基本上都是没有区别的。

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

  

@RestControllerpublic 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 方法:

  

@Overridepublic boolean hasPermission(Object target, Object permission) { return this.permissionEvaluator.hasPermission(this.authentication, target, permission);}@Overridepublic 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 */@Overridepublic boolean hasPermission(Authentication authentication, Object target, Object permission) {return false;}/** * @return false always */@Overridepublic boolean hasPermission(Authentication authentication, Serializable targetId, String targetType,Object permission) {return false;}}

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

 

  所以,在 Spring Security 中,如果想判断权限,需要自己提供一个 PermissionEvaluator 的实例,我们来看下:

  

@Componentpublic 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 是哪个,调用的是哪个方法,都是不支持权限通配符的)。

 

  例如我现在这样描述我的用户权限:

  

@BeanUserDetailsService 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 如下:

  

@Componentpublic 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));}

这个判断逻辑很简单,就是获取到当前登录的用户,判断当前登录用户的权限集合中是否具备当前请求所需要的权限。具体的判断逻辑没啥好说的,就是看集合中是否存在某个字符串,从判断的逻辑中我们也可以看出来,这个权限也是不支持通配符的。

 

  到此这篇关于详解Spring Security如何在权限中使用通配符的文章就介绍到这了,更多相关Spring Security使用通配符内容请搜索盛行IT以前的文章或继续浏览下面的相关文章希望大家以后多多支持盛行IT!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: