Epic ID: epic-5
关联 PRD: FR-AUTH-001
负责人: Architect Agent
状态: Planning
优先级: P0 (必需)
预估工作量: 7-8 天 (MVP)
实现完整的用户认证和基于项目的权限控制体系,支持 Web 端和 API 客户端。
- 用户可以安全登录和访问系统
- 项目间权限完全隔离
- 支持多种客户端类型(浏览器、移动端、API)
- 细粒度的资源访问控制
- 用户可通过 API 注册和登录(Email + Password)
- JWT Token 正确签发和验证
- 密码安全存储(bcrypt hashing)
- 项目级权限隔离正常工作
- 资源级权限控制生效
- API 响应时间 P95 < 100ms
- 单元测试覆盖率 > 80%
AppRun 采用 逻辑多租户 架构,而非物理隔离。
- Project 是权限和资源的引力中心。
- RBAC with Domains: 采用 Casbin 的
(sub, dom, obj, act)模型,其中dom为project_id。 - Context Isolation: 身份认证解决 "你是谁",项目上下文解决 "你在哪里",权限引擎解决 "你能做什么"。
- bcrypt: 密码哈希 (golang.org/x/crypto/bcrypt)
- JWT: Token 认证 (github.com/golang-jwt/jwt/v5)
- Casbin: RBAC 权限引擎
- 中间件: Chi Router 中间件链
- Session Store: 可选的 Cookie Session (github.com/gorilla/sessions)
注册流程:
用户 → POST /api/auth/register (email, password, name)
→ 验证邮箱格式
→ 密码强度检查
→ bcrypt.GenerateFromPassword
→ 存储到数据库
→ 返回 User 对象
登录流程:
用户 → POST /api/auth/login (email, password)
→ 查询用户
→ bcrypt.CompareHashAndPassword
→ 生成 JWT Token (access + refresh)
→ 返回 Token
API 访问流程:
客户端 → API 请求 (Authorization: Bearer <JWT>)
→ AuthMiddleware 验证 Token
→ 解析 Claims (user_id, project_id)
→ 存入 Context
→ RequirePermission 检查权限
→ 业务逻辑
| 端点 | 方法 | 功能 | 认证 |
|---|---|---|---|
/api/auth/register |
POST | 用户注册 | Public |
/api/auth/login |
POST | 用户登录 | Public |
/api/auth/refresh |
POST | 刷新 Access Token | Refresh Token |
/api/auth/me |
GET | 获取当前用户信息 | JWT |
/api/auth/logout |
POST | 登出(可选) | JWT |
/api/auth/change-password |
POST | 修改密码 | JWT |
请求:
POST /api/auth/register
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePass123!",
"name": "John Doe"
}响应:
{
"success": true,
"data": {
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name": "John Doe",
"created_at": "2026-01-08T10:00:00Z"
}
}
}请求:
POST /api/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePass123!"
}响应:
{
"success": true,
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 3600,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name": "John Doe"
}
}
}CREATE TABLE users (
id VARCHAR(36) PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL, -- bcrypt hash
name VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
email_verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
last_login_at TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);CREATE TABLE project_members (
id VARCHAR(36) PRIMARY KEY,
project_id VARCHAR(36) NOT NULL,
user_id VARCHAR(36) NOT NULL,
role VARCHAR(20) NOT NULL, -- owner, admin, member, viewer
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(project_id, user_id)
);全局角色:
platform_admin: 平台管理员(所有权限)platform_user: 普通用户(创建项目、加入项目)
项目角色:
owner: 项目所有者(所有权限)admin: 项目管理员(管理成员、配置)member: 项目成员(读写数据)viewer: 查看者(只读)
Model 文件内嵌在 core/internal/rbac/model.conf,使用 go:embed 指令,避免对配置中心的依赖。
# core/internal/rbac/model.conf (内嵌资源)
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.actfunc AuthMiddleware(jwtSecret []byte) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 1. 提取 Token(Authorization Header)
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
response.Error(w, 401, "AUTH_MISSING_TOKEN", "Missing authorization token")
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
// 2. 验证 JWT Token
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return jwtSecret, nil
})
if err != nil || !token.Valid {
response.Error(w, 401, "AUTH_INVALID_TOKEN", "Invalid token")
return
}
// 3. 提取 Claims 并存入 Context
claims, ok := token.Claims.(*Claims)
if !ok {
response.Error(w, 401, "AUTH_INVALID_CLAIMS", "Invalid token claims")
return
}
ctx := context.WithValue(r.Context(), "user_id", claims.UserID)
ctx = context.WithValue(ctx, "email", claims.Email)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// JWT Claims 结构
type Claims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}func RequirePermission(resource, action string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value("user_id").(string)
projectID := chi.URLParam(r, "project_id")
// Casbin 权限检查
allowed := enforcer.Enforce(userID, projectID, resource, action)
if !allowed {
response.Error(w, 403, "PERM_FORBIDDEN", "Permission denied")
return
}
next.ServeHTTP(w, r)
})
}
}# config/auth.yaml
auth:
jwt:
secret: "${JWT_SECRET}" # 环境变量,生产环境必须设置
access_token_expire: 3600 # 1 小时
refresh_token_expire: 604800 # 7 天
algorithm: "HS256"
password:
min_length: 8
require_uppercase: true
require_lowercase: true
require_number: true
require_special: false
bcrypt_cost: 12 # bcrypt 计算成本 (10-14)
rate_limit:
login_attempts: 5 # 5 次失败后锁定
lockout_duration: 900 # 锁定 15 分钟
casbin:
# model 已内嵌到 core/internal/rbac/model.conf
policy_path: "./config/casbin_policy.csv"优先级: P0
工作量: 1.5 天
文件: 5-1-user-registration.md
- 实现用户注册 API (
POST /api/auth/register) - 邮箱格式验证
- 密码强度验证(长度、复杂度)
- bcrypt 密码哈希存储
- 用户数据模型(Ent Schema)
- 编写单元测试
优先级: P0
工作量: 1.5 天
文件: 5-2-user-login.md
- 实现登录 API (
POST /api/auth/login) - 密码验证(bcrypt.CompareHashAndPassword)
- JWT Token 签发(Access + Refresh)
- Token Claims 设计(user_id, email, exp)
- 更新最后登录时间
- 编写单元测试
优先级: P0
工作量: 1 天
文件: 5-3-jwt-middleware.md
- 实现 AuthMiddleware(Token 提取和验证)
- 集成到 Chi Router
- Context 注入(user_id, email)
- 错误处理(401 响应)
- 编写集成测试
优先级: P0
工作量: 1 天
- 实现 Refresh Token 逻辑
- 实现
/api/auth/refresh端点 - Refresh Token 黑名单(可选,Redis)
- 编写单元测试
优先级: P0
工作量: 2 天
- 集成 Casbin
- 实现项目成员管理
- 实现 RequirePermission 中间件
- 定义权限策略
- 编写权限测试用例
优先级: P1
工作量: 0.5 天
- 实现
/api/auth/me端点 - 实现
/api/auth/change-password端点 - 实现
/api/auth/logout(可选,客户端删除 Token) - 编写 API 文档
golang.org/x/crypto/bcrypt- 密码哈希github.com/golang-jwt/jwt/v5- JWT Tokengithub.com/casbin/casbin/v2- RBAC 引擎github.com/gorilla/sessions- Session 管理(可选)golang.org/x/time/rate- 速率限制(可选)
- 数据库模块(Ent ORM)
- 配置模块(Viper)
- 日志模块(Logrus)
- Response 标准化模块
- PostgreSQL 14+ (用户数据存储)
- Redis 7+ (可选:Token 黑名单、权限缓存)
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| JWT Secret 泄露 | 高 | 使用环境变量,定期轮换,考虑 RSA 签名 |
| 密码暴力破解 | 高 | 速率限制、账户锁定、bcrypt 高成本 |
| Token 过期处理 | 中 | 实现完善的 Refresh Token 机制 |
| Casbin 策略复杂度 | 中 | 从简单策略开始,逐步扩展 |
| 多租户权限隔离 | 高 | 严格测试权限边界,Casbin 策略审计 |
| 缺少 MFA 支持 | 中 | MVP 阶段可接受,Post-MVP 添加 TOTP |
- JWT 签发和验证逻辑
- Casbin 策略匹配
- 中间件功能测试
- 完整认证流程(登录 → Token → API 调用)
- 权限验证场景(正常访问、拒绝访问)
- Token 刷新流程
- 认证中间件延迟 < 10ms
- 权限检查延迟 < 5ms
- 并发登录场景
auth_token_generated_total- Token 签发总数auth_token_validation_failed_total- Token 验证失败次数auth_permission_denied_total- 权限拒绝次数auth_session_validation_duration_seconds- Session 验证耗时
- 生产环境:使用强随机 Secret(至少 32 字节)
- 轮换策略:每 90 天轮换一次
- 存储方式:环境变量或密钥管理服务(如 AWS Secrets Manager)
- 多环境隔离:开发/测试/生产使用不同 Secret
- 生产环境必须启用 HTTPS
- 使用 HSTS Header 强制浏览器使用 HTTPS
- JWT Token 仅通过 HTTPS 传输
- 明确允许的 Origin 列表
- 不允许使用通配符
* - 正确设置 Credentials 模式
- 所有用户输入必须验证和消毒
- 使用参数化查询防止 SQL 注入
- 输出转义防止 XSS
- 登录端点:5 次/15 分钟(每 IP)
- 注册端点:3 次/小时(每 IP)
- Token 刷新:10 次/小时(每用户)
- 记录所有认证事件(成功/失败)
- 记录权限拒绝事件
- 日志包含:时间戳、用户 ID、IP、操作
| 错误码 | HTTP 状态码 | 说明 |
|---|---|---|
AUTH_INVALID_CREDENTIALS |
401 | 邮箱或密码错误 |
AUTH_INVALID_TOKEN |
401 | Token 无效或已过期 |
AUTH_MISSING_TOKEN |
401 | 缺少认证 Token |
AUTH_INVALID_CLAIMS |
401 | Token Claims 无效 |
AUTH_ACCOUNT_LOCKED |
403 | 账户已锁定(登录失败次数过多) |
AUTH_EMAIL_EXISTS |
409 | 邮箱已被注册 |
AUTH_WEAK_PASSWORD |
400 | 密码强度不足 |
PERM_FORBIDDEN |
403 | 无权限访问 |
PERM_PROJECT_NOT_MEMBER |
403 | 不是项目成员 |
文档维护: Winston (Architect Agent)
最后更新: 2025-12-26