跳转至

认证中间件体系(源码增强版)

1. 双认证体系架构

MonkeyCode 的认证中间件有两层:后端 Go 中间件(真正的认证执行者)和代理层认证管理(Cookie 生命周期管理器)。

后端 Go 认证链                        代理 TypeScript 认证
────────────────                      ────────────────────
Auth() 中间件                          AuthManager 类
├── Cookie 存在?                      ├── getSessionCookie()
├── → 否: 401                         │   ├── 缓存命中?→ 返回
├── → 是: Redis GET                    │   └── 过期 → login()
│   ├── Lookup Key → user_id          ├── loginUser()
│   └── Hash Key → session data       │   └── POST password-login
├── role 检查                          ├── loginTeam()
│   └── admin? → admin 端点           │   └── POST teams/login
├── TargetActive()                     ├── checkStatus()
│   ├── Redis 写入活跃时间             │   └── GET /users/status
│   └── Redis 写入活跃 IP              ├── authHeaders()
└── Handler()                          │   └── Cookie + Content-Type
                                       └── logout()
                                           └── POST /users/logout

2. 后端 Go 中间件

2.1 中间件类型

中间件 方法 行为 使用场景
Auth() 强制认证 未登录返回 401 大部分用户 API
Check() 可选认证 未登录继续,context 无用户 公开流、可选认证端点
TeamAuth() 强制团队认证 未登录或无团队返回 401 团队管理 API
TeamAuthCheck() 可选团队认证 未登录返回 401,无团队返回 401 团队可选端点
TeamAdminAuth() 团队管理员授权 需 TeamAuth + admin 权限 团队管理操作

2.2 完整的中间件代码(根据源码反推)

// backend/middleware/auth.go
func Auth() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 1. 从 Cookie 提取 session uuid
        cookie, err := c.Cookie("monkeycode_ai_session")
        if err != nil {
            c.AbortWithStatusJSON(401, Response{Code: 40100, Msg: "Unauthorized"})
            return
        }

        // 2. Redis Lookup Key → user_id
        //    Key: lookup:{cookie_name}:{cookie_uuid}
        //    Value: user_id (UUID)
        userID, err := redis.Get(ctx, fmt.Sprintf("lookup:%s:%s", cookieName, cookie))
        if err != nil {
            c.AbortWithStatusJSON(401, Response{Code: 40100, Msg: "Session expired"})
            return
        }

        // 3. Redis Hash Key → 用户完整信息
        //    Key: {cookie_name}:{user_id}
        //    Field: {cookie_uuid}
        //    Value: {user_id, email, role, subscription_level, created_at}
        sessionData, err := redis.HGetAll(ctx, fmt.Sprintf("%s:%s", cookieName, userID))
        if err != nil {
            c.AbortWithStatusJSON(401, Response{Code: 40100, Msg: "Session not found"})
            return
        }

        // 4. 注入用户上下文
        c.Set("user_id", userID)
        c.Set("email", sessionData["email"])
        c.Set("role", sessionData["role"])
        c.Set("subscription_level", sessionData["subscription_level"])
        c.Next()
    }
}

2.3 活跃追踪中间件(TargetActive)

func TargetActive() gin.HandlerFunc {
    return func(c *gin.Context) {
        user := c.Get("user").(*MonkeyCodeUser)

        // 记录用户活跃时间到 Redis(7 天 TTL)
        redis.Set(ctx,
            fmt.Sprintf("monkeycode_ai:user:active:%s", user.ID),
            time.Now().Unix(),
            7*24*time.Hour,
        )

        // 记录用户活跃 IP(7 天 TTL)
        redis.Set(ctx,
            fmt.Sprintf("monkeycode_ai:user:ip:%s", user.ID),
            c.ClientIP(),
            7*24*time.Hour,
        )
        c.Next()
    }
}

TargetActive 的设计意图: - 用于在管理后台显示用户的最后活跃时间 - 用于空闲检测(例如:用户长时间不活跃自动注销) - 不参与认证检查,仅做日志记录

2.4 端点分组与中间件映射

