Files
EasyGoAdmin_GoFrame_EleVue/document/wiki/02_中间件与请求处理链.txt
T
2026-10-20 11:37:25 +08:00

218 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
================================================================================
EasyGoAdmin 后端开发者文档 · 02 中间件与请求处理链
================================================================================
代码位置:internal/middleware/{cors,response,ratelimit,demo,jwt,tenant,
operation_log,public,util,wire,log}.go
注册顺序(router.go s.Use,顺序即执行顺序):
CORS → Response → RateLimit → Demo → JWT → Tenant → OperationLog → 处理器
(Bind 包装层做权限校验)
--------------------------------------------------------------------------------
1. CORS(cors.go)
--------------------------------------------------------------------------------
读取 cors.allowOrigin:
空或 "*" → r.Response.CORSDefault()(框架内置全放行策略)
具体来源 → 显式回显 Access-Control-Allow-Origin/Credentials/Headers/Methods,
OPTIONS 预检直接 204 返回。
Allow-Headers:Origin,Authorization,Content-Type,Accept,Token,X-Requested-With
Allow-Methods:GET,POST,PUT,DELETE,OPTIONS
--------------------------------------------------------------------------------
2. Response(response.go)—— 统一响应兜底
--------------------------------------------------------------------------------
职责一:SVG 静态文件加固。路径以 .svg 结尾时强制
Content-Disposition: attachment + X-Content-Type-Options: nosniff,
防止上传的 SVG 被浏览器同源内嵌执行(存储型 XSS 防线)。
职责二:请求元信息写入 ctx(realClientIP / User-Agent / Method / URL),
供登录日志与操作日志采集(consts.WithRequestMeta)。
职责三:panic 恢复。defer recover() 记录堆栈日志;若尚未写出响应则输出
500 统一信封(CodeServerError);已写出则只记录,避免双写。
--------------------------------------------------------------------------------
3. RateLimit(ratelimit.go)—— 固定窗口限流
--------------------------------------------------------------------------------
开关与阈值(manifest/config):
rateLimit.enable 默认 true
rateLimit.windowSeconds 窗口秒数,默认 60
rateLimit.limit 普通接口每窗口每 IP 每路径上限,默认 120(0=关闭普通限流)
rateLimit.authLimit 认证入口独立更严阈值,默认 10
规则:
* 仅限 /api 前缀路径;/api/health 显式豁免;/api.json、/swagger、静态资源
不带 /api/ 前缀自然不受限。
* 认证入口(/api/login、/api/captcha、/api/oauth2/token)用 authLimit。
* 键:ratelimit:{clientIP}:{path}:{窗口起点},窗口起点 = Unix/window*window
对齐;TTL = window+5s(仅用于回收,不影响窗口归属)。
* 计数经 utility/cache(Redis 优先、内存降级)→ 多实例共享计数。
* 计数存储异常时放行(限流不阻断业务);超限返回 code=429 信封。
* incrCounter:GET→SET 自增;不存在时 SET NX 抢占;并发下 NX 失败再读现值加一。
真实 IP 取值(util.go realClientIP):
直连来源不在 api.proxyIps(逗号分隔,支持精确 IP 与 CIDR)内 → 直接用
GetRemoteIp();在列表内才信任 X-Forwarded-For 首段 / X-Real-IP。
防止伪造 XFF 绕过限流或污染日志。
--------------------------------------------------------------------------------
4. Demo(demo.go)—— 演示模式
--------------------------------------------------------------------------------
配置 app.demo=true 时启用:
* GET/HEAD/OPTIONS 放行(演示站可浏览)
* 公开白名单路径放行(保证演示站可登录)
* 其余写操作(POST/PUT/PATCH/DELETE)一律拒绝:
code=1015 “演示环境,禁止操作!”
生产环境保持 app.demo=false。
--------------------------------------------------------------------------------
5. JWT(jwt.go)—— 身份认证
--------------------------------------------------------------------------------
流程:
1) IsPublicPath 命中 → 放行(登录/验证码/令牌兑换/健康检查/文档)
2) 取 token:Authorization: Bearer xxx(兼容小写与无前缀),
缺失时回退查询参数 ?token=(个别 URL 直传场景)
3) jwtutil.Parse:HS256 + 签名/过期/签发者(jwt.issuer)校验,
非 HMAC 或无效 → 401
4) 黑名单检查:cache.Exists(easygoadmin:token:blacklist:{jti}) → 401
(登出、令牌轮换、强制下线写入)
5) ctx 注入:UserID/TenantID/Username/Realname/TokenJTI/TokenExpire
6) 身份扩展(角色+权限+数据范围):优先读缓存 easygoadmin:user:identity:{userID}
(JSON,TTL 300s),未命中回源 IdentityLoader(= logic.LoadIdentity)并回写。
注入 ctx:Roles/Perms + DataScope(数据范围档位)/DeptID(部门ID),
后两者供数据权限过滤读取(见 03 章第 6 节)。
加载失败不阻断请求(告警日志),回退空身份(等价无权限;数据范围
GetDataScope 读 0 回退"全部"——无权限下列表本就不可达)。
loadIdentity 说明:userID<=0 直接跳过;IdentityLoader 为 nil(未接线)时
返回空身份。
--------------------------------------------------------------------------------
6. Tenant(tenant.go)—— 租户状态校验
--------------------------------------------------------------------------------
位于 JWT 之后:
* 公开路径放行
* ctx tenant_id=0(平台/超管)直接放行
* 其余加载租户摘要(缓存 easygoadmin:tenant:info:{id},TTL 60s,未命中
回源 TenantLoader = logic.LoadTenant):
- status != 1 → code=1006 租户已禁用
- expire_time 早于现在 → code=1005 租户已过期
* LoadTenant 语义:租户不存在 → 按禁用处理(失效关闭);数据库基础错误 →
返回 nil,中间件放行(避免存储抖动导致全站不可用)。
--------------------------------------------------------------------------------
7. OperationLog(operation_log.go)—— 操作日志采集
--------------------------------------------------------------------------------
开关:operationLog.enable(总开关);operationLog.logQuery(是否记录查询类)。
流程:请求前记 start → Next() → 从 ctx 取路由元数据(meta.CtxKey),
无 log 标签直接跳过 → logType=LogTypeMap[rm.LogType](query 且 logQuery=false
跳过)→ 组装 OperationLogEntry:
Title=log 标签 Type=logType Source=0
Method="GET /api/user/page"(路由模板) RequestMethod/URL=实际请求
Param:GET/DELETE 取 query JSON;写操作优先取 body(maskSecret 脱敏
password/oldPassword/newPassword/salt/access_token/refresh_token → ***)
Result:响应信封 JSON(截断 8000 字节,UTF-8 边界回退截断)
IP=realClientIP Location=ip2region OS/Browser=ua 解析
ConsumeTime=毫秒 Status:响应 code==0 → 0 正常,否则 1 异常(Error=msg)
CreateUser=ctx 用户名
异步落库:有界通道(2048)+ 单 worker;批量满 100 条或每 1s flush 一批,
逐条经 LogSink(= 业务侧注册的落库函数)写 goframe_operation_log;
落库失败仅记错误日志;通道满则丢弃(每丢 100 条告警一次),绝不阻塞业务。
--------------------------------------------------------------------------------
8. 免登录白名单(public.go)
--------------------------------------------------------------------------------
publicPaths 精确匹配:
/api/login、/api/register(白名单放行,路由未注册)、/api/captcha、/api/oauth2/token、/api/health
另放行 /api.json 与 /swagger 前缀(文档)。
三处须保持一致(改动时同步):
1) middleware/public.go 白名单
2) 各接口 gmeta 的 public:"true" 标注(auth.go)
3) 健康检查等 router.go 中的手工 BindHandler
注意:public 路由不带 /v1 版本段的历史约定已固化为“全部路由都不带 /v1”,
白名单与 gmeta path 均以 /api 直达。
--------------------------------------------------------------------------------
9. 依赖接线(wire.go)
--------------------------------------------------------------------------------
中间件层不 import 业务包,通过三个函数变量解耦:
var IdentityLoader func(ctx, userID) (*Identity, error) // 角色+权限
var TenantLoader func(ctx, tenantID) (*TenantInfo, error) // status/expire
var LogSink func(ctx, *OperationLogEntry) error // 日志落库
注册方(logic 各文件 init):
internal/logic/auth.go init: SetIdentityLoader(LoadIdentity)
SetTenantLoader(LoadTenant)
internal/logic/operation_log.go init: SetLogSink(SaveOperationLog)
(异步 worker 每条回调,写 goframe_operation_log)
Identity 结构:{Roles []string(角色编码), Perms []string(权限节点),
DataScope int(数据范围档位,多角色取最宽松;0=旧缓存缺字段,
读侧 GetDataScope 回退"全部"), DeptId int(所属部门ID)}
TenantInfo 结构:{ID, Status, ExpireTime}(热路径最小字段集)
cmd/wire.go 在启动时仅校验三者是否非 nil,缺失输出警告(不 panic)。
--------------------------------------------------------------------------------
10. 辅助工具(util.go / log.go)
--------------------------------------------------------------------------------
cfgBool/cfgInt 读配置,缺失回退默认值
gjsonEncode JSON 序列化(失败返回空串)
trunc 按字节截断并回退到 UTF-8 字符边界(防 MySQL 拒绝非法字节)
maskJSONField 正则替换 JSON 文本中指定字段值为 ***(正则缓存复用)
realClientIP 受信代理 + XFF/X-Real-IP
isTrustedProxy api.proxyIps 精确 IP / CIDR 匹配
requestLocation ip2region.Location(失败返回 "-")
glogWarn 中间件统一告警输出
--------------------------------------------------------------------------------
11. 请求生命周期(一个受保护写请求的完整路径)
--------------------------------------------------------------------------------
1 CORS 预检/头设置
2 Response 元信息入 ctx;defer panic 兜底
3 RateLimit 固定窗口计数,超限 429
4 Demo 演示模式拦截写操作
5 JWT 解析校验 token → 注入身份;黑名单 401
6 Tenant 租户状态/过期校验(60s 缓存)
7 OperationLog 记 start,Next()
8 Bind 包装层 元数据入 ctx → HasPerm 权限校验(403)→ Parse 参数 → 调 controller
9 controller 转发 service 接口
10 logic ModuleSpec 流水线 / 手写业务;dao.Ctx 查询(钩子已注入条件)
11 response.Write 信封写出(code/body 记入 ctx)
12 OperationLog 组装记录 → 异步通道 → 批量落库
12. 请求处理时序图(受保护写请求 + 操作日志异步落库)
Client 中间件链 Bind包装/业务 缓存 DB/日志
│ POST /api/user/update │ │ │
│───▶│ CORS 头/预检 │ │ │
│ │ Response: 元信息入ctx, defer recover │ │
│ │ RateLimit ──────────────────────────────────────▶ SET/GET 计数│
│ │ Demo(app.demo=false 放行) │ │ │
│ │ JWT: Parse+黑名单 ───────────────────────────────▶ Exists jti │
│ │ 注入身份; Roles/Perms/DataScope/DeptId ──────▶ 命中缓存 │
│ │ 未命中 → IdentityLoader 回源 ───────────────────────▶ user/role/menu
│ │ Tenant: status/expire ───────────────────────────▶ tenant:info │
│ │ OperationLog: start=now │ │ │
│ │ │ │ │ │
│ │ ▼ ▼ HasPerm(403?短路) │ │
│ │ Parse(req) → logic.Update │ │
│ │ spec 流水线: BeforeUpdate → │ │
│ │ saveTransform → 唯一预检 → ────────────────▶ UPDATE
│ │ (hooks 盖章 update_time/user) │ │
│ │ ▼ │ │ │
│◀───│ WriteJson{code:0,...}(code/body 记 ctx) │ │
│ │ OperationLog: 组装 entry(脱敏 param/result) │ │
│ │ select{ logChan<-entry } 非阻塞 │ │
│ │ worker: 攒批 100条/1s ──────────────────▶ INSERT operation_log
短路/异常分支:
* 任一中间件短路(429 / 演示拦截 / 401 / 403)→ 直接 WriteJson,后续不执行
* panic → Response recover → 500 信封(已写出响应则仅记录日志)
* logChan 满 → 丢弃并计数告警(每丢 100 条输出一次 Warning)