签入脚本
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
================================================================================
|
||||
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)
|
||||
Reference in New Issue
Block a user