引言
工作 8年有余,基本都在做后端业务,期间搭建过十几个业务系统,总是要接触账户、权限的问题,深知账号的管理问题、数据安全问题的重要性,最佳的实现方式:服务化,尤其是在业务开展初期便需要搭建,否则后期公司业务变多,审计介入就会陷入改不动,理不清的技术困境,那是在做数据治理基本无望,而面对突发的安全风险就只能打补丁,这属实让人难以接受;
根据个人经验来看, 传统的 RBAC 即能满足 80% 的需求,跟部门关联就能满足 99% 的业务需求,于是就搭建一个权限项目以便参考。
技术栈
- 后端:
- 前端:
- 语言:JavaScript
- 框架:vue-element-admin
- UI库:Element UI
- 数据存储:
- 缓存:Redis
- 缓存:Redis
- 安全性:
- 认证和授权:JWT (JSON Web Tokens) 2
- 数据加密:
- 部署和运维:
- 服务器:Ubuntu 20.04 WSL
- 容器化:Docker
- 自动化部署:[自动化部署工具]
- 其他:
- 日志记录:Yii Logger
- 后端测试框架:Yii.Codeception3
- 前端测试框架: Vue Test Utils
因为前端框架引入的 axios 版本过低存在安全风险,应升级,需注意不要升级到 IDE 建议的 0.21.2,会导致拦截器只能生效一个,建议大于该版本,我是 0.27.2
数据库设计
ER 图

说明
文章有对应资源下载的链接,可详细查看对应字段设计;
针对 department 的表设计初衷主要有两点
- 第一点满足业务需求,因为通常数据权限最小颗粒度为 API 级别,而往往一线业务特性比较复杂,例如 GET /users (获取用户列表),正常是获取绑定他的数据,可如果业务存在层级关系(必然),主管/经理需要额外获取自己组下的人员信息,此时该表就体现作用,方便递归查询对应组内成员的数据4;
- 第二点是统筹管理,因为现在的企业基本都要对接 钉钉、企业微信等,而他们(hr)会设置对应组织架构,当触发上述行为通过 webhook 方式对应创建我们服务的数据,反之亦然,从而保持数据的一致性,避免乌龙。
针对 admin_rules 表的设计中重点如下
- 权限应当支持 菜单、按钮、数据权限(API)维度,因为服务化,则新增 project 项目的概念;
- 对接前端只是给 标识符、名称、排序(当前没用),至于其他的体验下来是没有必要的,因为在路由匹配规则是前端为主,来跟后端数据匹配然后替换;
- 这里需要特别注意此表没有唯一键判定,业务中也没有,即该数据权限(API) 理应包含在每一个菜单下,从而解决配置不清晰的问题;
- 数据权限新增敏感等级,也是为后续数据治理,风控做准备,设置 5级,如下图所示。
| 等级 | 常规数据 | 去标识化个人数据 | 敏感数据 | 机密数据 | 查询 | 采取措施 |
|---|---|---|---|---|---|---|
| 0级 | √ | × | × | × | √ | 日志记录 |
| 1级 | √ | × | × | × | × | 日志记录、API 风控 |
| 2级 | √ | × | × | 日志记录、API 风控 | ||
| 3级 | √ | × | √ | 日志记录、API 风控、数据审计 | ||
| 4级 | √ | × | × | 日志记录、API 风控、数据审计 | ||
| 5级 | √ | 日志记录、API 风控、数据审计、数据审批 |
去标识化:通过对个人信息的技术处理,使其在不借助额外信息的情况下,无法识别的个人信息,或者关联个人信息主体的过程,而现在注册都要手机号 (实名), 所以我们自身是具备溯源的能力,所以只能叫做去标识化。
敏感数据分为两个类别: 个人信息、公司信息;
个人信息定义敏感:可以根据能否溯源到个人来定义,例如手机号、身份证号、账号&密码、照片、地址等等,也可以根据数量来定义,例如一次获取 X 条数据。
公司信息: 可以根据涉及公司核心业务来定义,例如详细销售报表,部分密钥,部分账号等,具体应当公司部门来指定;
时序图

