最常见的鉴权实现是ACL(Access Control List)模型,它通过在资源上定义访问控制列表来决定哪些用户或组可以访问该资源。每个ACL列表都包含了一系列的访问条目,每个条目指定了特定用户或组的访问权限,如读取、写入、执行等。在实际开发中,RBAC(Role-Based Access Control)模型实际上可以理解为一种更高层次抽象的ACL实现,RBAC中使用角色的概念,在较大型的管理系统中能够提供更方便的权限控制。
Spring Security框架支持ACL和RBAC鉴权模型,不过Spring Security鉴权部分API设计的极差,用法让人迷惑。这篇笔记我们主要以例子的形式,学习Spring Security中实现鉴权的一些最佳实践。
一种较为通用的权限表示法是:<访问主体>:<访问动作>:<访问主体的实例>
,例如doc:query:doc1
表示针对doc
文档模块下的doc1
文档有查询权限,使用*
通配符代表对该字段的所有取值都有权限,位于授权字符串后面的*
可以省略。不过这里注意Spring Security并没有要求使用这种标准写法,我们可以使用任意字符串表达一个权限,Spring Security内置的权限判断也仅支持简单的权限字符串匹配,不支持通配符。
对于角色定义,Spring Security要求角色必须以ROLE_
开头,除此之外可以使用任意字符串代表角色。
我们可以用5-6张表来表达用户、角色、权限三者之间的关系,用户表和角色表具有多对多关系,角色表和权限表具有多对多关系,此外实际开发中还经常添加用户表和权限表的多对多关系,以此实现更加灵活的鉴权控制。有关表结构可以参考信息安全RBAC相关章节,这里就不多介绍了。
Spring Security中,我们可以使用注解方式对Controller方法进行鉴权,但我们需要将@EnableGlobalMethodSecurity
标注在配置类上。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
}
前一篇介绍认证的时候,我们知道UserDetails
是用户信息的抽象,其中有一个方法getAuthorities()
用于获取权限信息,之前我们还没有实现鉴权因此返回null,实际上我们可以在其中添加角色和权限信息。
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 这里假设从数据库中查询权限和角色信息
String[] perms = new String[]{"userManagement", "menuManagement"};
String[] roles = new String[]{"ROLE_ADMIN"};
// 将权限和角色字符串组装为GrantedAuthority
List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
grantedAuthorityList.addAll(Arrays.stream(perms).map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
grantedAuthorityList.addAll(Arrays.stream(roles).map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
return grantedAuthorityList;
}
上面代码中,我们返回了List<GrantedAuthority>
类型的列表,实际上它内部的对象是SimpleGrantedAuthority
,它归根结底其实就是个字符串,这些字符串是权限和角色的标识。实际开发中一般存储在数据库中,通过和用户关联查询出来,这里为了演示方便我们直接通过代码模拟了一些权限和角色。注意Spring Security要求权限可以是任意字符串,角色必须以ROLE_
开头。
注意:通过上面代码我们可以发现,Spring Security将权限和角色都抽象成了GrantedAuthority
,仅仅通过字符串是否以ROLE_
开头为区分,这显然是很糟糕的设计,这种设计常被调侃为面向字符串编程(String Oriented Programming),虽然也能恰好运行,但假如我们有一个权限就叫ROLE_MANAGEMENT
呢?这时就会出现歧义。我们实际开发中设计框架底层实现时千万不要这样草率。
下面例子是一个Controller方法,我们使用了Spring Security的鉴权注解对其进行标注。
@PreAuthorize("hasAuthority('userManagement')")
@GetMapping("/userManagement")
public ApiResult<?> userManagement() {
// ...
}
@PreAuthorize
是最常用的鉴权注解,它表示在执行该方法前判断其权限。注解的内容hasAuthority('userManagement')
实际上是一个SpEL表达式,它调用了hasAuthority
方法,检查当前的SecurityContext是否有userManagement
权限。hasAuthority()
实际上定义在SecurityExpressionRoot
这个类中,我们也可以扩展这个类实现我们自己的鉴权逻辑。
下面例子判断了当前SecurityContext是否有ADMIN
权限,注意Spring Security要求角色以ROLE_
开头,因此我们实际上应该在数据库中存储ROLE_ADMIN
作为该角色的标识。
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/userManagement")
public ApiResult<?> userManagement() {
// ...
}
hasAuthority:判断是否有指定权限
hasAnyAuthority:判断是否有多个指定权限中的任意一个
hasRole:判断是否有指定角色
hasAnyRole:判断是否有指定角色中的任意一个