用户权限管理系统设计
1. 需求分析
设计一个用户权限管理系统,模型为RBAC(Role-Based Access Control),按用户-角色-权限的层次结构进行权限管理。该项目的主要功能有:
- 权限管理部分
- 用户管理:用户的增删改查
- 角色管理:角色的增删改查,给用户分配角色
- 权限管理:给角色分配权限
- 权限控制部分
- 每次请求都需要校验用户的权限,只有拥有相应权限的用户才能访问相应资源
- 用户的权限状态应当实时更新,比如退出登录、长时间未操作、修改密码等敏感信息、角色或权限变更等情况都应当立即生效并使旧的权限信息失效
- 可以应用于一般项目
2. 技术选型
- 前端
- Vue + ElementUI 用于快速搭建页面
- axios 用于前后端交互
- vuex 用于状态管理
- vue-router 用于路由管理
- 后端
- Spring Boot 用于快速搭建后端
- Spring Security 用于权限控制
- Gateway 用于统一进行权限校验,路由转发
- Eureka 用于服务注册与发现
- JWT 用于用户认证
- MyBatis 用于数据库操作
- MySQL 用于存储数据
- Redis 用于存储用户的token黑名单等信息
- 其他说明
- 上述几个技术栈在本项目中使用的版本为: vue 2.x, jdk 1.8, spring boot 2.x, mysql 8.x, redis 3.x
3. 系统设计
3.1 数据库设计
3.1.1 mysql
包括用户表、角色表、权限表、用户角色关联表、角色权限关联表等,属于比较常规的设计,以下是需要说明的几点:
- 数据库遵循只增不删的原则,即删除操作只做逻辑删除,不做物理删除。所以user和role表中都有一个字段
status
,用于标记是否删除 - 权限表中的数据直接写死,不提供修改的接口,且在后端代码中以常量的形式存在,可以方便使用例如
@PreAuthorize("hasAuthority('XXX')")
这样的注解来进行权限校验 - 考虑到可能会有多个系统,所以在权限表中增加了一个字段
platform
,用于标记权限所属的系统,判断用户是否有权限进入某个系统则是通过判断用户是否有该系统的权限来实现
3.1.2 redis
本项目采用jwt作为用户认证的方式,在快要过期的时候会给前端返回一个新的token,而自然过期的token由于可以直接使用jwt的verify方法来校验,所以不需要存储在redis中。在用户修改密码、权限或角色被修改、用户退出登录等情况下,需要立即使旧的token失效,这时候就需要将旧的token或者其他信息存储在redis中,以便在校验token的时候进行判断。基于此,本项目在redis中存储了以下几个信息:
- token黑名单:
- 存储内容:存储了所有已经失效的token,主要是退出登录的token。
- 存储方式:使用0号库,key为失效的token,value为空字符串
- 过期时间:token的自然过期时间减去当前时间
- 使用方式:在校验token的时候,先判断token是否在黑名单中,如果在则说明用户试图使用一个已经失效的token,直接返回401
- 需要刷新权限的用户:
- 存储内容:存储了所有需要刷新权限的用户的id(修改了密码、管理员修改了用户的角色或权限)
- 存储方式:使用1号库,key为用户id,value为(当前时间+token默认过期时长)
- 过期时间:token默认过期时长
- 使用方式:在校验token的时候,先判断用户是否在需要刷新权限的用户中,如果在则判断当前的token过期时间是否小于value,如果小于则说明该token是在权限更新前签发的,需要重新签发一个新的token给用户
- 刷新token表(可选,当用户一次发送多个请求又恰好需要更换token时,可以避免重复签发token):
- 存储内容:存储了即将过期的token
- 存储方式:使用2号库,key为即将过期的token,value为新的token
- 过期时间:旧token的自然过期时间减去当前时间
- 使用方式:判断用户token是否即将过期,如果是则判断用户的token是否在刷新token表中,如果在则直接将value作为新的token返回给用户,否则生成一个新的token并存储在刷新token表中
3.2 后端设计
系统的整体流程如下图所示:
-
网关gateway会校验并解析用户的token,如果token在3.1.2中的黑名单中则直接返回401,如果user_id在3.1.2中的需要刷新权限的用户中且判断为需要刷新权限则根据具体需求签发新的token给用户或者直接返回401强制用户重新登录。如果token即将过期,则请求依旧传递到下游服务,不过会在请求头中添加新的token并返回给前端。如果token已经过期,则说明用户长时间未登录或长时间未操作,直接返回401强制用户重新登录。校验完token后,gateway会将token解析出来的用户信息传递给下游服务,如果没有token也是传递给下游服务(毕竟有些接口不需要登录就可以访问)。
-
从gateway传递过来的用户信息会最先到达Security的过滤器,在这里会根据用户信息设置权限,设置用户信息上下文,没有登录则设置一个匿名用户。如果匿名用户试图访问不在白名单中的接口则直接返回401。为了提高性能,这些权限都是从上一步的token解析出来的,不需要查询数据库(jwt有签名可以有效防止token中的内容伪造)。
-
在Security的过滤器之后,请求会到达具体的controller,一般的请求默认权限是登录用户可以访问,如果需要允许匿名用户访问则需要在controller或者方法上添加
@PermitAll
注解,或者在Security的配置类中添加http.authorizeRequests().antMatchers("/xxx").permitAll()
。如果需要特定权限才能访问,则需要在controller或者方法上添加@PreAuthorize
注解,例如@PreAuthorize("hasAuthority('XXX')")
。
3.3 前端设计
3.3.1 动态路由
根据用户具体的角色和权限信息来生成对应的路由,不具有特定权限的用户或者游客则访问不到对应的页面。具体的实现方式是在前端请求登录接口成功后,将用户的角色和权限信息存储在vuex中,然后根据这些信息生成对应的路由(路由列表事先写好了各自需要的权限),最后将这些路由添加到vue-router中。这样就可以实现动态路由的功能。 前端项目基于vue-element-admin进行开发,其中已经实现了动态路由的功能,所以这个部分只需要根据具体需求进行修改即可。
3.3.2 权限刷新
用户首次登录时会签发一个token给用户,将token保存在localStorage中,每次请求都会将token放在请求头中(axios的请求拦截器中设置),后端会校验token的合法性。在本项目中约定后端返回401状态码时,前端会清除用户信息并退回到登录页面。返回403状态码时,前端会刷新页面重新获取用户信息保存到vuex中,然后重新发起请求。这样就可以实现权限实时更新的功能。
4. 接入其他系统
本项目的权限管理系统可以应用于一般的项目,源代码见5源代码。如果需要将本项目接入到其他系统中,需要进行以下几个步骤。
4.1 后端
- 依赖项版本核对
- 确保spring boot版本一致
- 确保根目录的pom.xml中各依赖项版本与目标项目一致或尽量接近
- 如果版本没有问题且依赖项没有冲突,则可以将当前管理后端移入目标后端中,否则需要作为一个独立的后端模块运行。
- 将各子模块复制到目标后端项目中
- 将gateway(网关模块),common(公用模块)user(用户管理模块)移入目标后端项目。如果依赖项版本不兼容则忽略此步骤,将本项目作为一个独立的后端模块运行即可。
- 修改网关配置
- 将各子模块都注册到同一个eureka服务,并在gateway中的配置路由转发(当后端版本不兼容时该步骤也可以进行)。具体配置可以参考gateway模块的application.yml文件。
- 修改数据库配置
- 按具体需求调整用户表中的字段,并预置好若干角色和权限。
- 后端的common模块中有一些角色和权限的常量(在enum或constants包中),可以根据具体需求进行修改。
- 配置security
- 使用公共模块common中的security工具类给各子模块配置好过滤器,并使用security的注解(
@PreAuthorize
,@PermitAll
)给每个接口配置权限。
- 使用公共模块common中的security工具类给各子模块配置好过滤器,并使用security的注解(
4.2 前端
- 本项目管理系统前端
- 根据后端的调整,修改api接口的地址和参数
- 根据用户表、角色表、权限表的调整,修改前端src/views/user目录下的用户管理、角色管理、权限管理的页面中的表单结构和表格结构
- 根据权限的调整,修改src/router/index.js中的路由表,在其中的meta中添加权限字段,根据权限动态生成路由
- 接入的其他系统的前端
- 可参考本项目管理前端的方式,根据前端使用的具体技术栈来根据需求选择性的实现动态路由和权限刷新的功能
5. 源代码
本项目的源代码已经上传到github,地址为:https://github.com/lxmghct/user-role-admin