①:setToken
如果下载 vue-template 基础模板 需要切换 permission-control 分支
解决 token 存取问题,使用 LocalStorage 持久化保存 token,使用 store 解决 Axios 请求自定义 Header 头的处理。
关于关闭浏览器是否需要重新登录的问题,而所谓的安全问题应当 ip 限制(即 VPN)来解决, 而不能过于强调 ”所谓的安全“ 来牺牲用户体验。
相关代码如下
// user.js
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
// 方便 store 进行更新
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// request.js
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
timeout: 5000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// 授权验证
if (store.getters.token) {
config.headers['Authorization'] = 'Bearer ' + store.getters.token
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
② checkAccess (后端)
读取缓存,具体可以看 ③ 返回权限数据 里说明的存取结构说明。
相关代码
public function checkAccess(): bool
{
$path = Yii::$app->requestedRoute;
if(str_ends_with($path, '/index')) {
$path = substr($path, 0, -strlen('/index'));
}
$uniKey = Yii::$app->request->method. ' /'. $path;
if(isset(self::$whiteList[$uniKey])) {
return true;
}
$roles = User::getRoles();
// 默认 admin 拥有所有权限
if(in_array('admin', $roles)) {
return true;
}
$redis = Has::t()->redis;
$rules = [];
foreach ($roles AS $v) {
$tempRules = $redis->get(PrefixWrapper::getRole(APP_NAME, $v, AdminRule::RULE_TYPE_API));
if($tempRules) {
$rules = array_merge($rules, json_decode($tempRules, true));
}
}
$rules = array_flip($rules);
return isset($rules[$uniKey]);
}
因为采取 Restful 的请求方式,会存在 path 存在参数,即 PATCH user/1 ,所以这里是调用Yii::$app->requestedRoute 方法, 从而获取 user/update(接合 method ,就变成我配置的 API 路径),从而解决二次正则匹配的场景,避免性能问题。
PS: 因为配置 Yii 通过 rules 路由规则匹配到控制器的方法,如果换其他语言其实一样,只是那样可能要写在更外一层了。
后面如果服务化,可以封装下并且实现 redis 连接池,内部服务使用 RPC 调用服务间调用,从而解决网络开销和连接开销。(当然也可以换语言,例如 Go 其实就很好的实现这个,小巧而性能强悍)
③ 返回权限数据
这里主要说明下, 用户的权限数据应当从 Redis 获取到的,而并非读取表来获取,从而提升性能;
对应缓存设计应当选用 K-V 结构即可,存储 Key 格式应当遵循 project:table:project-role:type, 这里 project-role 和 type 是项目和角色的关联和类型,是为了控制 value 的大小,避免性能问题,如下图所示。

的返回示例, 这里是没有采用递归处理的,这样前端反而会好算一些。
{
"code": 200,
"msg": "成功",
"data": {
"menu": [
{
"name": "account-manage",
"title": "账号管理",
"sort": 0
},
{
"name": "account",
"title": "员工管理",
"sort": 0
},
{
"name": "role",
"title": "角色管理",
"sort": 0
},
{
"name": "department",
"title": "部门管理",
"sort": 0
},
{
"name": "rule",
"title": "权限管理",
"sort": 0
},
{
"name": "personal-center",
"title": "个人中心",
"sort": 0
}
],
"list": [],
"button": [
"rule-add"
]
}
}
④加载路由配置
当获取后端吐出的 rules,将原本从 roles 的判定改为 rules 的判定, 相关代码如下所示。
modules\permission.js 代码
export function filterAsyncRoutes(routes, rules) {
const res = []
// 循环第一个数组
routes.forEach(route => {
const tmp = { ...route }
// 查找 routes 具有相同 Name 值的元素 即 Name
const matchingElement = rules.find(item => item.name === tmp.name)
// 如果找到匹配的元素,将其添加到匹配元素数组中
if (matchingElement) {
tmp.sort = matchingElement.sort
if (tmp.meta) {
tmp.meta.title = matchingElement.title
}
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, rules)
}
res.push(tmp)
}
})
return res
}
另外调用父级需要额外判定是否为 admin 角色, 该 store 是获取用户信息的时候存取的。
const actions = {
generateRoutes({ commit }, rules) {
return new Promise(resolve => {
let accessedRoutes = asyncRoutes
// 判断是否为 admin 角色
if (!store.state.user.roles.includes('admin')) {
accessedRoutes = filterAsyncRoutes(asyncRoutes, rules)
accessedRoutes.push({ path: '*', redirect: '/404', hidden: true })
}
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
⑤: v-auth 按钮权限的实现
在 directive 目录,生成相关代码
/**
* @description 鉴权指令
* 判断当前按钮权限是否存在,
* 当传入的权限当前用户没有时,会移除该组件
* 用例:<Tag v-auth="'user-button'">text</Tag>
* */
import store from '@/store'
export default {
inserted(el, binding) {
const { value } = binding
const buttons = store.state.user.buttons
const roles = store.state.user.roles
if (value && value.length && buttons && buttons.length && !roles.includes('admin')) {
const isPermission = buttons.includes(value)
if (!isPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
}
}
}
相关页面展示




项目部署
TODO 待定,想开发 go 的后台后,在统一打包分享。
编程语言可以平替,尤其涉及账号中台服务建议切换 Java、Go,只是提者图方便# 使用技术栈 ↩︎
lcobucci/jwt: 解决 token 生成 ↩︎
Yii的单元测试框架 Codeception 基于 PHPUnit,Codeception 建议遵从 PHPUnit 的文档的进行开发: ↩︎
因为业务需求很灵活,而从数据敏感程度上二者区别不大,如果新增 API 就会造成前端和后端额外的工作量,也增加测试成本,属实没必要。 ↩︎
文章讲述了作者在后端开发中如何通过服务化和RBAC模型进行账号权限管理,包括使用PHP、Yii2、JWT等技术栈,强调了数据库设计、去标识化和数据权限分级的重要性,以及如何优化部署和前后端协作流程。

被折叠的 条评论
为什么被折叠?



