权限设计


权限设计

  • 登录认证

登录认证是对用户身份进行确认。

  • 权限认证

权限认证是对用户能否访问某个资源进行确认,一般在用户登录成功之后进行。

RBAC 模型

用户,角色,权限。在这种设计模式下,用户不直接拥有权限,权限是赋值给角色的,用户只能具有某种角色后,才能获取对应角色的权限。

用户和角色是多对多的关系角色与权限也是多对多的关系用户通过角色来关联到权限。

  • 数据库的设计如下:

用户-角色-权限

页面权限

权限表内存储的是前端对应页面的 url,在用户登录时,把用户信息返回给前端,有前端控制是否展示对应的界面。

操作权限

权限表内存储是后端对应接口的地址,在用户登录时,把用户信息返回给前端,有前端控制是否展示对应的按钮。

数据权限

数据权限说直白点就是哪些人能看到什么数据。

例如:有一张数据表和公司表,数据表里有company_id字段,当前用户能看到哪些公司的数据,只需要增加一个用户和公司的关联表,然后在 SQL 查询的时候,SELECT * FROM data WHERE company_id in (?, ?, ?...) ORDER BY create_time DESC LIMIT ?,? 即可实现。

注意事项

  1. 无论是角色表还是权限表,都需要存储对应角色或权限的标识码。
  2. 权限一般分为:目录,菜单,按钮,只有对应按钮才会触发后端的方法,因此权限表内存储的按钮信息,需要存储对应的标识码(sys:user:add:用户新增),其它类型的可以不存。
  3. 在给用户分配菜单时,要么默认菜单上的按钮全部分配,要么默认都不分配。不能只分配菜单,不处理菜单上的按钮。
  4. 权限要做好分层的设计。
  5. 目录,菜单,按钮的权限有前端控制是否展示,但按钮触发的接口后端也需要校验。

设计思路

用户登录时把用户的权限码给用户

  • 设置用户的权限码,其实就是用户能够访问的接口在权限表内的标识码,这一步主要为了后续的校验。
// 保存用户登录日志 生成 Token 信息
LoginUser loginUser = new LoginUser();
loginUser.setId(tSysUser.getId());
loginUser.setName(tSysUser.getName());
loginUser.setEmail(tSysUser.getEmail());
// 设置用户的权限码,其实就是用户能够访问的接口在权限表内的标识码,这一步主要为了后续的校验
loginUser.setPermissions(sysMenuService.getMenuByUserId(loginUser.getId()));
sysUserLoginLogsService.setSysUserLoginLogs(tSysUser, null, "success", "login");
return R.ok(sysUserService.createToken(loginUser));

返回用户-角色-权限 树形信息

  • 用户登录后,前端请求该接口,拿到用户能够看到的数据,其中包括:用户信息,用户角色信息,对应角色下的权限信息,重叠的数据取并集。
// 用户登录后,前端请求该接口,拿到用户能够看到的数据,其中包括:用户基本信息,用户角色基本信息,对应角色下的权限信息,重叠的数据取并集
public SysUserRoleMenuTreeVO getUserRoleMenuTree() {
    LoginUser loginUser = LoginUserUtils.get();
    TSysUser tSysUser = sysUserService.getById(loginUser.getId());
    SysUserRoleMenuTreeVO vo = new SysUserRoleMenuTreeVO();
    BeanUtils.copyProperties(tSysUser, vo);
    Set<SysRoleVO> rolesList = sysRoleService.getRolesByUserId(loginUser.getId());
    rolesList.forEach(m -> {
        m.setMenus(buildTree(sysMenuService.getMenuTreeByRoleId(m.getId())));
    });
    vo.setRoles(rolesList);
    return vo;
}

// 封装数据树形方法
public Set<SysMenuVO> buildTree(Set<SysMenuVO> set) {
    Map<Long, SysMenuVO> parentTree = new HashMap<>();
    Set<SysMenuVO> children = new HashSet<>();

    for (SysMenuVO data : set){
        parentTree.put(data.getId(), data);
    }
    for (SysMenuVO data : set){
        if (data.getParent() == 0){
            children.add(data);
        } else {
            SysMenuVO vo = parentTree.get(data.getParent());
            if (ObjectUtils.isNotEmpty(vo)) vo.getChildren().add(data);
        }
    }
    return children;
}