路径前缀 中间件链 说明
/api/v1/public/* 公开端点(验证码、OAuth 跳转)
/api/v1/users/* Auth + TargetActive 大多数用户 API
/api/v1/teams/* TeamAuth + TargetActive 团队 API(部分再加 TeamAdminAuth)
/api/v1/admin/* Auth (admin role check) 管理员 API
/api/v1/auth/* Auth 认证相关

3. 代理层认证实现

3.1 AuthManager 类设计

代理层不执行真正的认证检查——它通过持有 Cookie 来"代表"一个已经认证的后端用户:

// proxy/src/auth.ts — AuthManager 类设计
export class AuthManager {
  private sessionCookie: string = ""       // Cookie UUID 值
  private sessionCookieName: string = "monkeycode_ai_session"
  private email: string = ""
  private passwordHash: string = ""
  private captchaToken: string = ""
  private lastAuthTime: number = 0          // 上次认证时间戳
  private sessionTTL: number = 24 * 60 * 60 * 1000  // 24 小时缓存
  private loginMode: LoginMode = "user"     // "user" | "team"
}
来源 优先级 代码位置 说明
环境变量 MONKEYCODE_SESSION_COOKIE 最高 auth.ts:48-52 直接使用预提取 Cookie
构造函数参数传入 auth.ts:48-52 从浏览器 DevTools 提取
密码登录 (loginUser/loginTeam) auth.ts:91-163 需要验证码
OAuth 自动化登录 admin-login.ts 6 步 HTTP 流程

3.3 密码登录实现

// proxy/src/auth.ts — 普通用户密码登录
async loginUser(): Promise<void> {
  const url = `${MONKEYCODE_BASE_URL}/api/v1/users/password-login`
  const body: Record<string, string> = {
    email: this.email.trim(),
    password: this.passwordHash,   // 明文密码
  }
  if (this.captchaToken) {
    body.captcha_token = this.captchaToken  // 验证码 token
  }

  const response = await fetch(url, {
    method: "POST",
    headers: mkHeaders({ "Content-Type": "application/json" }),
    body: JSON.stringify(body),
    redirect: "manual",  // 不自动跟随 302
  })

  if (!response.ok && response.status !== 302) {
    throw new Error(`User login failed (${response.status}): ${respBody}`)
  }

  // 从 Set-Cookie header 提取 cookie UUID
  const cookie = this.extractCookie(response, SESSION_COOKIE_NAME)
  this.sessionCookie = cookie
  this.sessionCookieName = SESSION_COOKIE_NAME
  this.lastAuthTime = Date.now()
}

3.4 团队登录与普通登录的区别

特性 普通用户登录 团队管理员登录
API 端点 POST /api/v1/users/password-login POST /api/v1/teams/users/login
Cookie 名称 monkeycode_ai_session monkeycode_ai_team_session
loginMode "user" "team"
登录后权限 用户级 API 团队级 API
状态检查端点 GET /api/v1/users/status GET /api/v1/teams/users/status
private extractCookie(response: Response, cookieName: string): string {
  const setCookie = response.headers.get("set-cookie")
  if (!setCookie) {
    throw new Error("No Set-Cookie header in login response")
  }

  // 从 Set-Cookie header 中正则提取
  // Set-Cookie: monkeycode_ai_session=uuid; Path=/; Domain=.monkeycode-ai.com; Max-Age=2592000; HttpOnly; Secure; SameSite=Lax
  const match = setCookie.match(new RegExp(`${cookieName}=([^;]+)`))
  if (!match) {
    throw new Error(`Cannot extract ${cookieName} from Set-Cookie`)
  }

  return match[1]  // 只提取 UUID 部分
}
// proxy/src/auth.ts
async getSessionCookie(): Promise<string> {
  // 代理侧 24 小时缓存检查
  if (this.sessionCookie && Date.now() - this.lastAuthTime < this.sessionTTL) {
    return this.sessionCookie  // 缓存命中,直接返回
  }
  await this.login()  // 缓存过期,重新登录
  return this.sessionCookie
}

// 构造认证请求头
async authHeaders(): Promise<Record<string, string>> {
  const cookie = await this.getSessionCookie()  // 自动触发刷新
  return {
    Cookie: `${this.sessionCookieName}=${cookie}`,
    "Content-Type": "application/json",
  }
}

4. 代理层 vs 后端的 Session 管理差异

特性 后端 Go 代理 TypeScript
Session TTL 30 天(Redis 硬限制) 24 小时(内存缓存)
刷新方式 不可刷新(30 天后必须重新登录) 过期后自动调用 login()
Cookie 提取 http.Cookie 对象 Set-Cookie 正则提取
并发管理 Redis 负责 lockedByWs 互斥锁
状态检查 中间件每个请求都检查 Redis checkStatus() 仅在健康检查中调用
多设备支持 Hash 多 field 设计 单 Cookie 单用

5. 代理管理端点的认证现状

// proxy/src/server.ts — 管理端点均无内置认证
app.post("/admin/session", ...)          // 设置 Cookie(可劫持)
app.post("/admin/login/send-code", ...)   // 发送短信(OAuth)
app.post("/admin/login/verify", ...)      // 短信验证
app.post("/admin/login/callback", ...)    // 回调登录
app.get("/admin/pool/status", ...)        // 号池状态
app.post("/admin/pool/refresh", ...)      // 号池重登录
app.post("/admin/refresh-models", ...)    // 刷新模型缓存

6. 错误码

HTTP 状态码 Error Code 含义 处理方式
401 40100 Session 过期或无效 代理标记 EXPIRED → 重登录
403 权限不足 代理标记 INVALID
401 40002 密码错误 账号永久不可用
401 40003 账号被封禁 账号永久不可用
401 40004 账号未激活 账号永久不可用

相关章节