4. 编写一个基础的HTTP权限检查中间件
在Go语言的标准库 net/http 中,中间件是一种函数式编程模式,用于在HTTP请求处理链中插入横切关注点。权限检查中间件负责在请求到达业务处理器之前,验证调用方是否具备执行当前操作所需的权限。本节将基于标准库实现一个无外部依赖、可测试、可组合的基础权限中间件。
4.1 中间件的核心模式
Go标准库的中间件本质是一个接收 http.Handler 并返回 http.Handler 的函数。权限中间件在此基础上增加一个权限检查函数作为参数,形成“高阶函数”模式。
核心签名:
func PermissionMiddleware(next http.Handler, checkFn func(r *http.Request) bool) http.Handler
这种模式的优势在于:
- 无侵入:不修改业务Handler的代码
- 可组合:多个中间件可以链式叠加
- 可测试:权限检查函数可独立单元测试
4.2 基础权限中间件实现
以下代码实现了一个完整的HTTP权限检查中间件,包含请求上下文传递、权限检查失败处理、以及可配置的拒绝响应。
package middleware
import (
"context"
"net/http"
)
// 定义权限检查函数类型
type PermissionCheck func(r *http.Request) bool
// 定义权限拒绝时的响应函数类型
type DenyHandler func(w http.ResponseWriter, r *http.Request)
// PermissionMiddleware 创建一个权限检查中间件
func PermissionMiddleware(next http.Handler, check PermissionCheck, deny DenyHandler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 执行权限检查
if !check(r) {
// 权限不足,调用拒绝处理器
if deny != nil {
deny(w, r)
} else {
http.Error(w, "Forbidden", http.StatusForbidden)
}
return
}
// 权限通过,继续处理链
next.ServeHTTP(w, r)
})
}
// 默认拒绝处理器:返回 403 JSON 响应
func DefaultDenyHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(`{"error":"permission_denied","message":"权限不足"}`))
}
4.3 权限检查函数的典型实现
权限检查函数通常需要从请求中提取用户身份和资源标识,然后与权限规则进行匹配。以下是一个基于角色和资源的检查示例:
// 用户角色定义
type Role string
const (
RoleAdmin Role = "admin"
RoleUser Role = "user"
RoleGuest Role = "guest"
)
// 权限规则:角色 -> 允许的资源路径前缀
var rolePermissions = map[Role][]string{
RoleAdmin: {"/api/admin", "/api/user", "/api/public"},
RoleUser: {"/api/user", "/api/public"},
RoleGuest: {"/api/public"},
}
// 从请求中提取角色(示例:从Header或JWT中获取)
func extractRole(r *http.Request) Role {
// 实际项目中应从JWT Token或Session中解析
roleStr := r.Header.Get("X-User-Role")
switch roleStr {
case "admin":
return RoleAdmin
case "user":
return RoleUser
default:
return RoleGuest
}
}
// 权限检查函数:基于角色和请求路径
func RolePathCheck(r *http.Request) bool {
role := extractRole(r)
path := r.URL.Path
permissions, exists := rolePermissions[role]
if !exists {
return false
}
for _, prefix := range permissions {
if len(path) >= len(prefix) && path[:len(prefix)] == prefix {
return true
}
}
return false
}
4.4 中间件的使用与组合
在HTTP服务中,权限中间件可以与其他中间件(如日志、认证)组合使用,形成完整的请求处理链。
func main() {
// 业务处理器
adminHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Admin Dashboard"))
})
userHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("User Profile"))
})
// 应用权限中间件
protectedAdmin := PermissionMiddleware(adminHandler, RolePathCheck, DefaultDenyHandler)
protectedUser := PermissionMiddleware(userHandler, RolePathCheck, DefaultDenyHandler)
// 注册路由
http.Handle("/api/admin/", protectedAdmin)
http.Handle("/api/user/", protectedUser)
http.Handle("/api/public/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Public Content"))
}))
http.ListenAndServe(":8080", nil)
}
4.5 权限检查流程
下图展示了HTTP请求经过权限中间件的完整处理流程:
4.6 权限检查函数对比
不同的权限检查策略适用于不同的场景,下表对比了三种常见的检查方式:
| 检查策略 | 检查依据 | 适用场景 | 性能 | 灵活性 |
|---|---|---|---|---|
| 基于角色(RBAC) | 用户角色 + 资源路径 | 管理后台、企业系统 | 高(O(1) 查找) | 中 |
| 基于属性(ABAC) | 用户属性 + 资源属性 + 环境 | 金融、医疗等合规场景 | 中(需解析规则) | 高 |
| 基于ACL | 用户ID + 资源ID + 操作 | 文件系统、文档管理 | 中(需查表) | 高 |
4.7 常见陷阱与最佳实践
⚠️ 常见陷阱:
- 忽略上下文传递:权限检查结果应写入
context.Context,避免后续重复解析 - 硬编码拒绝响应:应提供可配置的
DenyHandler,支持JSON/HTML/重定向 - 路径匹配漏洞:使用
strings.HasPrefix时注意路径规范化,防止/api/admin/../user绕过
💡 最佳实践:
- 将权限检查函数设计为纯函数,便于单元测试
- 使用
http.Hijacker或http.Flusher接口时注意中间件包装 - 在中间件中尽早返回,减少不必要的资源消耗
- 记录权限拒绝日志,便于审计和调试
4.8 权限检查结果上下文传递
为了在后续的Handler中复用权限检查结果,建议将角色、权限列表等信息写入请求上下文:
// 定义上下文键类型,避免冲突
type contextKey string
const (
RoleKey contextKey = "role"
PermissionsKey contextKey = "permissions"
)
// 增强版中间件:将权限信息写入上下文
func PermissionMiddlewareWithContext(next http.Handler, check PermissionCheck) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !check(r) {
DefaultDenyHandler(w, r)
return
}
// 将角色信息写入上下文
role := extractRole(r)
ctx := context.WithValue(r.Context(), RoleKey, role)
ctx = context.WithValue(ctx, PermissionsKey, rolePermissions[role])
// 使用新上下文继续处理
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// 在业务Handler中读取上下文
func AdminHandler(w http.ResponseWriter, r *http.Request) {
role := r.Context().Value(RoleKey).(Role)
permissions := r.Context().Value(PermissionsKey).([]string)
// 使用角色和权限信息...
}
4.9 中间件链的架构示意
下图展示了权限中间件在完整中间件链中的位置和协作关系:
4.10 单元测试示例
权限中间件的可测试性是其核心优势之一。以下是一个完整的测试用例:
func TestPermissionMiddleware(t *testing.T) {
tests := []struct {
name string
role string
path string
expected int
}{
{"admin访问admin路径", "admin", "/api/admin/dashboard", http.StatusOK},
{"user访问admin路径", "user", "/api/admin/dashboard", http.StatusForbidden},
{"guest访问public路径", "guest", "/api/public/info", http.StatusOK},
{"guest访问user路径", "guest", "/api/user/profile", http.StatusForbidden},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", tt.path, nil)
req.Header.Set("X-User-Role", tt.role)
recorder := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
middleware := PermissionMiddleware(handler, RolePathCheck, DefaultDenyHandler)
middleware.ServeHTTP(recorder, req)
if recorder.Code != tt.expected {
t.Errorf("期望状态码 %d,实际 %d", tt.expected, recorder.Code)
}
})
}
}