diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 1a12a237..db2741f7 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -26,6 +26,8 @@ sidebar: false ### 变更 +- 提取并新增 `common/response` 通用响应子包,替换 `controller/response.go` 中的响应细节,重构所有中间件中硬编码的 `c.JSON` 响应,统一 API 响应格式并隔离 Gin 依赖。 + - 将 `usage.md` 改名为 `proxy-config.md`(新建反代配置),重新梳理大纲结构,专注于如何从导入/申请证书开始,一步步新增并发布代理路由规则,并同步更新全部导航与文档引用链接 - WAF 白名单调整为准入名单语义:存在白名单规则时,未命中白名单的请求会被拦截 - 更新仓库结构设计文档,使 `openflare_agent` 和 `openflare_server` 的目录结构描述与实际物理结构保持一致 diff --git a/docs/plan/20260605-move-response-to-common.md b/docs/plan/20260605-move-response-to-common.md new file mode 100644 index 00000000..62c54ef3 --- /dev/null +++ b/docs/plan/20260605-move-response-to-common.md @@ -0,0 +1,53 @@ +# 提取公共 Response 包以支持 Middleware 统一响应实现计划 + +--- + +## 1. 目标与背景 (Goal & Context) +* **需求背景**: + 原本 `response.go` 放在 `controller` 目录下。由于 `middleware` 处于 `controller` 上游,并且和 `controller` 跨包,`middleware` 无法直接调用 `controller` 的响应逻辑。这导致 `middleware` 中存在许多手写的、格式硬编码的 `c.JSON` 调用。 + 如果直接将 `response.go` 放入已有的 `common` 根包,会引入 `github.com/gin-gonic/gin` 依赖,导致依赖 `common` 的底层 `service` 和 `model` 也受到 `gin` 框架的依赖污染。 +* **开发范围 (Scope)**: + * 在 `openflare_server/common/response` 下新建 `response.go` 子包(`package response`),存放通用的 HTTP 响应逻辑。 + * 将 `controller/response.go` 中的响应逻辑移植到 `common/response/response.go`。 + * 在 `controller/response.go` 中保留参数解析逻辑,并作为代理调用 `common/response` 中的方法,从而实现对 controller 内 140+ 处现有调用的**零改动**。 + * 修改 `middleware` 包下的所有 `c.JSON` 手写响应,统一采用 `common/response` 的方法。 + +## 2. 设计与决策 (Design & Decisions) +* **核心对象/数据模型**:无需改动任何数据库或数据模型。 +* **API 与鉴权设计**:不改变任何公开 API 接口路由与现有的成功/失败响应 JSON 格式。 +* **设计决策权衡**: + * **方案 A:直接把 response.go 放入 common 根包** + * 缺点:导致原本不应该感知 Web 传输协议的 `service`、`model`、`job` 包间接依赖了 `github.com/gin-gonic/gin` 框架。 + * **方案 B:在 common 下建子包 `common/response`** + * 优点:满足 `common` 归类的直觉,又保持了包的物理依赖隔离。业务底层不受 `gin` 污染,而 `controller` 和 `middleware` 这类传输层可引入该包进行代码复用。**(采用此方案)** + +## 3. 具体修改文件清单 (Proposed Changes) + +### 后端 Server +* #### [NEW] [response.go](file:///Users/ryan/DEV/Go/OpenFlare/openflare_server/common/response/response.go) + * 职责:通用的 Gin 响应工具包,包括 `RespondSuccess`、`RespondSuccessWithExtras`、`RespondSuccessMessage`、`RespondFailure`、`RespondBadRequest`、`RespondUnauthorized` 和 `RespondForbidden`。 +* #### [MODIFY] [response.go](file:///Users/ryan/DEV/Go/OpenFlare/openflare_server/controller/response.go) + * 职责:删去具体的 HTTP 响应渲染实现,以代理方式调用 `common/response` 包中导出的函数,保持 controller 包内现有调用的向后兼容;保留原有的 `decodeJSONBody`、`decodeOptionalJSONBody`、`parseIDParam`、`parseIDParamByName`、`bindJSON` 参数绑定解析逻辑。 +* #### [MODIFY] [agent-auth.go](file:///Users/ryan/DEV/Go/OpenFlare/openflare_server/middleware/agent-auth.go) + * 职责:替换 `c.JSON(http.StatusUnauthorized, ...)` 为使用 `response.RespondUnauthorized(...)` 渲染统一响应。 +* #### [MODIFY] [auth.go](file:///Users/ryan/DEV/Go/OpenFlare/openflare_server/middleware/auth.go) + * 职责:替换 `c.JSON` 相关的未授权和失败响应为使用 `response` 包方法。 +* #### [MODIFY] [jwt.go](file:///Users/ryan/DEV/Go/OpenFlare/openflare_server/middleware/jwt.go) + * 职责:替换 JWT 未授权的回调响应为使用 `response` 统一格式。 +* #### [MODIFY] [relay-auth.go](file:///Users/ryan/DEV/Go/OpenFlare/openflare_server/middleware/relay-auth.go) + * 职责:替换未授权和 StatusForbidden 响应为使用 `response` 方法。 +* #### [MODIFY] [tunnel-auth.go](file:///Users/ryan/DEV/Go/OpenFlare/openflare_server/middleware/tunnel-auth.go) + * 职责:替换未授权和 StatusForbidden 响应为使用 `response` 方法。 + +--- + +## 4. 验证计划 (Verification Plan) + +### 自动化单元测试 +* 运行项目已有测试验证重构是否影响 API 连通性与响应结构: + ```bash + go test -v ./router/... + ``` + ```bash + go test -v ./service/... + ``` diff --git a/openflare_server/common/response/response.go b/openflare_server/common/response/response.go new file mode 100644 index 00000000..dcae0d23 --- /dev/null +++ b/openflare_server/common/response/response.go @@ -0,0 +1,82 @@ +package response + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +const invalidParamsMessage = "参数错误" + +// RespondSuccess sends a successful response with data +func RespondSuccess(c *gin.Context, data any) { + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": data, + }) +} + +// RespondSuccessWithExtras sends a successful response with data and extra fields +func RespondSuccessWithExtras(c *gin.Context, data any, extras gin.H) { + payload := gin.H{ + "success": true, + "message": "", + "data": data, + } + for key, value := range extras { + payload[key] = value + } + c.JSON(http.StatusOK, payload) +} + +// RespondSuccessMessage sends a successful response with a custom message +func RespondSuccessMessage(c *gin.Context, message string) { + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": message, + }) +} + +// RespondFailure sends a failed response with http.StatusOK and a failure message +func RespondFailure(c *gin.Context, message string) { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": message, + }) +} + +// RespondBadRequest sends a bad request response (400) +func RespondBadRequest(c *gin.Context, message string) { + if message == "" { + message = invalidParamsMessage + } + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": message, + }) +} + +// RespondUnauthorized sends an unauthorized response (401) +func RespondUnauthorized(c *gin.Context, message string) { + c.JSON(http.StatusUnauthorized, gin.H{ + "success": false, + "message": message, + }) +} + +// RespondForbidden sends a forbidden response (403) +func RespondForbidden(c *gin.Context, message string) { + c.JSON(http.StatusForbidden, gin.H{ + "success": false, + "message": message, + }) +} + +// RespondErrorWithStatus sends a response with target HTTP status code and a message +func RespondErrorWithStatus(c *gin.Context, code int, message string) { + c.JSON(code, gin.H{ + "success": false, + "message": message, + }) +} diff --git a/openflare_server/controller/response.go b/openflare_server/controller/response.go index 812bee1f..0492293a 100644 --- a/openflare_server/controller/response.go +++ b/openflare_server/controller/response.go @@ -4,63 +4,35 @@ import ( "encoding/json" "errors" "io" - "net/http" "strconv" "github.com/gin-gonic/gin" + + "openflare/common/response" ) -const invalidParamsMessage = "参数错误" - func respondSuccess(c *gin.Context, data any) { - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": data, - }) + response.RespondSuccess(c, data) } func respondSuccessWithExtras(c *gin.Context, data any, extras gin.H) { - payload := gin.H{ - "success": true, - "message": "", - "data": data, - } - for key, value := range extras { - payload[key] = value - } - c.JSON(http.StatusOK, payload) + response.RespondSuccessWithExtras(c, data, extras) } func respondSuccessMessage(c *gin.Context, message string) { - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": message, - }) + response.RespondSuccessMessage(c, message) } func respondFailure(c *gin.Context, message string) { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": message, - }) + response.RespondFailure(c, message) } func respondBadRequest(c *gin.Context, message string) { - if message == "" { - message = invalidParamsMessage - } - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "message": message, - }) + response.RespondBadRequest(c, message) } func respondUnauthorized(c *gin.Context, message string) { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": message, - }) + response.RespondUnauthorized(c, message) } func decodeJSONBody(body io.Reader, target any) error { diff --git a/openflare_server/middleware/agent-auth.go b/openflare_server/middleware/agent-auth.go index 368c1963..b000bfa3 100644 --- a/openflare_server/middleware/agent-auth.go +++ b/openflare_server/middleware/agent-auth.go @@ -2,7 +2,7 @@ package middleware import ( "github.com/gin-gonic/gin" - "net/http" + "openflare/common/response" "openflare/service" ) @@ -11,10 +11,7 @@ func AgentAuth() func(c *gin.Context) { token := c.GetHeader("X-Agent-Token") node, err := service.AuthenticateAccessToken(token) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "无权进行此操作,Agent Token 无效", - }) + response.RespondUnauthorized(c, "无权进行此操作,Agent Token 无效") c.Abort() return } @@ -32,10 +29,7 @@ func AgentRegisterAuth() func(c *gin.Context) { return } if err := service.ValidateDiscoveryToken(token); err != nil { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "无权进行此操作,注册 Token 无效", - }) + response.RespondUnauthorized(c, "无权进行此操作,注册 Token 无效") c.Abort() return } diff --git a/openflare_server/middleware/auth.go b/openflare_server/middleware/auth.go index 434d6605..0c100d5d 100644 --- a/openflare_server/middleware/auth.go +++ b/openflare_server/middleware/auth.go @@ -1,8 +1,8 @@ package middleware import ( - "net/http" "openflare/common" + "openflare/common/response" "openflare/model" jwt "github.com/appleboy/gin-jwt/v2" @@ -14,20 +14,14 @@ const OpenFlareTokenHeader = "OpenFlare-Token" func authHelper(c *gin.Context, minRole int) { tokenStr := c.GetHeader(OpenFlareTokenHeader) if tokenStr == "" { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "无权进行此操作,未登录或 token 无效", - }) + response.RespondUnauthorized(c, "无权进行此操作,未登录或 token 无效") c.Abort() return } token, err := JWTMiddleware.ParseTokenString(tokenStr) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "无权进行此操作,token 无效: " + err.Error(), - }) + response.RespondUnauthorized(c, "无权进行此操作,token 无效: "+err.Error()) c.Abort() return } @@ -35,10 +29,7 @@ func authHelper(c *gin.Context, minRole int) { claims := jwt.ExtractClaimsFromToken(token) id, ok := claims["id"].(float64) if !ok { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "无权进行此操作,token 格式错误", - }) + response.RespondUnauthorized(c, "无权进行此操作,token 格式错误") c.Abort() return } @@ -47,37 +38,25 @@ func authHelper(c *gin.Context, minRole int) { dbErr := model.DB.Select([]string{"id", "username", "display_name", "role", "status", "token"}). First(dbUser, "id = ?", int(id)).Error if dbErr != nil || dbUser.Username == "" { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "无权进行此操作,用户不存在", - }) + response.RespondUnauthorized(c, "无权进行此操作,用户不存在") c.Abort() return } if dbUser.Token != tokenStr { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "无权进行此操作,token 已失效或已登出", - }) + response.RespondUnauthorized(c, "无权进行此操作,token 已失效或已登出") c.Abort() return } if dbUser.Status == common.UserStatusDisabled { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "用户已被封禁", - }) + response.RespondFailure(c, "用户已被封禁") c.Abort() return } if int(dbUser.Role) < minRole { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "无权进行此操作,权限不足", - }) + response.RespondFailure(c, "无权进行此操作,权限不足") c.Abort() return } diff --git a/openflare_server/middleware/jwt.go b/openflare_server/middleware/jwt.go index a32be3e4..01ef6e2f 100644 --- a/openflare_server/middleware/jwt.go +++ b/openflare_server/middleware/jwt.go @@ -3,6 +3,7 @@ package middleware import ( "log" "openflare/common" + "openflare/common/response" "openflare/model" "time" @@ -57,10 +58,7 @@ func InitJWTMiddleware() { return data != nil }, Unauthorized: func(c *gin.Context, code int, message string) { - c.JSON(code, gin.H{ - "success": false, - "message": "无权进行此操作,未登录或 token 无效: " + message, - }) + response.RespondErrorWithStatus(c, code, "无权进行此操作,未登录或 token 无效: "+message) }, TokenLookup: "header: OpenFlare-Token", TokenHeadName: "", // Empty for raw token value directly diff --git a/openflare_server/middleware/relay-auth.go b/openflare_server/middleware/relay-auth.go index 7e9d72c3..1905c13f 100644 --- a/openflare_server/middleware/relay-auth.go +++ b/openflare_server/middleware/relay-auth.go @@ -2,7 +2,7 @@ package middleware import ( "github.com/gin-gonic/gin" - "net/http" + "openflare/common/response" "openflare/service" ) @@ -13,18 +13,12 @@ func RelayAuth() func(c *gin.Context) { token := c.GetHeader("X-Agent-Token") node, err := service.AuthenticateAccessToken(token) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "无权进行此操作,Agent Token 无效", - }) + response.RespondUnauthorized(c, "无权进行此操作,Agent Token 无效") c.Abort() return } if node.NodeType != "tunnel_relay" { - c.JSON(http.StatusForbidden, gin.H{ - "success": false, - "message": "此节点不是 TunnelRelay 类型", - }) + response.RespondForbidden(c, "此节点不是 TunnelRelay 类型") c.Abort() return } diff --git a/openflare_server/middleware/tunnel-auth.go b/openflare_server/middleware/tunnel-auth.go index af169438..b0ac85c7 100644 --- a/openflare_server/middleware/tunnel-auth.go +++ b/openflare_server/middleware/tunnel-auth.go @@ -1,7 +1,7 @@ package middleware import ( - "net/http" + "openflare/common/response" "openflare/service" "github.com/gin-gonic/gin" @@ -15,18 +15,12 @@ func TunnelAuth() func(c *gin.Context) { token := c.GetHeader("X-Tunnel-Token") node, err := service.AuthenticateAccessToken(token) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "无权进行此操作,Tunnel Token 无效", - }) + response.RespondUnauthorized(c, "无权进行此操作,Tunnel Token 无效") c.Abort() return } if node.NodeType != "tunnel_client" { - c.JSON(http.StatusForbidden, gin.H{ - "success": false, - "message": "此节点不是 TunnelClient 类型", - }) + response.RespondForbidden(c, "此节点不是 TunnelClient 类型") c.Abort() return }