权限系统设计-功能设计

背景

Snapper权限系统是一套完善的权限解决方案,是在复杂的业务需求中不断完善,直到现在形成一套完善的解决方案,开始总结下系统如何从0开始逐步到1完善的过程,第一步先从功能设计开始。

功能设想

良好的系统设计,总是从一个有效的简单系统发展而来,千万不要从零开始设计一个复杂的系统。-摘自GitHub 的高级工程师肖恩·戈德克

我们先从最简单的开始头脑风暴,想一想一套完善的权限系统大概都有哪些功能,哪些功能是重要的?那些是次要的?token登录、用户设计、角色设计、RBAC、组织设计、资源权限、数据权限....等等等等
大概罗列了一下形成下面的图:

通过花型图总结有以下几个核心功能:

  • 登录 包括用户名密码校验 、登录后其他功能请求的鉴权和认证,这些功能需要用户及其关联结构的支持
  • 分配权限 权限涉及到资源权限数据权限
  • 架构 当前比较流行的两种架构 单体和微服务,为了设计简洁先以单体版为准
  • 安全 包括必要的限流及黑白名单设计

暂时先从以上三个核心功能开始,逐步完善,最终覆盖整个花型图的完整功能

功能定义

功能定义在系统中抽象为按钮,在权限系统中定义为资源,我们把请求URL+请求方法作为访问资源的唯一路径,有以下约定:

  • POST /api/user 是用户新增
  • PUT /api/user 是用户更新
  • DELETE /api/user 是用户删除
  • GET /api/user/{} 是用户明细 其中{}中是用户的ID,ID为全数字

功能流程

核心功能总结起来就是:登录、鉴权、认证、执行业务、SQL拦截改写,以及通用的框架层面处理,包括异常处理、统一参数返回处理,以请求功能为例,流程大概如下所示:

  • 鉴权 判断当前的登录是否有效
  • 认证 判断当前的用户是否有资源的权限
  • 封装用户 帮助我们在业务中能够更好的获取当前登录的用户相关信息
  • SQL拦截 通过数据权限的配置拦截业务的SQL并进行改写以满足数据权限的要求
  • 异常处理 处理系统的错误,包括权限相关的错误、业务相关错误以及不可预知的错误
  • 数据返回处理 通过处理返回统一格式的数据,接收端就可以进行标准格式数据的操作

登录功能

登录是每个系统必备的功能,对于权限系统尤为重要,大概思考下登录的流程包括:用户名查询是否存在、密码匹配 等于成功后生成TOKEN信息、是否支持同一账号同时登录、是否支持验证码登录等等,大概流程如下:

其中缓存用户信息尤为重要,是用于后续操作的数据基础。

流程中:密码加密、密码匹配功能等诸多功能我们可以借助安全框架来完成,包括Spring Security和Shiro等,当前系统我们采用Spring Security。

登录成功后会生成TOKEN信息,后续用户的请求都以TOKEN为准,根据不同的需求为TOKEN生成不同时长的有效期,比如针对手机端有效期长一点,对于客户端有效期与工作时间大概匹配,登录中有一个"终端"的概念,也是后续用户设计中需要注意的地方,此部分将在后续用户设计中深入讨论

登录中有一些细节需要注意:

  • TOKEN的安全性及携带信息 为保证TOKEN的安全性, 采用公钥和私钥的方式进行处理, 公钥参与生成TOKEN, 私钥参与校验TOKEN。TOKEN中会封装一些用户信息,比如用户名信息,便于识别用户,这对后续的鉴权很重要。
  • 同一账号同时登录问题,前面我们引入"终端"概念,可以保证同一账号在不同的"终端"中同时登录,此功能在Snapper系统中为默认约定,如果同一账号在同一终端要求可以同时登录时, 需要借助缓存中间进行进一步处理,该需求可以在用户功能进行设置,要求某个用户满足账号同时登录的需求,此部分在用户设计中深入讨论
  • 多开问题,如果设置了"业务"终端只允许同一个账号登录,在浏览器窗口只能打开一个窗口,这对业务的操作很不利,有时需要多窗口进行对照操作,此时可以在前端添加SESSION复制功能,即复制地址,打开新浏览器窗口进行粘贴会将SESSION信息进行复制
  • 验证码问题 Snapper演示系统(账号密码ximen/123456)默认登录时没有验证码的,但系统集成了验证码的功能,提供了验证码验证的抽象方法,如果需要验证码只需实现该抽象方法即可

鉴权

登录获取TOKEN信息后,后续的功能请求都通过TOKEN来进行身份校验,也就是鉴权。流程如下:

鉴权的核心是获取TOKEN信息后根据私钥验证TOKEN的有效性,一些细节需要注意:

  • 获取用户缓存信息 是在登录时缓存的信息,Snapper系统中主要缓存了,当前用户的角色信息组织信息公司信息职位信息 等,这些信息会封装到用户上下文中,在业务逻辑中通过上下文就能获取这些信息。
  • 流程中的添加头信息 ,会将缓存的用户信息添加到请求头,方便后续的过滤器使用, 这在认证功能中尤为重要。

认证

鉴权完成后就要认证资源的有效性(URL+请求方法),资源的认证主要是基于角色(RBAC)的模式,采用松散匹配模式:即资源的角色列表中只要包含当前用户所拥有的角色,认为当前用户是拥有该资源权限的,具体流程如下:

认证的核心是找当前用户的角色以及角色与资源的匹配。为提高匹配效率,资源拥有的角色在项目启动时进行缓存预热

