最近在优化电商后台项目的时候,权限管理这块踩了不少坑。今天就把我的实战经验分享出来,希望能帮到正在做类似需求的朋友们。
前言
做后台管理系统,权限管理几乎是绑死的需求。但说实话,很多教程要么讲得太理论,要么代码不完整跑不起来。
我们这次正好优化了一下 GoFrame 电商项目,做了一套完整的 RBAC 权限系统,从数据库设计到中间件实现,全程实战代码。文章有点长,建议先收藏,慢慢看。
一、先搞清楚 RBAC 是个啥
RBAC 全称是 Role-Based Access Control,翻译过来就是"基于角色的访问控制"。
说人话就是:
用户 不直接拥有权限
用户 拥有 角色
角色 拥有 权限
举个例子:张三是"商品管理员"角色,这个角色有"查看商品"、"编辑商品"的权限,那张三就能操作商品模块。
这样设计的好处是什么?解耦。
你想想,如果直接给用户分配权限,100 个用户就要配 100 次。但如果用角色,只需要配置好角色的权限,然后把角色分给用户就行了。后面权限调整,改角色就行,用户那边自动生效。
二、数据库怎么设计
2.1 四张核心表
RBAC 最少需要这四张表:
2.2 管理员表
CREATE TABLE `admin_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(30) NOT NULL DEFAULT '' COMMENT '用户名', `password` varchar(50) NOT NULL DEFAULT '' COMMENT '密码', `role_ids` varchar(50) NOT NULL DEFAULT '' COMMENT '角色ids', `user_salt` varchar(10) NOT NULL DEFAULT '' COMMENT '加密盐', `is_admin` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否超级管理员', `created_at` datetime NULL DEFAULT NULL, `updated_at` datetime NULL DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `name_unique`(`name`));
复制代码
这里有个设计点要说一下:role_ids 我用的是逗号分隔的字符串,比如 "1,2,3" 表示这个用户有 1、2、3 三个角色。
有人可能会说,这不符合数据库范式啊,应该再建一张 admin_role 关联表。
确实,从规范性来说应该这么做。但实际项目中,一个管理员的角色数量通常不会太多(一般就 1-3 个),用逗号分隔反而更简单,查询也方便。这就是工程上的取舍。
2.3 角色表
CREATE TABLE `role_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL DEFAULT '' COMMENT '角色名称', `desc` varchar(255) NOT NULL COMMENT '描述', `created_at` datetime NULL DEFAULT NULL, `updated_at` datetime NULL DEFAULT NULL, `deleted_at` datetime NULL DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `unique_index`(`name`));
复制代码
角色表很简单,就是名称和描述。注意有个 deleted_at 字段,这是软删除,删除的时候不是真删,而是标记一下。
2.4 权限表
CREATE TABLE `permission_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(30) NOT NULL DEFAULT '' COMMENT '权限名称', `path` varchar(100) NOT NULL DEFAULT '' COMMENT '路径', `created_at` datetime NULL DEFAULT NULL, `updated_at` datetime NULL DEFAULT NULL, `deleted_at` datetime NULL DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `unique_name`(`name`));
复制代码
重点是 path 字段,这个存的是 API 路径前缀。比如存 /backend/goods,那么 /backend/goods/list、/backend/goods/add 这些接口都会被这个权限覆盖。
2.5 角色-权限关联表
CREATE TABLE `role_permission_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `role_id` int(11) NOT NULL DEFAULT 0 COMMENT '角色id', `permission_id` int(11) NOT NULL COMMENT '权限id', `created_at` datetime NULL DEFAULT NULL, `updated_at` datetime NULL DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `unique_index`(`role_id`, `permission_id`));
复制代码
这是个多对多的关联表,一个角色可以有多个权限,一个权限也可以分给多个角色。
2.6 表关系图
画个简单的关系图:
┌─────────────┐ ┌─────────────────────┐ ┌─────────────────┐│ admin_info │ │ role_permission_info│ │ permission_info │├─────────────┤ ├─────────────────────┤ ├─────────────────┤│ id │ │ id │ │ id ││ name │ │ role_id ─────┼───┐ │ name ││ password │ │ permission_id ─────┼───┼───│ path ││ role_ids ───┼───┐ └─────────────────────┘ │ └─────────────────┘│ is_admin │ │ │└─────────────┘ │ ┌─────────────┐ │ │ │ role_info │ │ └──►│ id ◄────┼───────────┘ │ name │ │ desc │ └─────────────┘
复制代码
三、核心代码实现
3.1 登录时把角色信息塞进 JWT
登录的时候,不能只存用户 ID,还要把 is_admin 和 role_ids 一起存进 JWT Token 里。
为什么?因为后面每次请求都要校验权限,如果每次都去数据库查用户的角色信息,性能会很差。存在 JWT 里,解析 Token 就能拿到,省一次数据库查询。
// 登录验证并返回包含角色信息的数据(用于JWT存储)func (s *sAdmin) GetAdminByNamePasswordWithRoles(ctx context.Context, in model.UserLoginInput) map[string]interface{} { adminInfo := entity.AdminInfo{} err := dao.AdminInfo.Ctx(ctx).Where("name", in.Name).Scan(&adminInfo) if err != nil { return nil } // 验证密码 if utility.EncryptPassword(in.Password, adminInfo.UserSalt) != adminInfo.Password { return nil } // 返回的数据会被存入 JWT return g.Map{ "id": adminInfo.Id, "username": adminInfo.Name, "is_admin": adminInfo.IsAdmin, // 是否超级管理员 "role_ids": adminInfo.RoleIds, // 角色ID列表 }}
复制代码
3.2 权限校验中间件(核心中的核心)
这是整个权限系统最核心的部分,每个需要鉴权的请求都会经过这个中间件:
// 超管专属路径(权限管理模块本身)var adminOnlyPaths = []string{ "/backend/role", "/backend/permission", "/backend/admin", "/backend/user",}
// 不需要权限校验的路径var noPermissionCheckPaths = []string{ "/backend/login", "/backend/logout", "/backend/admin/create", "/backend/refresh-token",}
// PermissionCheck 权限校验中间件func (s *sMiddleware) PermissionCheck(r *ghttp.Request) { ctx := r.Context() requestPath := r.URL.Path
// 1. 白名单路径直接放行 for _, path := range noPermissionCheckPaths { if strings.HasPrefix(requestPath, path) { r.Middleware.Next() return } }
// 2. 从 JWT 中获取用户信息 claims := jwt.ExtractClaims(ctx) if claims == nil { response.JsonExit(r, 401, "未登录或登录已过期") return }
isAdmin := gconv.Int(claims["is_admin"]) roleIdsStr := gconv.String(claims["role_ids"])
// 3. 超级管理员直接放行,不用校验 if isAdmin == 1 { r.Middleware.Next() return }
// 4. 普通管理员不能访问权限管理模块 for _, adminPath := range adminOnlyPaths { if strings.HasPrefix(requestPath, adminPath) { response.JsonExit(r, 403, "权限不足,该功能仅超级管理员可访问") return } }
// 5. 解析 role_ids var roleIds []int if roleIdsStr != "" { roleIdStrs := strings.Split(roleIdsStr, ",") for _, idStr := range roleIdStrs { idStr = strings.TrimSpace(idStr) if idStr != "" { roleIds = append(roleIds, gconv.Int(idStr)) } } }
// 6. 没有角色 = 没有权限 if len(roleIds) == 0 { response.JsonExit(r, 403, "权限不足,未分配角色") return }
// 7. 根据角色查询权限路径 allowedPaths, err := service.Permission().GetPathsByRoleIds(ctx, roleIds) if err != nil { response.JsonExit(r, 500, "权限校验失败") return }
// 8. 前缀匹配 for _, allowedPath := range allowedPaths { if strings.HasPrefix(requestPath, allowedPath) { r.Middleware.Next() return } }
// 9. 没有匹配到任何权限 response.JsonExit(r, 403, "权限不足,无法访问该功能")}
复制代码
代码有点长,但逻辑其实很清晰,我画个流程图:
请求进来 │ ▼是白名单路径? ──是──► 放行 │ 否 ▼从 JWT 提取 is_admin 和 role_ids │ ▼is_admin == 1? ──是──► 放行(超管无敌) │ 否 ▼是超管专属路径? ──是──► 403 拒绝 │ 否 ▼解析 role_ids,查询权限路径 │ ▼请求路径匹配权限路径? ──是──► 放行 │ 否 ▼403 拒绝
复制代码
3.3 根据角色查询权限路径
// GetPathsByRoleIds 根据角色ID列表获取所有权限路径func (s *sPermission) GetPathsByRoleIds(ctx context.Context, roleIds []int) ([]string, error) { if len(roleIds) == 0 { return []string{}, nil }
// 1. 查询角色-权限关联表 var rolePermissions []entity.RolePermissionInfo err := dao.RolePermissionInfo.Ctx(ctx). WhereIn(dao.RolePermissionInfo.Columns().RoleId, roleIds). Scan(&rolePermissions) if err != nil { return nil, err }
if len(rolePermissions) == 0 { return []string{}, nil }
// 2. 提取 permission_ids 并去重 permissionIdMap := make(map[int]bool) for _, rp := range rolePermissions { permissionIdMap[rp.PermissionId] = true } permissionIds := make([]int, 0, len(permissionIdMap)) for id := range permissionIdMap { permissionIds = append(permissionIds, id) }
// 3. 查询权限表获取 path var permissions []entity.PermissionInfo err = dao.PermissionInfo.Ctx(ctx). WhereIn(dao.PermissionInfo.Columns().Id, permissionIds). Scan(&permissions) if err != nil { return nil, err }
// 4. 提取所有 path paths := make([]string, 0, len(permissions)) for _, p := range permissions { if p.Path != "" { paths = append(paths, p.Path) } }
return paths, nil}
复制代码
四、路由怎么配置
GoFrame 的路由配置还是挺优雅的,用 Group 分组,然后绑定中间件:
// 管理后台路由组s.Group("/backend", func(group *ghttp.RouterGroup) { group.Middleware( service.Middleware().CORS, service.Middleware().Ctx, service.Middleware().ResponseHandler, ) // 不需要登录的接口 group.Bind( controller.Admin.Create, // 管理员创建 controller.Login.Login, // 登录 controller.Login.RefreshToken, // 刷新Token ) // 需要登录 + 权限校验的接口 group.Group("/", func(group *ghttp.RouterGroup) { group.Middleware( service.Middleware().Auth, // JWT 认证 service.Middleware().PermissionCheck, // 权限校验 ) group.Bind( controller.Role, // 角色管理 controller.Permission, // 权限管理 controller.Admin.List, controller.Admin.Update, controller.Admin.Delete, // ... 其他接口 ) })})
复制代码
五、实际使用
5.1 创建权限
POST /backend/permission/add{ "name": "商品管理", "path": "/backend/goods"}
复制代码
5.2 创建角色并分配权限
# 创建角色POST /backend/role/add{ "name": "商品管理员", "desc": "负责商品相关管理"}
# 批量添加权限POST /backend/role/add/permissions{ "role_id": 2, "permission_ids": [1, 2, 3]}
复制代码
5.3 创建管理员并分配角色
POST /backend/admin/add{ "name": "zhangsan", "password": "123456", "role_ids": "2,3", "is_admin": 0}
复制代码
六、几个注意事项
超级管理员:is_admin = 1 的用户拥有所有权限,不受任何限制。建议只给老板或者核心开发人员。
路径匹配是前缀匹配:权限路径 /backend/goods 会匹配 /backend/goods/list、/backend/goods/add 等所有以它开头的路径。
JWT 存储角色信息:登录时把 is_admin 和 role_ids 存入 JWT,避免每次请求都查数据库。
权限管理模块本身只有超管能访问:这是为了安全,普通管理员不能给自己加权限。
七、写在最后
这套权限系统是我在做 GoFrame 电商后台项目时实现的,除了权限管理,还包括商品管理、订单管理、用户管理、数据统计等完整功能模块。
如果你正在学习 GoFrame,或者想找一个完整的后台项目参考,可以看看我的这个项目。代码结构清晰,注释也比较完整,应该能帮你少走一些弯路。
项目地址:https://mp.weixin.qq.com/s/jNspWJrXq3pu7u9AZkS8iw):https://mp.weixin.qq.com/s/jNspWJrXq3pu7u9AZkS8iw
觉得有帮助的话,点个赞、收藏一下呗~ 有问题欢迎评论区交流!
评论