权限系统设计-功能设计

背景

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

总结

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

相关推荐
yuuki2332334 小时前
【C语言】文件操作(附源码与图片)
c语言·后端
IT_陈寒4 小时前
Python+AI实战:用LangChain构建智能问答系统的5个核心技巧
前端·人工智能·后端
无名之辈J4 小时前
系统崩溃(OOM)
后端
码农刚子4 小时前
ASP.NET Core Blazor简介和快速入门 二(组件基础)
javascript·后端
间彧4 小时前
Java ConcurrentHashMap如何合理指定初始容量
后端
catchadmin4 小时前
PHP8.5 的新 URI 扩展
开发语言·后端·php
少妇的美梦4 小时前
Maven Profile 教程
后端·maven
白衣鸽子5 小时前
RPO 与 RTO:分布式系统容灾的双子星
后端·架构
Jagger_5 小时前
SOLID原则与设计模式关系详解
后端
间彧5 小时前
Java: HashMap底层源码实现详解
后端