以上流程有一些细节需要注意:

  • 头部信息 是在鉴权中进行封装,所以有一个过滤器的执行顺序问题,认证过滤器要在鉴权过滤器之后
  • 查找资源 目的是从资源缓存预热中查找对应的资源,对于GET请求需要特别处理,例如当前GET请求的URL为/api/user/1234567(暂时以此为例),要求ID全是数字这样就区分GET /api/user/listAll类似的接口,根据前面功能定义中的约定GET /api/user/{} 把全是数字的替换成{}以获取正确的资源信息
  • 无权限匹配 有些资源是无权限的,在缓存预热时可以将无权限的资源对应角色设置为固定的常量标记,在认证时根据资源所需的角色来判断是否无权限。

异常处理

在以上的功能中都包含异常处理数据返回处理,这是保证框架稳定性的基础,异常做如下处理:

  • 定义通用的异常接口 ,包括编号内容 ,后续所有的异常枚举都实现此接口,按照不同的业务类型分配不同的编号段,例如系统级的异常从0-199999(仅举例,具体以业务为准),不同的业务根据需要也划分不同的段,这样根据对应的号段就能判断出对应的异常种类。
  • 定义通用的异常类,所有异常抛出的类都以此为准,异常类包含异常接口的构造函数,例如 throw new ServerRuntimeException(ExceptionEnum.NO_PRIVILEGE_EXCEPTION)
  • 通用的异常处理 所有的异常最终都转化为通用的异常类。

详情请参考框架依赖 snapper-dependence

java 复制代码
public interface ExceptionEnumable {

    int getExceptionCode();
    
    String getExceptionMessage();

    String name();

    default ExceptionEnumable loadByLang(String v)  {
       try {
          String language = RequestContext.reqLanguage();
          ExceptionEnumable e = null;
          if (Checker.beEmpty(language) || Strings.ZH_CN.equalsIgnoreCase(language)) {
             e = this;
          } else {
             Class clz = Class.forName(this.getClass().getName() + Strings.UNDERSCORE + language);;
             e = (ExceptionEnumable) EnumUtils.getEnum(clz, v);
          }
          if (Checker.beNull(e)) {
             return this;
          }
          return e;
       } catch (Throwable e) {
          throw new RuntimeException(e);
       }
    }
}
java 复制代码
public enum ExceptionEnum implements ExceptionEnumable {

    NO_PRIVILEGE_EXCEPTION(403, "用户没有访问资源的权限!"),
    USER_BE_UNAUTHERIZED(401, "用户权限资源认证失败,没有权限!"),
    CAPTCHA_NOT_CORRECT(995, "验证码错误, 请填写正确验证码!"),
    OBJECT_BE_LOCK_NOT_OPERATE(996, "该信息已被锁定,无法进行此操作!"),
    USER_NAME_NOT_EXIST(999, "用户名不存在!")
    ...
    ;
    
    private int code;

    private String message;
    
    @Override
    public int getExceptionCode() {
        return this.code;
    }

    @Override
    public String getExceptionMessage() {
        return this.message;
    }

    ExceptionEnum(final int code, final String message){
        this.code = code;
        this.message = message;
    }
}
java 复制代码
public  class ServerRuntimeException extends RuntimeException {
    protected int code;
    protected String codeMessage;

    public ServerRuntimeException(ExceptionEnumable exceptionEnum) {
       super(MessageFormat.format(exceptionEnum.loadByLang(exceptionEnum.name()).getExceptionMessage(), new Object[] {"","","",""}));
       codeMessage = super.getMessage();
       this.code = exceptionEnum.getExceptionCode();
    }
    
    public ServerRuntimeException(ExceptionEnumable exceptionEnum, Object... arguments) {
       super(MessageFormat.format(exceptionEnum.loadByLang(exceptionEnum.name()).getExceptionMessage(),
             Checker.beEmpty(arguments) ? new Object[] { "", "", "", "" } : arguments));
       codeMessage = super.getMessage();
       this.code = exceptionEnum.getExceptionCode();
    }

    public ServerRuntimeException(int code, String message) {
       super(message);
       this.code = code;
       this.codeMessage = message;
    }
  
}

数据返回处理

统一的数据返回是将所有的数据都汇总成固定的格式进行返回,包括异常的数据,具体做如下处理:

  • 某些情况下需要返回原始数据,针对此定义一个 @NotWrap 注解,用于控制器方法上,拥有此注解不做统一封装处理,同样可以针对包路径进行处理,只在 snapper.wrap.package定义的包目录下的才会进行处理
  • 私有方法不做处理
  • 返回数据格式 统一为 {code:200, message:"OK", data:[]}格式的数据,data中为对应的业务返回数据

详情请参考:ReturnHandlerAdvice

总结

通过以上的功能设计,完成了权限系统设计最基础的功能设计,后续会逐渐完善其他模块,包括 用户设计角色设计职位设计组织设计 ,还有核心的数据权限设计 , 下一章节将开始用户相关的设计

相关推荐
粘豆煮包4 小时前
脑抽研究生Go并发-1-基本并发原语-下-Cond、Once、Map、Pool、Context
后端·go
IT_陈寒5 小时前
Vite5.0性能翻倍秘籍:7个极致优化技巧让你的开发体验飞起来!
前端·人工智能·后端
Edward.W5 小时前
用 Go + HTML 实现 OpenHarmony 投屏(hdckit-go + WebSocket + Canvas 实战)
开发语言·后端·golang
南囝coding5 小时前
Claude 封禁中国?为啥我觉得是个好消息
前端·后端
六边形工程师5 小时前
Docker安装神通数据库ShenTong
后端
六边形工程师5 小时前
快速入门神通数据库
后端
重生成为编程大王5 小时前
FreeMarker快速入门指南
java·后端
Dear.爬虫5 小时前
Golang的协程调度器原理
开发语言·后端·golang
元闰子6 小时前
怎么用CXL加速数据库?· SIGMOD'25
数据库·后端·面试