Gin RBAC 权限基础实现
RBAC (基于角色的访问控制) 是一种广泛应用的权限管理模型, 通过 角色 将 用户 和 权限 解耦, 简化权限分配管理.
- 用户 (User): 系统的使用者
- 权限 (Permission): 对资源 (页面, 按钮, API) 的增删改查许可
- 角色 (Role): 权限的集合
- 会话 (Session): 用户激活角色的临时上下文
它的层级按复杂度又分为 基础模型, 角色集成, 约束模型, 完整模型等, 在企业后台管理系统, 如 OA, ERP, SaaS 等应用广泛.
这里还是以上次的项目, 来进行一个类似商城后台相关的权限设计实现 (RBAC0).
用户表
drop table if exists `users`;
create table `manager` (
id int not null auto_increment comment '自增id',
user_id varchar(36) not null comment '对外暴露的uuid',
role_id int not null default 0 comment '角色',
is_super tinyint not null default 0 comment '是否为超级管理员',
status tinyint not null default 0 comment '0禁用, 1启用',
username varchar(50) unique not null comment '用户名',
password varchar(255) not null comment '用户哈希密码',
avatar varchar(255) default null comment '头像地址',
create_time datetime default current_timestamp comment '创建时间',
update_time datetime default current_timestamp on update current_timestamp comment '更新时间',
primary key (`id`)
);
-- 测试数据
insert into manager (`user_id`, `role_id`, `is_super`, `status`, `username`, `password`, `avatar`)
values ('abcde', 1, 0, 0, 'admin', 'admin123', null);
insert into manager (`user_id`, `role_id`, `is_super`, `status`, `username`, `password`, `avatar`)
values ('abcde', 1, 0, 0, 'admin2', 'admin123', null);
insert into manager (`user_id`, `role_id`, `is_super`, `status`, `username`, `password`, `avatar`)
values ('abcde', 1, 0, 0, 'admin3', 'admin123', null);
角色表
drop table if exists role;
create table role (
id int not null auto_increment comment '自增id'
, name varchar(50) unique not null comment '角色名称'
, descr varchar(255) default null comment '角色描述'
, create_time datetime default current_timestamp comment '创建时间'
, update_time datetime default current_timestamp on update current_timestamp comment '更新时间'
, primary key (`id`)
);
-- 测试数据
insert into role (`name`, `descr`) values ('超级管理员', '超级管理员');
insert into role (`name`, `descr`) values ('普通管理员', '普通管理员');
insert into role (`name`, `descr`) values ('普通用户', '普通用户');
权限表
drop table if exists rule;
create table rule(
id int not null auto_increment primary key comment '自增id'
, parent_id int default 0 comment '父级id'
, status tinyint default 1 comment '0关闭, 1启用'
, name varchar(50) default null comment '权限菜单名称'
, descr varchar(50) default null comment '前端路由名称'
, frontpath varchar(50) default null comment '前端路由注册路径'
, cond varchar(50) default null comment '菜单下的功能描述'
, menu tinyint default 1 comment '菜单或权限, 1菜单0权限'
, orders int default 50 comment '排序'
, icon varchar(100) default null comment '图标路径'
, method varchar(50) not null default 'GET' comment '请求方式'
, create_time datetime default current_timestamp
, update_time datetime default current_timestamp on update current_timestamp
);
-- 测试数据
-- 一级菜单, 后台面板, index
insert into `rule`(`parent_id`, `status`, `name`, `descr`, `frontpath`, `cond`, `menu`, `orders`, `icon`, `method`)
values (0, 1, '后台面板', 'index', null, null, 1, 1, 'help', 'GET');
-- 二级菜单, 后台面板/主控台, index, /
insert into `rule`(`parent_id`, `status`, `name`, `descr`, `frontpath`, `cond`, `menu`, `orders`, `icon`, `method`)
values (1, 1, '主控台', 'index', '/', null, 1, 20, 'home-filled', 'GET');
-- 二级菜单, 后台面板/bi看板, index, /
insert into `rule`(`parent_id`, `status`, `name`, `descr`, `frontpath`, `cond`, `menu`, `orders`, `icon`, `method`)
values (1, 1, 'bi看板', 'index', '/bi', null, 1, 20, 'home-filled', 'GET');
-- 一级菜单, 商品管理
insert into `rule`(`parent_id`, `status`, `name`, `descr`, `frontpath`, `cond`, `menu`, `orders`, `icon`, `method`)
values (0, 1, '商品管理', 'goods', null, null, 1, 2, 'shop-bag', 'GET');
-- 二级菜单, 商品模块/商品列表, /goods/list
insert into `rule`(`parent_id`, `status`, `name`, `descr`, `frontpath`, `cond`, `menu`, `orders`, `icon`, `method`)
values (4, 1, '商品列表', 'index', '/goods/list', null, 1, 20, 'home-filled', 'GET');
-- 二级菜单, 商品模块/分类管理, /category/list
insert into `rule`(`parent_id`, `status`, `name`, `descr`, `frontpath`, `cond`, `menu`, `orders`, `icon`, `method`)
values (4, 1, '分类管理', 'index', '/category/list', null, 1, 20, 'aim', 'GET');
-- 功能权限, 通过 menu = 0 区分
insert into `rule`(`parent_id`, `status`, `name`, `descr`, `frontpath`, `cond`, `menu`, `orders`, `icon`, `method`)
values (4, 1, '新增商品分类', 'goods', null, 'createCategory', 0, 20, null, 'GET');
insert into `rule`(`parent_id`, `status`, `name`, `descr`, `frontpath`, `cond`, `menu`, `orders`, `icon`, `method`)
values (4, 1, '获取商品分类', 'goods', null, 'getCategory', 0, 40, null, 'GET');
角色-权限表
drop table if exists `role_rule`;
create table `role_rule`(
`id` int not null auto_increment primary key comment '自增id'
, `role_id` int default null comment '角色id'
, `rule_id` int default null comment '权限id'
, create_time datetime default current_timestamp
, update_time datetime default current_timestamp on update current_timestamp
);
-- 测试数据
insert into `role_rule`(role_id, rule_id) values (2, 1), (3, 1);
insert into role_rule(role_id, rule_id) values (2, 4), (2, 1);
查询角色-菜单权限
-- 查询角色-菜单权限
select
parent.id as pid
, parent.name as p_name
, coalesce(parent.icon, '') as p_icon
, child.id
, coalesce(child.icon, '') as c_icon
, child.status
, child.name
, coalesce(child.descr, '') as descr
, child.frontpath
, coalesce(child.cond, '') as cond
, child.menu
, child.orders
, coalesce(child.method, '') as method
, child.create_time
from role_rule as rr
join rule as child on rr.rule_id = child.parent_id
join rule as parent on child.parent_id = parent.id
where rr.role_id = 2
and child.menu = 1
order by parent.parent_id
查询角色-功能操作权限
-- 操作权限
select
b.cond,
b.method
from role_rule a
inner join rule b on a.rule_id = b.parent_id
where a.role_id = 2
and b.parent_id is not null
and menu = 0
Gin 从JWT令牌解析出当前用户及角色ID存上下文
pkg/jwtt/jwt.go
type Claims struct {
UID int `json:"uid"`
RoleID int `json:"role_id"`
Username string `json:"username"`
Password string `json:"password"`
jwt.RegisteredClaims // 嵌入标准声明
}
Middleware/auth.go
// 从令牌中提取当前用户的 uid, roleId 作为上下文
c.Set("uid", claims.UID)
c.Set("roleId", claims.RoleID)
Gin 从中间件中获取当前用户及角色ID
utils/tools.go
package utils
import (
"errors"
"github.com/gin-gonic/gin"
)
// 从中间件上下文获取当前用户的 uid, roleId, 类型是 interface
func GetContextUser(c *gin.Context) (int, int, error) {
// 先获取接口数据, 再通过断言解析
uidInterface, ok := c.Get("uid")
if !ok {
return 0, 0, errors.New("用户认证失败")
}
uid, ok := uidInterface.(int)
if !ok {
return 0, 0, errors.New("用户认证失败")
}
// roleId
roleIdInterface, ok := c.Get("roleId")
if !ok {
return 0, 0, errors.New("用户权限不足")
}
roleId, ok := roleIdInterface.(int)
if !ok {
return 0, 0, errors.New("用户权限解析失败")
}
return uid, roleId, nil
}
Gin 获取用户及其权限接口实现
api/routers.go
package api
import (
"github.com/gin-gonic/gin"
"youge.com/api/handlers/auth"
"youge.com/api/handlers/user"
"youge.com/internal/middleware"
)
// 统一注册入口
func RegisterAllRouters(r *gin.Engine) {
// 登录认证模块
authGroup := r.Group("/api/auth")
{
// auth.POST("/register", Register)
authGroup.POST("/login", auth.Login)
}
// 用户管理模块
userGroup := r.Group("api/user")
// 需要 token 认证的哦
userGroup.Use(middleware.JWT())
{
userGroup.GET("/getinfo", user.GetAuthInfo)
}
}
api/handlers/user/menu.go
package user
import (
"time"
"github.com/gin-gonic/gin"
"youge.com/internal/db"
"youge.com/pkg/utils"
)
// 用户表
type User struct {
UID int `db:"id" json:"id"`
UserID string `db:"user_id" json:"user_id"`
RoleID int `db:"role_id" json:"role_id"`
IsSuper int `db:"is_super" json:"is_super"`
Status int `db:"status" json:"status"`
Username string `db:"username" json:"username"`
Password string `db:"password" json:"-"`
Avatar string `db:"avatar" json:"avatar"`
}
// 用户菜单权限, 数据集查询记录
type MenuItem struct {
PID int `db:"pid" json:"pid"`
PName string `db:"p_name" json:"p_name"`
PIcon string `db:"p_icon" json:"p_icon"`
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Status int `db:"status" json:"status"`
Cicon string `db:"c_icon" json:"c_icon"`
FrontPath string `db:"frontpath" json:"frontpath"`
Descr string `db:"descr" json:"descr"`
Cond string `db:"cond" json:"cond"`
Menu int `db:"menu" json:"menu"`
Orders int `db:"orders" json:"orders"`
Method string `db:"method" json:"method"`
CreatedAt time.Time `db:"create_time" json:"create_time"`
}
// 子菜单
type Child struct {
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Status int `db:"status" json:"status"`
Cicon string `db:"c_icon" json:"c_icon"`
FrontPath string `db:"frontpath" json:"frontpath"`
Descr string `db:"descr" json:"descr"`
Cond string `db:"cond" json:"cond"`
Menu int `db:"menu" json:"menu"`
Orders int `db:"orders" json:"orders"`
Method string `db:"method" json:"method"`
CreatedAt time.Time `db:"create_time" json:"create_time"`
}
// 返回给前端结构
type FrontendMenu struct {
PID int `json:"pid"`
PName string `json:"p_name"`
PIcon string `json:"p_icon"`
Children []Child `json:"children"` // 子菜单集合
}
// 菜单下的功能权限数据
type Condition struct {
Cond string `db:"cond" json:"cond"`
Method string `db:"method" json:"method"`
}
func buildMenuTree(menus []MenuItem) []FrontendMenu {
// 双索引结构:顺序保持 + 快速查找
orderedPIDs := make([]int, 0, 10) // 保持PID出现顺序
menuMap := make(map[int]*FrontendMenu) // 快速查找父节点
for _, item := range menus {
// 过滤无效数据, sql 层面其实已经处理了
if item.ID == 0 || item.PID <= 0 {
continue
}
// 初始化父容器(利用SQL的ORDER BY保证顺序)
if _, exist := menuMap[item.PID]; !exist {
menuMap[item.PID] = &FrontendMenu{
PID: item.PID,
PName: item.PName,
PIcon: item.PIcon,
Children: make([]Child, 0, 5), // 根据实际子项数调整
}
// 记录首次出现的PID顺序
orderedPIDs = append(orderedPIDs, item.PID)
}
// 添加子项
menuMap[item.PID].Children = append(
menuMap[item.PID].Children,
Child{
ID: item.ID,
Name: item.Name,
Status: item.Status,
Cicon: item.Cicon,
FrontPath: item.FrontPath,
Descr: item.Descr,
Cond: item.Cond,
Menu: item.Menu,
Orders: item.Orders,
Method: item.Method,
CreatedAt: item.CreatedAt,
},
)
}
// 按SQL原始顺序重组结果
tree := make([]FrontendMenu, 0, len(orderedPIDs))
for _, pid := range orderedPIDs {
if menu, exists := menuMap[pid]; exists {
tree = append(tree, *menu)
}
}
return tree
}
func getMenuData(roleId int) ([]MenuItem, error) {
// 将查询数据封装
var menus []MenuItem
mysql := `
select
parent.id as pid
, parent.name as p_name
, coalesce(parent.icon, '') as p_icon
, child.id
, coalesce(child.icon, '') as c_icon
, child.status
, child.name
, coalesce(child.descr, '') as descr
, child.frontpath
, coalesce(child.cond, '') as cond
, child.menu
, child.orders
, coalesce(child.method, '') as method
, child.create_time
from role_rule as rr
join rule as child on rr.rule_id = child.parent_id
join rule as parent on child.parent_id = parent.id
where rr.role_id = ?
and child.menu = 1
order by parent.parent_id
`
err := db.Query(&menus, mysql, roleId)
if err != nil {
return nil, err // 没数据, 返回空切片和错误
}
return menus, nil // 有数据, 返回数据和 nil 错误
}
func getRules(roleId int) ([]Condition, error) {
mysql := `
select
b.cond,
b.method
from role_rule a
inner join rule b on a.rule_id = b.parent_id
where a.role_id = ?
and b.parent_id is not null
and menu = 0
`
var conditions []Condition
err := db.Query(&conditions, mysql, roleId)
if err != nil {
return nil, err
}
return conditions, nil
}
func GetUser(uid int) (User, error) {
var user User
mysql := `
select
id
, user_id
, role_id
, is_super
, status
, username
, password
, coalesce(avatar, '') as avatar
from manager
where id= ?;
`
err := db.Get(&user, mysql, uid)
if err != nil {
return User{}, err // 失败时, 返回空用户和错误
}
return user, nil // 成功时, 返回用户信息和 nil
}
// 统一整合返回
func GetAuthInfo(c *gin.Context) {
// 01 获取当前用户的 uid, roleId
uid, roleId, err := utils.GetContextUser(c)
if err != nil {
utils.Error(c, 401, "", err)
return
}
// 02 获取用户信息
user, err := GetUser(uid)
if err != nil {
utils.BadRequest(c, "用户不存在")
return
}
// 03 获取菜单权限
menus, err := getMenuData(roleId)
if err != nil {
utils.Error(c, 400, "获取菜单数据失败")
return
}
// 将 flat menus 数据按 pid 转为 menusTree
menusTree := buildMenuTree(menus)
// 04 获取菜单下功能权限
conditions, err := getRules(roleId)
if err != nil {
utils.Error(c, 400, "获取菜单下的功能权限失败")
}
// 处理切片为 ["createUser,POST", "getUsers,GET", ....]
ruleList := make([]string, len(conditions))
for i, item := range conditions {
ruleList[i] = (item.Cond + "," + item.Method)
}
// 05 统一处理返回
utils.Success(c, gin.H{
"user": user,
"menus": menusTree,
"rules": ruleList,
})
}
还算是比较通用的, 里面很多都是用AI辅助编写的, 果然有了AI后, 真的是极大提升学习效率呀.
耐心和恒心, 总会获得回报的.

浙公网安备 33010602011771号