定义后端接口校验方法

  • 权限注解的校验模式
/**
 * 权限注解的验证模式
 *
 */
public enum Logical {
    /**
     * 必须具有所有的元素
     */
    AND,

    /**
     * 只需具有其中一个元素
     */
    OR
}
  • 权限认证接口
/**
 * 权限认证:必须具有指定权限才能进入该方法
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequiresPermissions {
    /**
     * 需要校验的权限码
     */
    String[] value() default {};

    /**
     * 验证模式:AND | OR,默认AND
     */
    Logical logical() default Logical.AND;
}
  • 角色认证接口
/**
 * 角色认证:必须具有指定角色标识才能进入该方法
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequiresRoles {
    /**
     * 需要校验的角色标识
     */
    String[] value() default {};

    /**
     * 验证逻辑:AND | OR,默认AND
     */
    Logical logical() default Logical.AND;
}
  • 定义权限/角色认证出错的异常类
/**
 * 未能通过的权限认证异常
 *
 * @author ruoyi
 */
public class NotPermissionException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    public NotPermissionException(String permission) {
        super(permission);
    }

    public NotPermissionException(String[] permissions) {
        super(StringUtils.join(permissions, ","));
    }
}

/**
 * 未能通过的角色认证异常
 *
 * @author ruoyi
 */
public class NotRoleException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    public NotRoleException(String role) {
        super(role);
    }

    public NotRoleException(String[] roles) {
        super(StringUtils.join(roles, ","));
    }
}

// 封装的接口类里需要增加这两个类
/**
     * 权限的校验
     * @param notPermissionException
     * @return
     */
@ExceptionHandler(NotPermissionException.class)
public R<?> notPermissionException(NotPermissionException notPermissionException) {
    log.error(notPermissionException.getMessage());
    return R.fail(notPermissionException.getMessage());
}

/**
     * 角色的校验
     * @param notRoleException
     * @return
     */
@ExceptionHandler(NotRoleException.class)
public R<?> notRoleException(NotRoleException notRoleException) {
    log.error(notRoleException.getMessage());
    return R.fail(notRoleException.getMessage());
}
  • 基于 AOP 注解校验类
/**
 * 基于 Spring Aop 的注解鉴权
 *
 */
@Aspect
@Component
public class PreAuthorizeAspect {
    /**
     * 构建
     */
    public PreAuthorizeAspect() {}

    /**
     * 定义AOP签名 (切入所有使用鉴权注解的方法)
     */
    public static final String POINTCUT_SIGN = "@annotation(com.rhx.manage.config.auth.RequiresPermissions) || " + "@annotation(com.rhx.manage.config.auth.RequiresRoles)";

    /**
     * 声明AOP签名
     */
    @Pointcut(POINTCUT_SIGN)
    public void pointcut() {}

    /**
     * 环绕切入
     *
     * @param joinPoint 切面对象
     * @return 底层方法执行后的返回值
     * @throws Throwable 底层方法抛出的异常
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 注解鉴权
        checkMethodAnnotation(((MethodSignature) joinPoint.getSignature()).getMethod());
        // 执行原有逻辑
        return joinPoint.proceed();
    }

    /**
     * 对一个Method对象进行注解检查
     */
    public void checkMethodAnnotation(Method method) {
        // 校验 @RequiresRoles 注解
        RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);
        if (ObjectUtils.isNotEmpty(requiresRoles)) AuthUtils.checkRole(requiresRoles);

        // 校验 @RequiresPermissions 注解
        RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
        if (ObjectUtils.isNotEmpty(requiresPermissions)) AuthUtils.checkPermi(requiresPermissions);
    }
}
  • 公共接口类
/**
 * 权限校验公共方法
 */
public class AuthUtils {

    /**
     * 根据角色鉴权
     * @param requiresRoles
     */
    public static void checkRole(RequiresRoles requiresRoles) {
        if (requiresRoles.logical() == Logical.AND) {
            checkRoleAnd(requiresRoles.value());
        } else {
            checkRoleOr(requiresRoles.value());
        }
    }

