Ruoyi RBAC权限
rbac简介
RBAC是Role Based Access Control,基于角色的访问控制。
事实上就是在以前的用户直接关联权限的基础上,中间再抽象出一角色层,实现用户和权限的解耦,使之更易扩展与维护(试想一个员工入离职业务人员就要手动勾选取消几十个权限..)
常见名词:
- User(用户):每个用户都有唯一的UID识别,并被授予不同的角色
- Role(角色):不同角色具有不同的权限
- Permission(权限):访问权限
- 用户-角色映射:用户和角色之间的映射关系
- 角色-权限映射:角色和权限之间的映射
- 资源:属于某个角色允许访问的,如菜单、按钮
ruoyi RBAC
若依是经典的RBAC设计,有 用户-角色-菜单(资源权限)
数据库设计
主要有
- 用户表
- 角色表
- 菜单表
- 部门表
基本使用
菜单
ry的菜单又细分了三个维度
- 目录(顶级菜单)
- 菜单(子级菜单)
- 按钮
数据库设计
- 父组件的
parent_id为0,component组件路径为空,perms权限标识为空 - 比较关键的
perms字段,后期做权限校验,就是将登录用户的所有perm加载到缓存中,用户要访问某一资源时,再去判断其是否拥有该资源perm - 按钮没有路由标识,只有权限标识
用户
此时除了在sys_user表保存用户信息,还会在sys_user_role表保存用户所关联的角色。这样通过用户->角色->菜单这样的关系,就可以定义用户所有的权限
登录我们所创建的用户,只能看见角色对应的资源
角色
可以给一个角色分配菜单权限,对应修改sys_role_menu表
原理分析
前端
当用户登录成功后,会将用户信息存到redis中。其中就包含了角色和权限字符串的信息,前端在登录成功后,也会调用getInfo接口将用户权限信息在vuex存储
前端自定义了两个指令:
hasPermi 和 hasRole,前者是根据自定义的roleKey权限字符判断是否可见,后者是通过角色判断是否可见。基本上每一个按钮新建的时候都会指定权限字符,所以做到了按钮级的权限管理
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['system:dict:export']"
>导出</el-button>
export default {
inserted(el, binding, vnode) {
const { value } = binding
const all_permission = "*:*:*";
const permissions = store.getters && store.getters.permissions
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value
const hasPermissions = permissions.some(permission => {
return all_permission === permission || permissionFlag.includes(permission)
})
if (!hasPermissions) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`请设置操作权限标签值`)
}
}
}
前端做了可见性判断,后端也需要做,不然就可以直接调用接口获取数据了。
后端
后端使用SpringSecurity框架,主要依靠@PreAuthorize注解对接口进行权限校验。
@PreAuthorize("@ss.hasPermi('system:role:list')")
@GetMapping("/list")
public TableDataInfo list(SysRole role)
{
startPage();
List<SysRole> list = roleService.selectRoleList(role);
return getDataTable(list);
}
/**
* 验证用户是否具备某权限
*
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
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;
}
PermissionContextHolder.setContext(permission);
return hasPermissions(loginUser.getPermissions(), permission);
}
loginUser的数据是通过拦截器前置存入
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
ruoyi 数据隔离
基本使用
不仅要让没有权限的角色看不到菜单、按钮,在数据上也需要做隔离。
ry在分配角色的时候可以指定其可见数据范围
自定义权限可更细粒度隔离数据
原理分析
在我们创建角色信息的时候,会保存所勾选的权限范围
/**
* 全部数据权限
*/
public static final String DATA_SCOPE_ALL = "1";
/**
* 自定数据权限
*/
public static final String DATA_SCOPE_CUSTOM = "2";
/**
* 部门数据权限
*/
public static final String DATA_SCOPE_DEPT = "3";
/**
* 部门及以下数据权限
*/
public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
/**
* 仅本人数据权限
*/
public static final String DATA_SCOPE_SELF = "5";
/**
* 数据权限过滤关键字
*/
public static final String DATA_SCOPE = "dataScope";
当调用接口需要做数据隔离的时候,ry自定义DataScope注解,将其加在方法上。
/**
* 数据权限过滤注解
*
* @author ruoyi
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{
/**
* 部门表的别名
*/
public String deptAlias() default "";
/**
* 用户表的别名
*/
public String userAlias() default "";
/**
* 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取,多个权限用逗号分隔开来
*/
public String permission() default "";
}
/**
* 查询部门管理数据
*
* @param dept 部门信息
* @return 部门信息集合
*/
@Override
@DataScope(deptAlias = "d")
public List<SysDept> selectDeptList(SysDept dept)
{
return deptMapper.selectDeptList(dept);
}
去寻找这个注解的切面
@Before("@annotation(controllerDataScope)")
public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable
{
clearDataScope(point);
handleDataScope(point, controllerDataScope);
}
protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope)
{
// 获取当前的用户
LoginUser loginUser = SecurityUtils.getLoginUser();
if (StringUtils.isNotNull(loginUser))
{
SysUser currentUser = loginUser.getUser();
// 如果是超级管理员,则不过滤数据
if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
{
String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext());
dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
controllerDataScope.userAlias(), permission);
}
}
}
调用dataScopeFilter方法动态将参数放到BaseEntity的扩展字段里面。
/**
* 数据范围过滤
*
* @param joinPoint 切点
* @param user 用户
* @param deptAlias 部门别名
* @param userAlias 用户别名
* @param permission 权限字符
*/
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission)
{
StringBuilder sqlString = new StringBuilder();
List<String> conditions = new ArrayList<String>();
for (SysRole role : user.getRoles())
{
String dataScope = role.getDataScope();
if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope))
{
continue;
}
if (StringUtils.isNotEmpty(permission) && StringUtils.isNotEmpty(role.getPermissions())
&& !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission)))
{
continue;
}
if (DATA_SCOPE_ALL.equals(dataScope))
{
sqlString = new StringBuilder();
break;
}
else if (DATA_SCOPE_CUSTOM.equals(dataScope))
{
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
role.getRoleId()));
}
else if (DATA_SCOPE_DEPT.equals(dataScope))
{
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
}
else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
{
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
deptAlias, user.getDeptId(), user.getDeptId()));
}
else if (DATA_SCOPE_SELF.equals(dataScope))
{
if (StringUtils.isNotBlank(userAlias))
{
sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
}
else
{
// 数据权限为仅本人且没有userAlias别名不查询任何数据
sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
}
}
conditions.add(dataScope);
}
if (StringUtils.isNotBlank(sqlString.toString()))
{
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
{
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
}
}
}
我们可以在对应xml语句上看到对应的sql注入
<select id="selectDeptList" parameterType="SysDept" resultMap="SysDeptResult">
<include refid="selectDeptVo"/>
where d.del_flag = '0'
<if test="deptId != null and deptId != 0">
AND dept_id = #{deptId}
</if>
<if test="parentId != null and parentId != 0">
AND parent_id = #{parentId}
</if>
<if test="deptName != null and deptName != ''">
AND dept_name like concat('%', #{deptName}, '%')
</if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
<!-- 数据范围过滤 -->
${params.dataScope}
order by d.parent_id, d.order_num
</select>
仅实体继承BaseEntity才会进行处理,SQL语句会存放到BaseEntity对象中的params属性中供xml参数params.dataScope获取