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后, 真的是极大提升学习效率呀.

posted @ 2025-04-28 23:24  致于数据科学家的小陈  阅读(137)  评论(0)    收藏  举报