Spring Security----RBAC权限控制模型,和权限相关知识点整理
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Security----RBAC权限控制模型,和权限相关知识点整理相关的知识,希望对你有一定的参考价值。
Spring Security----RBAC权限控制模型
我们开发一个系统,必然面临权限控制的问题,即不同的用户具有不同的访问、操作、数据权限。形成理论的权限控制模型有:自主访问控制(DAC: Discretionary Access Control)、强制访问控制(MAC: Mandatory Access Control)、基于属性的权限验证(ABAC: Attribute-Based Access Control)等。最常被开发者使用也是相对易用、通用的就是RBAC权限模型(Role-Based Access Control),本文就将向大家介绍该权限模型。
RBAC权限模型简介
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。模型中有几个关键的术语:
- 用户:系统接口及功能访问的操作者
- 权限:能够访问某接口或者做某操作的授权资格
- 角色:具有一类相同操作权限的用户的总称
RBAC权限模型核心授权逻辑如下:
- 某用户是什么角色?
- 某角色具有什么权限?
- 通过角色的权限推导用户的权限
RBAC的演化进程
用户与权限直接关联
想到权限控制,人们最先想到的一定是用户与权限直接关联的模式,简单地说就是:某个用户具有某些权限。如图:
- 张三具有创建用户和删除用户的权限,所以他可能系统维护人员
- 李四具有产品记录管理和销售记录管理权限,所以他可能是一个业务销售人员
这种模型能够清晰的表达用户与权限之间的关系,足够简单。但同时也存在问题:
- 现在用户是张三、李四,以后随着人员增加,每一个用户都需要重新授权
- 或者张三、李四离职,需要针对每一个用户进行多种权限的回收
一个用户拥有一个角色
在实际的团体业务中,都可以将用户分类。比如对于薪水管理系统,通常按照级别分类:经理、高级工程师、中级工程师、初级工程师。也就是按照一定的角色分类,通常具有同一角色的用户具有相同的权限。这样改变之后,就可以将针对用户赋权转换为针对角色赋权。因为角色少、权限多,所以基于角色管理权限,减少用户在授权与权限回收过程中的过多操作。
- 一个用户有一个角色
- 一个角色有多个操作(菜单)权限
- 一个操作权限可以赋予多个角色
我们可以用下图中的数据库设计模型,描述这样的关系。
一个用户一个或多个角色
但是在实际的应用系统中,一个用户一个角色远远满足不了需求。如果我们希望一个用户既担任销售角色、又暂时担任副总角色。该怎么做呢?为了增加系统设计的适用性,我们通常设计:
- 一个用户有一个或多个角色
- 一个角色包含多个用户
- 一个角色有多种权限
- 一个权限可以赋予多个角色
我们可以用下图中的数据库设计模型,描述这样的关系。
- sys_user是用户信息表,用于存储用户的基本信息,如:用户名、密码
- sys_role是角色信息表,用于存储系统内所有的角色
- sys_menu是系统的菜单信息表,用于存储系统内所有的菜单。用id与父id的字段关系维护一个菜单树形结构。
- sys_user_role是用户角色多对多关系表,一条userid与roleid的关系记录表示该用户具有该角色,该角色包含该用户。
- sys_role_menu是角色菜单(权限)关系表,一条roleid与menuid的关系记录表示该角色由某菜单权限,该菜单权限可以被某角色访问。
页面访问权限与操作权限
- 页面访问权限:
所有系统都是由一个个的页面组成,页面再组成模块,用户是否能看到这个页面的菜单、是否能进入这个页面就称为页面访问权限。 - 操作权限:
用户在操作系统中的任何动作、交互都需要有操作权限,如增删改查等。比如:某个按钮,某个超链接用户是否可以点击,是否应该看见的权限。
为了适应这种需求,我们可以把页面资源(菜单)和操作资源(按钮)分表存放,如上图。也可以把二者放到一个表里面存放,用一个字段进行标志区分。
数据权限
数据权限比较好理解,就是某个用户能够访问和操作哪些数据。
- 通常来说,数据权限由用户所属的组织来确定。比如:生产一部只能看自己部门的生产数据,生产二部只能看自己部门的生产数据;销售部门只能看销售数据,不能看财务部门的数据。而公司的总经理可以看所有的数据。
- 在实际的业务系统中,数据权限往往更加复杂。非常有可能销售部门可以看生产部门的数据,以确定销售策略、安排计划等。
所以为了面对复杂的需求,数据权限的控制通常是由程序员书写个性化的SQL来限制数据范围的,而不是交给权限模型或者Spring Security或shiro来控制。当然也可以从权限模型或者权限框架的角度去解决这个问题,但适用性有限。
动态加载用户角色权限数据
我们所有的用户、角色、权限信息都是在配置文件里面写死的,然而在实际的业务系统中,这些信息通常是存放在RBAC权限模型的数据库表中的。
下面来把这些信息从数据库里面进行加载。
下面我们来回顾一下其中的核心概念:
- RBAC的权限模型可以从用户获取为用户分配的一个或多个角色,从用户的角色又可以获取该角色的多种权限。通过关联查询可以获取某个用户的角色信息和权限信息
- 如果我们不希望用户、角色、权限信息写死在配置里面。我们应该实现UserDetails与UserDetailsService接口,从而从数据库或者其他的存储上动态的加载这些信息。
UserDetails与UserDetailsService接口
UserDetails接口表达你是谁?你有什么角色权限。UserDetailsService接口表达的是如何动态加载UserDetails数据。
- UserDetailsService接口有一个方法叫做loadUserByUsername,我们实现动态加载用户、角色、权限信息就是通过实现该方法。函数见名知义:通过用户名加载用户。该方法的返回值就是UserDetails
- UserDetails就是用户信息,即:用户名、密码、该用户所具有的权限。
下面我们来看一下UserDetails接口都有哪些方法。
public interface UserDetails extends Serializable
//获取用户的权限集合
Collection<? extends GrantedAuthority> getAuthorities();
//获取密码
String getPassword();
//获取用户名
String getUsername();
//账号是否没过期
boolean isAccountNonExpired();
//账号是否没被锁定
boolean isAccountNonLocked();
//密码是否没过期
boolean isCredentialsNonExpired();
//账户是否可用
boolean isEnabled();
现在我们明白了,只要我们把这些信息提供给Spring Security,Spring Security就知道怎么做登录验证了,根本不需要我们自己写Controller实现登录验证逻辑。那我们怎么把这些信息提供给Spring Security,用的就是下面的接口方法
public interface UserDetailsService
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
实现UserDetails 接口
public class MyUserDetails implements UserDetails
String password; //密码
String username; //用户名
boolean accountNonExpired; //是否没过期
boolean accountNonLocked; //是否没被锁定
boolean credentialsNonExpired; //密码是否没过期
boolean enabled; //账号是否可用
Collection<? extends GrantedAuthority> authorities; //用户的权限集合
public void setPassword(String password)
this.password = password;
public void setUsername(String username)
this.username = username;
public void setAccountNonExpired(boolean accountNonExpired)
this.accountNonExpired = accountNonExpired;
public void setAccountNonLocked(boolean accountNonLocked)
this.accountNonLocked = accountNonLocked;
public void setCredentialsNonExpired(boolean credentialsNonExpired)
this.credentialsNonExpired = credentialsNonExpired;
public void setEnabled(boolean enabled)
this.enabled = enabled;
public void setAuthorities(Collection<? extends GrantedAuthority> authorities)
this.authorities = authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
return authorities;
@Override
public String getPassword()
return password;
@Override
public String getUsername()
return username;
@Override
public boolean isAccountNonExpired()
return true; //暂时未用到,直接返回true,表示账户未过期
@Override
public boolean isAccountNonLocked()
return true; //暂时未用到,直接返回true,表示账户未被锁定
@Override
public boolean isCredentialsNonExpired()
return true; //暂时未用到,直接返回true,表示账户密码未过期
@Override
public boolean isEnabled()
return enabled;
我们就是写了一个适应于UserDetails的java POJO类,所谓的 UserDetails接口实现就是一些get方法。
- get方法由Spring Security调用,获取认证及鉴权的数据
- 我们通过set方法或构造函数为 Spring Security提供UserDetails数据(从数据库查询)。
- 当enabled的值为false的时候,Spring Security会自动的禁用该用户,禁止该用户进行系统登录。
- 通常数据库表sys_user字段要和MyUserDetails 属性一一对应,比如username、password、enabled。
目前数据库表里面没有定义accountNonExpired、accountNonLocked、credentialsNonExpired这三个字段,目前暂时也用不到,因此这三个属性对应的get方法,返回值默认为true
实现UserDetailsService接口
@Component
public class MyUserDetailsService implements UserDetailsService
@Resource
private MyUserDetailsServiceMapper myUserDetailsServiceMapper;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException
//获得用户信息
MyUserDetails myUserDetails =
myUserDetailsServiceMapper.findByUserName(username);
if(myUserDetails == null)
throw new UsernameNotFoundException("用户名不存在");
//获得用户角色列表
List<String> roleCodes =
myUserDetailsServiceMapper.findRoleByUserName(username);
//通过角色列表获取权限列表
List<String> authorities =
myUserDetailsServiceMapper.findAuthorityByRoleCodes(roleCodes);
//为角色标识加上ROLE_前缀(Spring Security规范)
roleCodes = roleCodes.stream()
.map(rc -> "ROLE_" + rc )
.collect(Collectors.toList());
//角色是一种特殊的权限,所以合并
authorities.addAll(roleCodes);
//转成用逗号分隔的字符串,为用户设置权限标识
myUserDetails.setAuthorities(
AuthorityUtils.commaSeparatedStringToAuthorityList(
String.join(",",authorities)
)
);
return myUserDetails;
- 角色是一种特殊的权限,在Spring Security我们可以使用hasRole(角色标识)表达式判断用户是否具有某个角色,决定他是否可以做某个操作;通过hasAuthority(权限标识)表达式判断是否具有某个操作权限。
- 上述实现中用到的MyUserDetailsServiceMapper 是Mybatis操作数据库的接口实现,看文末代码。
注册UserDetailsService
重写WebSecurityConfigurerAdapter的 configure(AuthenticationManagerBuilder auth)方法
@Bean("passwordEncoder")
public PasswordEncoder passwordEncoder()
return new BCryptPasswordEncoder();
@Resource
private MyUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception
builder.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
使用BCryptPasswordEncoder,表示存储中(数据库)取出的密码必须是经过BCrypt加密算法加密的。
这里需要注意的是,因为我们使用了BCryptPasswordEncoder加密解密,所以数据库表里面存的密码应该是加密之后的密码(造数据的过程),可以使用如下代码加密(如密码是:123456)。将打印结果保存保存到密码字段。
@Resource
PasswordEncoder passwordEncoder;
@Test
public void contextLoads()
System.out.println(passwordEncoder.encode("123456"));
总结
至此,我们将系统里面的所有的用户、角色、权限信息都通过UserDetailsService和UserDetails告知了Spring Security。但是多数朋友可能仍然不知道该怎样实现登录的功能,其实剩下的事情很简单了:
- 写一个登录界面,写一个登录表单,表单使用post方法提交到默认的/login路径
- 表单的用户名、密码字段名称默认是username、password。
- 写一个登录成功之后的跳转页面,比如index.html
然后把这些信息通过配置方式告知Spring Security ,以上的配置信息名称都可以灵活修改。
附录:Mybatis持久层数据接口
public interface MyUserDetailsServiceMapper
//根据userID查询用户信息
@Select("SELECT username,password,enabled\\n" +
"FROM sys_user u\\n" +
"WHERE u.username = #userId")
MyUserDetails findByUserName(@Param("userId") String userId);
//根据userID查询用户角色
@Select("SELECT role_code\\n" +
"FROM sys_role r\\n" +
"LEFT JOIN sys_user_role ur ON r.id = ur.role_id\\n" +
"LEFT JOIN sys_user u ON u.id = ur.user_id\\n" +
"WHERE u.username = #userId")
List<String> findRoleByUserName(@Param("userId") String userId);
//根据用户角色查询用户权限
@Select(
"<script>",
"SELECT url " ,
"FROM sys_menu m " ,
"LEFT JOIN sys_role_menu rm ON m.id = rm.menu_id " ,
"LEFT JOIN sys_role r ON r.id = rm.role_id ",
"WHERE r.role_code IN ",
"<foreach collection='roleCodes' item='roleCode' open='(' separator=',' close=')'>",
"#roleCode",
"</foreach>",
"</script>"
)
List<String> findAuthorityByRoleCodes(@Param("roleCodes") List<String> roleCodes);
动态加载资源鉴权规则
我们已经实现了从RBAC数据库中加载用户的角色、权限信息。在我们的Spring Security配置类SecurityConfig中仍然有一部分内容是通过代码静态配置的,那就是:资源鉴权规则。
简单说“资源鉴权规则”就是:你有哪些权限?这些权限能够访问哪些资源?即:权限与资源的匹配关系。
实现效果
上图是资源鉴权规则完成之后的效果:
- 首先将静态规则去掉(注释掉的部分内容),这部分内容我们将替换为动态从数据库加载
- 登录页面“login.html”和登录认证处理路径“/login”需完全对外开发,不需任何鉴权就可以访问
- 首页"/index"必须authenticated,即:登陆之后才能访问。不做其他额外鉴权规则控制。
- 最后,其他的资源的访问我们通过权限规则表达式实现,表达式规则中使用了rbacService,这个类我们自定义实现。该类服务hasPermission从内存(或数据库)动态加载资源匹配规则,进行资源访问鉴权。
动态资源鉴权规则
@Component("rbacService")
public class MyRBACService
/**
* 判断某用户是否具有该request资源的访问权限
*/
public boolean hasPermission(HttpServletRequest request, Authentication authentication)
//获取认证主体
Object principal = authentication.getPrincipal();
if(principal instanceof UserDetails)
//获取当前登录用户的UserDetails
UserDetails userDetails = ((UserDetails)principal);
//将当前请求的访问资源路径,如:"/syslog",包装成资源权限标识
SimpleGrantedAuthority simpleGrantedAuthority
= new SimpleGrantedAuthority(request.getRequestURI());
//判断用户已授权访问的资源中,是否包含“本次请求的资源”
return userDetails.getAuthorities().contains(simpleGrantedAuthority);
return false;
上述代码逻辑很简单:
- 首先从authentication中获取principal (即UserDetails),UserDetails里面包含authorities(即当前登录用户可以访问的所有的资源访问路径、资源唯一标识)
- 如果authorities列表中任何一个元素,能够和request.getRequestURI()请求资源路径相匹配,则表示该用户具有访问该资源的权限。
- hasPermission有两个参数,第一个参数是HttpServletRequest ,第二个参数是Authentication认证主体
- 用户每一次访问系统资源的时候,都会执行这个方法,判断该用户是否具有访问该资源的权限。
测试一下
如果使用admin用户登录,其加载数据内容如下图(根据之前章节调整RBAC模型数据库表里面的数据)。所以通过admin登录只能访问“用户管理”和“日志管理”功能。
如果使用admin用户登录,其加载数据内容如下图(根据之前章节调整RBAC模型数据库表里面的数据)。所以通过admin登录只能访问“具体业务一”和“具体业务二”功能。
权限表达式使用方法总结
SPEL表达式权限控制
从spring security 3.0开始已经可以使用spring Expression表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。Spring Security可用表达式对象的基类是SecurityExpressionRoot。
表达式函数 | 描述 |
---|---|
hasRole([role]) | 用户拥有指定的角色时返回true (Spring security默认会带有ROLE_前缀),去除前缀参考Remove the ROLE_ |
hasAnyRole([role1,role2]) | 用户拥有任意一个指定的角色时返回true |
hasAuthority([authority]) | 拥有某资源的访问权限时返回true |
hasAnyAuthority([auth1,auth2]) | 拥有某些资源其中部分资源的访问权限时返回true |
permitAll | 永远返回true |
denyAll | 永远返回false |
anonymous | 当前用户是anonymous时返回true |
rememberMe | 当前用户是rememberMe用户返回true |
authentication | 当前登录用户的authentication对象 |
fullAuthenticated | 当前用户既不是anonymous也不是rememberMe用户时返回true |
hasIpAddress(‘192.168.1.0/24’) | 请求发送的IP匹配时返回true |
部分朋友可能会对Authority和Role有些混淆。Authority作为资源访问权限可大可小,可以是某按钮的访问权限(如资源ID:biz1),也可以是某类用户角色的访问权限(如资源ID:ADMIN)。当Authority作为角色资源权限时,hasAuthority(‘ROLE_ADMIN’)与hasRole(‘ADMIN’)是一样的效果
SPEL在全局配置中的使用
我们可以通过继承WebSecurityConfigurerAdapter,实现相关的配置方法,进行全局的安全配置 。下面就为大家介绍一些如何在全局配置中使用SPEL表达式。
URL安全表达式
config.antMatchers("/person/*").access("hasRole('admin') or hasAuthority('ROLE_user')")
.anyRequest().authenticated();
这里我们定义了应用/person/*URL的范围,只有拥有ADMIN或者USER权限的用户才能访问这些person资源。
安全表达式中引用bean
这种方式,比较适合有复杂权限验证逻辑的情况,当Spring Security提供的默认表达式方法无法满足我们的需求的时候。实际上在上面的动态加载资源鉴权规则里面,我么已经使用了这种方法。首先我们定义一个权限验证的RbacService。
@Component("rbacService")
@Slf4j
public class RbacService
//返回true表示验证通过
public boolean hasPermission(HttpServletRequest request, Authentication authentication)
//验证逻辑代码
return true;
public boolean checkUserId(Authentication authentication, int id)
//验证逻辑代码
return true;
对于"/person/id"对应的资源的访问,调用rbacService的bean的方法checkUserId进行权限验证,传递参数为authentication对象和person的id。该id为PathVariable,以#开头表示。
config.antMatchers("/person/id").access("@rbacService.checkUserId(authentication,#id)")
.anyRequest().access("@rbacService.hasPermission(request,authentication)");
Method表达式安全控制
如果我们想实现方法级别的安全配置,Spring Security提供了四种注解,分别是@PreAuthorize
, @PreFilter ,
@PostAuthorize
和 @PostFilter
开启方法级别注解的配置
在Spring安全配置代码中,加上EnableGlobalMethodSecurity
注解,开启方法级别安全配置功能。
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter
@EnableGlobalMethodSecurity是一个组合注解,里面包含了@Configuration,所以不需要再加一个@Configuration注解了
使用PreAuthorize注解
@PreAuthorize
注解适合进入方法前的权限验证。只有拥有ADMIN
角色才能访问findAll方法。
@PreAuthorize("hasRole('admin')")
public List<PersonDemo> findAll()
return null;
如果当前登录用户没有PreAuthorize需要的权限,将抛出org.springframework.security.access.AccessDeniedException异常!
使用PostAuthorize注解
@PostAuthorize 在方法执行后再进行权限验证,适合根据返回值结果进行权限验证。Spring EL 提供返回对象能够在表达式语言中获取返回的对象returnObject。下文代码只有返回值的name等于authentication对象的name(当前登录用户名)才能正确返回,否则抛出异常。
@PostAuthorize("returnObject.name == authentication.name")
public PersonDemo findOne()
String authName =
SecurityContextHolder.getContext().getAuthentication()Spring安全权限管理(Spring Security)
Spring boot + Spring Security实现权限管理
Spring Boot + Spring Cloud 实现权限管理系统 后端篇(二十四):权限控制(Shiro 注解)
spring boot oauth2.0 和 spring security:如何通过 facebook 或 slack 授予用户登录权限(权限)