    /**
     * 验证用户是否含有指定角色,必须全部拥有
     *
     * @param roles 角色标识数组
     */
    public static void checkRoleAnd(String... roles) {
        Set<String> roleList = getRoleList();
        for (String role : roles) {
            if (!hasRole(roleList, role)) {
                throw new NotRoleException(role);
            }
        }
    }

    /**
     * 验证用户是否含有指定角色,只需包含其中一个
     *
     * @param roles 角色标识数组
     */
    public static void checkRoleOr(String... roles) {
        Set<String> roleList = getRoleList();
        for (String role : roles) {
            if (hasRole(roleList, role)) {
                return;
            }
        }
        if (roles.length > 0) {
            throw new NotRoleException(roles);
        }
    }

    /**
     * 获取当前账号的角色列表
     *
     * @return 角色列表
     */
    public static Set<String> getRoleList() {
        try {
            LoginUser loginUser = LoginUserUtils.get();
            return loginUser.getRoles();
        } catch (Exception e) {
            return new HashSet<>();
        }
    }

    /**
     * 判断是否包含角色
     *
     * @param roles 角色列表
     * @param role  角色
     * @return 用户是否具备某角色权限
     */
    public static boolean hasRole(Collection<String> roles, String role) {
        return roles.stream().filter(StringUtils::hasText)
                .anyMatch(x -> PatternMatchUtils.simpleMatch(x, role));
    }

    /**
     * 校验用户权限
     * @param requiresPermissions
     * @throws Exception
     */
    public static void checkPermi(RequiresPermissions requiresPermissions) {
        if (requiresPermissions.logical() == Logical.AND) {
            checkPermiAnd(requiresPermissions.value());
        } else {
            checkPermiOr(requiresPermissions.value());
        }
    }

    /**
     * 验证用户是否含有指定权限,必须全部拥有
     *
     * @param permissions 权限列表
     */
    public static void checkPermiAnd(String... permissions) {
        Set<String> permissionList = getPermiList();
        for (String permission : permissions) {
            if (!hasPermi(permissionList, permission)) {
                throw new NotPermissionException("无对应权限: " + permission);
            }
        }
    }

    /**
     * 验证用户是否含有指定权限,只需包含其中一个
     *
     * @param permissions 权限码数组
     */
    public static void checkPermiOr(String... permissions) {
        Set<String> permissionList = getPermiList();
        for (String permission : permissions) {
            if (hasPermi(permissionList, permission)) {
                return;
            }
        }
        if (permissions.length > 0) {
            throw new NotPermissionException(permissions);
        }
    }

    /**
     * 判断是否包含权限
     *
     * @param authorities 权限列表
     * @param permission  权限字符串
     * @return 用户是否具备某权限
     */
    public static boolean hasPermi(Collection<String> authorities, String permission){
        if (CollectionUtils.isEmpty(authorities)) {
            throw new NotPermissionException("无对应权限: " + permission);
        }
        return authorities.stream().filter(StringUtils::hasText)
                .anyMatch(x -> PatternMatchUtils.simpleMatch(x, permission));
    }

    /**
     * 获取当前账号的权限列表
     *
     * @return 权限列表
     */
    public static Set<String> getPermiList() {
        try {
            LoginUser loginUser = LoginUserUtils.get();
            return loginUser.getPermissions();
        } catch (Exception e) {
            return new HashSet<>();
        }
    }
}
  • 接口使用
/**
* @RequiresPermissions(value = "sys:user:page")
* @RequiresPermissions(value = "sys:user:page", logical = Logical.OR)
* 使用的时候,注意上述两种注解的区别
*/
@RequiresPermissions(value = "sys:user:page", logical = Logical.OR)
@ApiOperation(value = "注册用户分页查询")
@PostMapping(value = "/page")
public R<?> getSysUserPage(@RequestBody SysUserPageDTO pageDTO) {
    return R.ok(sysUserHandler.getSysUserPage(pageDTO));
}

补充

当对用户的角色信息操作时,已登录的用户如何感知?

  • 方案一

增加拦截器,每次的接口访问都对用户的角色信息进行校验,然后把信息存储到 Redis 内,当操作用户的角色信息时,及时更新或者刷新缓存。

  • 方案二

操作用户角色信息后,强制用户下线,然后重新登录。


文章作者: L Q
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 L Q !
  目录