From 66bc98ca8aba0ee0fecda4bd263afeee0b666b60 Mon Sep 17 00:00:00 2001 From: xchou Date: Sun, 9 Aug 2026 01:16:53 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E7=8A=B6=E6=80=81=E7=A0=81=E7=BA=A6=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.go | 28 ++------------- docs/swagger.json | 28 ++------------- docs/swagger.yaml | 20 ++--------- middlewares/jwt.go | 2 +- middlewares/jwt_test.go | 23 ++++++++++++- modules/auth/controller.go | 36 +++++++++---------- modules/auth/controller_test.go | 61 +++++++++++++++++++++++++++++++++ 7 files changed, 106 insertions(+), 92 deletions(-) create mode 100644 modules/auth/controller_test.go diff --git a/docs/docs.go b/docs/docs.go index 13acbc2..90f1753 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -57,12 +57,6 @@ const docTemplate = `{ } ] } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/utils.APIResponse" - } } } } @@ -108,12 +102,6 @@ const docTemplate = `{ } ] } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/utils.APIResponse" - } } } } @@ -142,8 +130,8 @@ const docTemplate = `{ } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { "allOf": [ { @@ -159,18 +147,6 @@ const docTemplate = `{ } ] } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/utils.APIResponse" - } - }, - "409": { - "description": "Conflict", - "schema": { - "$ref": "#/definitions/utils.APIResponse" - } } } } diff --git a/docs/swagger.json b/docs/swagger.json index 7ee9b0e..38ba56d 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -49,12 +49,6 @@ } ] } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/utils.APIResponse" - } } } } @@ -100,12 +94,6 @@ } ] } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/utils.APIResponse" - } } } } @@ -134,8 +122,8 @@ } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { "allOf": [ { @@ -151,18 +139,6 @@ } ] } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/utils.APIResponse" - } - }, - "409": { - "description": "Conflict", - "schema": { - "$ref": "#/definitions/utils.APIResponse" - } } } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e7f1822..2470170 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -108,10 +108,6 @@ paths: data: $ref: '#/definitions/middlewares.TokenPair' type: object - "401": - description: Unauthorized - schema: - $ref: '#/definitions/utils.APIResponse' summary: 用户登录 tags: - auth @@ -138,10 +134,6 @@ paths: data: $ref: '#/definitions/middlewares.TokenPair' type: object - "401": - description: Unauthorized - schema: - $ref: '#/definitions/utils.APIResponse' summary: 刷新并轮换 Token tags: - auth @@ -159,8 +151,8 @@ paths: produces: - application/json responses: - "201": - description: Created + "200": + description: OK schema: allOf: - $ref: '#/definitions/utils.APIResponse' @@ -168,14 +160,6 @@ paths: data: $ref: '#/definitions/auth.UserResponse' type: object - "400": - description: Bad Request - schema: - $ref: '#/definitions/utils.APIResponse' - "409": - description: Conflict - schema: - $ref: '#/definitions/utils.APIResponse' summary: 注册示例用户 tags: - auth diff --git a/middlewares/jwt.go b/middlewares/jwt.go index 46f86d7..151da2a 100644 --- a/middlewares/jwt.go +++ b/middlewares/jwt.go @@ -136,7 +136,7 @@ func BearerToken(header string) (string, error) { func abortUnauthorized(c *gin.Context, message string) { Logger.Warn("JWT认证失败", zap.String("message", message), zap.String("path", c.Request.URL.Path)) - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": message, "data": nil}) + c.AbortWithStatusJSON(http.StatusOK, gin.H{"code": 401, "message": message, "data": nil}) } func GetCurrentUser(c *gin.Context) (int, string, bool) { diff --git a/middlewares/jwt_test.go b/middlewares/jwt_test.go index 6c5aed0..97046f3 100644 --- a/middlewares/jwt_test.go +++ b/middlewares/jwt_test.go @@ -1,6 +1,13 @@ package middlewares -import "testing" +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) func TestGenerateTokenPair(t *testing.T) { InitJWT("test-secret", 15, 24, "test-issuer") @@ -24,6 +31,20 @@ func TestGenerateTokenPair(t *testing.T) { } } +func TestJWTAuthFailureUsesHTTP200(t *testing.T) { + Logger = zap.NewNop() + gin.SetMode(gin.TestMode) + router := gin.New() + router.GET("/private", JWTAuth(), func(c *gin.Context) { + c.Status(http.StatusNoContent) + }) + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/private", nil)) + if recorder.Code != http.StatusOK { + t.Fatalf("HTTP status = %d, want 200", recorder.Code) + } +} + func TestBearerToken(t *testing.T) { if token, err := BearerToken("Bearer abc"); err != nil || token != "abc" { t.Fatalf("BearerToken() = %q, %v", token, err) diff --git a/modules/auth/controller.go b/modules/auth/controller.go index 4067b11..2560791 100644 --- a/modules/auth/controller.go +++ b/modules/auth/controller.go @@ -25,27 +25,25 @@ func NewController() *Controller { return &Controller{} } // @Accept json // @Produce json // @Param body body Credentials true "注册信息" -// @Success 201 {object} utils.APIResponse{data=UserResponse} -// @Failure 400 {object} utils.APIResponse -// @Failure 409 {object} utils.APIResponse +// @Success 200 {object} utils.APIResponse{data=UserResponse} // @Router /auth/register [post] func (c *Controller) Register(ctx *gin.Context) { var input Credentials if err := ctx.ShouldBindJSON(&input); err != nil { - ctx.JSON(http.StatusBadRequest, utils.Failure(400, err.Error())) + ctx.JSON(http.StatusOK, utils.Failure(400, err.Error())) return } hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.Failure(500, "密码处理失败")) + ctx.JSON(http.StatusOK, utils.Failure(500, "密码处理失败")) return } user := models.User{Username: input.Username, Password: string(hash)} if err := database.GetDB().Create(&user).Error; err != nil { - ctx.JSON(http.StatusConflict, utils.Failure(409, "用户名已存在或数据无效")) + ctx.JSON(http.StatusOK, utils.Failure(409, "用户名已存在或数据无效")) return } - ctx.JSON(http.StatusCreated, utils.Success(UserResponse{ID: user.ID, Username: user.Username})) + ctx.JSON(http.StatusOK, utils.Success(UserResponse{ID: user.ID, Username: user.Username})) } // Login godoc @@ -55,31 +53,30 @@ func (c *Controller) Register(ctx *gin.Context) { // @Produce json // @Param body body Credentials true "登录信息" // @Success 200 {object} utils.APIResponse{data=middlewares.TokenPair} -// @Failure 401 {object} utils.APIResponse // @Router /auth/login [post] func (c *Controller) Login(ctx *gin.Context) { var input Credentials if err := ctx.ShouldBindJSON(&input); err != nil { - ctx.JSON(http.StatusBadRequest, utils.Failure(400, err.Error())) + ctx.JSON(http.StatusOK, utils.Failure(400, err.Error())) return } var user models.User err := database.GetDB().Where("username = ?", input.Username).First(&user).Error if errors.Is(err, gorm.ErrRecordNotFound) { - ctx.JSON(http.StatusUnauthorized, utils.Failure(401, "用户名或密码错误")) + ctx.JSON(http.StatusOK, utils.Failure(401, "用户名或密码错误")) return } if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.Failure(500, "查询用户失败")) + ctx.JSON(http.StatusOK, utils.Failure(500, "查询用户失败")) return } if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)) != nil { - ctx.JSON(http.StatusUnauthorized, utils.Failure(401, "用户名或密码错误")) + ctx.JSON(http.StatusOK, utils.Failure(401, "用户名或密码错误")) return } pair, err := middlewares.GenerateTokenPair(int(user.ID), user.Username) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.Failure(500, "生成 token 失败")) + ctx.JSON(http.StatusOK, utils.Failure(500, "生成 token 失败")) return } ctx.JSON(http.StatusOK, utils.Success(pair)) @@ -92,31 +89,30 @@ func (c *Controller) Login(ctx *gin.Context) { // @Produce json // @Param body body RefreshRequest true "Refresh Token" // @Success 200 {object} utils.APIResponse{data=middlewares.TokenPair} -// @Failure 401 {object} utils.APIResponse // @Router /auth/refresh [post] func (c *Controller) Refresh(ctx *gin.Context) { var input RefreshRequest if err := ctx.ShouldBindJSON(&input); err != nil { - ctx.JSON(http.StatusBadRequest, utils.Failure(400, err.Error())) + ctx.JSON(http.StatusOK, utils.Failure(400, err.Error())) return } claims, err := middlewares.ParseToken(input.RefreshToken) if err != nil || claims.TokenType != middlewares.TokenTypeRefresh { - ctx.JSON(http.StatusUnauthorized, utils.Failure(401, "refresh token 无效或已过期")) + ctx.JSON(http.StatusOK, utils.Failure(401, "refresh token 无效或已过期")) return } revoked, err := database.IsTokenRevoked(ctx.Request.Context(), claims.ID) if err != nil || revoked { - ctx.JSON(http.StatusUnauthorized, utils.Failure(401, "refresh token 已撤销")) + ctx.JSON(http.StatusOK, utils.Failure(401, "refresh token 已撤销")) return } if err := revokeClaims(ctx, claims); err != nil { - ctx.JSON(http.StatusInternalServerError, utils.Failure(500, "撤销旧 token 失败")) + ctx.JSON(http.StatusOK, utils.Failure(500, "撤销旧 token 失败")) return } pair, err := middlewares.GenerateTokenPair(claims.UserID, claims.Username) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.Failure(500, "生成 token 失败")) + ctx.JSON(http.StatusOK, utils.Failure(500, "生成 token 失败")) return } ctx.JSON(http.StatusOK, utils.Success(pair)) @@ -134,7 +130,7 @@ func (c *Controller) Refresh(ctx *gin.Context) { func (c *Controller) Logout(ctx *gin.Context) { claims, _ := middlewares.GetCurrentClaims(ctx) if err := revokeClaims(ctx, claims); err != nil { - ctx.JSON(http.StatusInternalServerError, utils.Failure(500, "撤销 access token 失败")) + ctx.JSON(http.StatusOK, utils.Failure(500, "撤销 access token 失败")) return } var input LogoutRequest diff --git a/modules/auth/controller_test.go b/modules/auth/controller_test.go new file mode 100644 index 0000000..6a39b33 --- /dev/null +++ b/modules/auth/controller_test.go @@ -0,0 +1,61 @@ +//go:build cgo + +package auth + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "skeleton/config" + "skeleton/database" + "skeleton/middlewares" + "skeleton/models" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +func TestBusinessErrorsUseHTTP200(t *testing.T) { + cfg := config.DatabaseConfig{Driver: "sqlite", DSN: ":memory:", MaxIdleConns: 1, MaxOpenConns: 1} + if err := database.Init(&cfg, zap.NewNop()); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = database.Close() }) + if err := database.GetDB().AutoMigrate(&models.User{}); err != nil { + t.Fatal(err) + } + middlewares.InitJWT("test-secret", 15, 24, "test") + + gin.SetMode(gin.TestMode) + controller := NewController() + router := gin.New() + router.POST("/register", controller.Register) + router.POST("/login", controller.Login) + + assertBusinessResponse(t, router, "/register", `{"username":"x","password":"short"}`, 400) + assertBusinessResponse(t, router, "/register", `{"username":"demo","password":"change-me-123"}`, 200) + assertBusinessResponse(t, router, "/login", `{"username":"demo","password":"wrong-password"}`, 401) +} + +func assertBusinessResponse(t *testing.T, handler http.Handler, path, body string, wantCode int) { + t.Helper() + request := httptest.NewRequest(http.MethodPost, path, bytes.NewBufferString(body)) + request.Header.Set("Content-Type", "application/json") + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Fatalf("HTTP status = %d, want 200; body=%s", recorder.Code, recorder.Body.String()) + } + var response struct { + Code int `json:"code"` + } + if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { + t.Fatal(err) + } + if response.Code != wantCode { + t.Fatalf("business code = %d, want %d; body=%s", response.Code, wantCode, recorder.Body.String()) + } +}