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请求经过权限中间件的完整处理流程:

HTTP权限中间件请求处理流程 HTTP请求到达 权限检查函数 (提取角色 → 匹配资源路径) 检查结果? 通过 调用 next.ServeHTTP 拒绝 调用 DenyHandler 200 OK 响应 403 Forbidden 权限检查函数返回 true 则继续处理链,返回 false 则触发拒绝处理器

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 中间件链的架构示意

下图展示了权限中间件在完整中间件链中的位置和协作关系:

Go HTTP中间件链架构 HTTP请求 日志中间件 请求日志记录 认证中间件 Token验证 权限中间件 角色/资源检查 业务Handler 核心逻辑 中间件按注册顺序依次执行,权限中间件通常在认证之后、业务Handler之前 任一中间件返回错误则中断链式调用,后续中间件和Handler不再执行 请求流向:Request → Logger → Auth → Permission → Handler → Response 权限中间件是安全防线中的关键一环,负责细粒度的访问控制

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)
            }
        })
    }
}