在Golang开发的后端服务中,用户权限控制是保护系统资源、规范用户操作的重要功能,合理的权限设计可以避免越权访问、数据泄露等安全问题。主流的权限实现方案里,RBAC(基于角色的访问控制)是最常用的选择,它通过用户关联角色、角色关联权限的方式,降低权限管理的复杂度,方便后续业务扩展。

权限模型设计
实现RBAC权限控制首先需要设计对应的数据表结构,核心需要四张表:用户表、角色表、权限表、以及两张关联表。下面是MySQL的表结构示例:
-- 用户表 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 角色表 CREATE TABLE `role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `role_name` varchar(50) NOT NULL COMMENT '角色名称', `role_desc` varchar(100) DEFAULT NULL COMMENT '角色描述', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 权限表 CREATE TABLE `permission` ( `id` int(11) NOT NULL AUTO_INCREMENT, `perm_name` varchar(50) NOT NULL COMMENT '权限名称', `perm_path` varchar(100) NOT NULL COMMENT '权限对应的接口路径', `perm_method` varchar(10) NOT NULL COMMENT '接口请求方法', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 用户角色关联表 CREATE TABLE `user_role` ( `user_id` int(11) NOT NULL, `role_id` int(11) NOT NULL, PRIMARY KEY (`user_id`,`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 角色权限关联表 CREATE TABLE `role_permission` ( `role_id` int(11) NOT NULL, `permission_id` int(11) NOT NULL, PRIMARY KEY (`role_id`,`permission_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
核心权限校验逻辑
首先需要定义Golang的结构体对应数据库表,然后编写查询用户权限的方法,判断当前请求的路径和方法是否在用户的权限列表中。
先定义结构体:
package model
// User 用户结构体
type User struct {
ID int
Username string
Password string
}
// Role 角色结构体
type Role struct {
ID int
RoleName string
RoleDesc string
}
// Permission 权限结构体
type Permission struct {
ID int
PermName string
PermPath string
PermMethod string
}接下来编写查询用户权限的方法,这里使用database/sql作为数据库操作示例:
package dao
import (
"database/sql"
"errors"
"your_project/model"
)
// GetUserPermissions 查询用户拥有的所有权限
func GetUserPermissions(db *sql.DB, userId int) ([]model.Permission, error) {
sqlStr := `
SELECT p.id, p.perm_name, p.perm_path, p.perm_method
FROM user u
JOIN user_role ur ON u.id = ur.user_id
JOIN role_permission rp ON ur.role_id = rp.role_id
JOIN permission p ON rp.permission_id = p.id
WHERE u.id = ?
`
rows, err := db.Query(sqlStr, userId)
if err != nil {
return nil, err
}
defer rows.Close()
var permissions []model.Permission
for rows.Next() {
var perm model.Permission
err := rows.Scan(&perm.ID, &perm.PermName, &perm.PermPath, &perm.PermMethod)
if err != nil {
return nil, err
}
permissions = append(permissions, perm)
}
if err = rows.Err(); err != nil {
return nil, err
}
return permissions, nil
}
// CheckPermission 校验用户是否有指定路径和方法的访问权限
func CheckPermission(db *sql.DB, userId int, path string, method string) (bool, error) {
permissions, err := GetUserPermissions(db, userId)
if err != nil {
return false, err
}
if len(permissions) == 0 {
return false, errors.New("用户无权限")
}
for _, perm := range permissions {
if perm.PermPath == path && perm.PermMethod == method {
return true, nil
}
}
return false, nil
}权限校验中间件实现
Golang的Web框架比如Gin、Echo都支持中间件,我们可以在中间件中完成用户身份识别和权限校验,避免在每个接口中重复编写校验逻辑。
下面以Gin框架为例实现权限校验中间件:
package middleware
import (
"net/http"
"your_project/dao"
"your_project/model"
"github.com/gin-gonic/gin"
)
// AuthPermissionMiddleware 权限校验中间件
func AuthPermissionMiddleware(db *sql.DB) gin.HandlerFunc {
return func(c *gin.Context) {
// 从请求头获取用户ID,实际项目中可能是从JWT token中解析
userIdStr := c.GetHeader("X-User-ID")
if userIdStr == "" {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "未登录"})
c.Abort()
return
}
// 转换用户ID为整数,这里省略错误处理
var userId int
// 实际项目中需要解析字符串转int,这里简化示例
// 获取当前请求的路径和方法
reqPath := c.Request.URL.Path
reqMethod := c.Request.Method
// 校验权限
hasPerm, err := dao.CheckPermission(db, userId, reqPath, reqMethod)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "权限校验失败"})
c.Abort()
return
}
if !hasPerm {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "msg": "无访问权限"})
c.Abort()
return
}
// 权限校验通过,继续执行后续逻辑
c.Next()
}
}中间件使用方式
在路由注册时,将权限中间件绑定到需要校验的路由组上即可:
package main
import (
"database/sql"
"your_project/middleware"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// 初始化数据库连接
db, err := sql.Open("mysql", "root:password@tcp(127.0.0.1:3306)/test_db")
if err != nil {
panic(err)
}
defer db.Close()
r := gin.Default()
// 不需要权限校验的公开路由
publicGroup := r.Group("/public")
{
publicGroup.GET("/login", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "登录接口"})
})
}
// 需要权限校验的路由组
authGroup := r.Group("/api")
authGroup.Use(middleware.AuthPermissionMiddleware(db))
{
authGroup.GET("/user/list", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "用户列表"})
})
authGroup.POST("/user/add", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "添加用户"})
})
}
r.Run(":8080")
}注意事项
- 实际项目中用户身份一般通过JWT token传递,中间件需要先解析token获取用户ID,再执行权限校验
- 权限路径匹配可以支持通配符,比如
/api/user/*匹配所有用户相关接口,减少权限配置的工作量 - 可以将用户权限缓存到Redis中,避免每次请求都查询数据库,提升校验性能
- 超级管理员角色可以跳过权限校验,需要在中间件中增加对应的判断逻辑