Echo 和 oapi-codegen 应用程序中的 JWT 中间件问题导致多个 401 响应

问题描述 投票:0回答:1

我正在开发一个基于 Echo 的 Go 应用程序,其中使用 JWT 进行身份验证。我集成了 echo-jwt 中间件和 oapi-codegen 以进行 OpenAPI 验证。但是,我面临中间件和受保护的路由处理程序都返回 401 响应的问题。这会产生以下响应:

{"code": 401,"message": "invalid token"}
{"code": 401,"message": "missing or malformed JWT"}

第一个 401 响应来自中间件,第二个响应来自受保护的路由。当令牌无效或丢失时,中间件似乎没有正确停止请求,导致路由处理程序执行并返回另一个 401 错误。

这是我的主要功能和中间件的结构:

主要功能

package main

import (
    "context"
    "database/sql"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "auth-service/internal/application"
    "auth-service/internal/handlers"
    "auth-service/internal/middleware"
    "auth-service/internal/services/auth"
    "auth-service/pkg/database"
    "auth-service/pkg/ent"
    "auth-service/pkg/openapi"

    "github.com/labstack/echo/v4"
    echoMiddleware "github.com/labstack/echo/v4/middleware"
)

func main() {
    startPprofServer()

    e := setupEcho()
    db, client := setupDatabase()
    defer db.Close()
    defer client.Close()

    env := loadEnv()
    secretKey := env["SECRET_KEY"]

    tokenBlacklist := auth.NewTokenBlacklist(client)
    authService := auth.NewAuthService(client, secretKey, tokenBlacklist)

    // Initialize services and handlers
    services := initServices(authService)
    handlers := initHandlers(services)
    strictHandler := openapi.NewStrictHandler(handlers, nil)

    registerRoutes(e, strictHandler, tokenBlacklist, secretKey)

    // Start server with graceful shutdown
    go func() {
        if err := e.Start(":8080"); err != nil && err != http.ErrServerClosed {
            e.Logger.Fatal("shutting down the server")
        }
    }()

    gracefulShutdown(e)
}

func startPprofServer() {
    go func() {
        log.Println("Starting pprof server on :6060")
        if err := http.ListenAndServe("0.0.0.0:6060", nil); err != nil {
            log.Fatalf("Failed to start pprof server: %v", err)
        }
    }()
}

func setupEcho() *echo.Echo {
    e := echo.New()
    e.Use(echoMiddleware.Logger())
    e.Use(echoMiddleware.Recover())
    e.Use(echoMiddleware.CORSWithConfig(echoMiddleware.CORSConfig{
        AllowOrigins: []string{"http://localhost:9000"},
        AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
    }))
    e.HTTPErrorHandler = customHTTPErrorHandler
    return e
}

func setupDatabase() (*sql.DB, *ent.Client) {
    db, err := database.OpenDB()
    handleError(err)

    client, err := database.NewEntClient(db)
    handleError(err)

    database.ResetDatabase(db, client)
    return db, client
}

func handleError(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func loadEnv() map[string]string {
    env := map[string]string{
        "SECRET_KEY": os.Getenv("SECRET_KEY"),
    }

    for key, value := range env {
        if value == "" {
            log.Fatalf("%s environment variable is not set", key)
        }
    }
    return env
}

func initServices(authService *auth.AuthService) map[string]interface{} {
    return map[string]interface{}{
        "postLogin":      application.NewPostLogin(authService),
        "postRegister":   application.NewPostRegister(authService),
        "changeEmail":    application.NewChangeEmail(authService),
        "refreshToken":   application.NewRefreshToken(authService),
        "updatePassword": application.NewUpdatePassword(authService),
        "logout":         application.NewLogout(authService),
    }
}

func initHandlers(services map[string]interface{}) *handlers.CompositeHandler {
    return handlers.NewCompositeHandler(
        handlers.NewPostLoginHandler(services["postLogin"].(*application.PostLogin)),
        handlers.NewPostRegisterHandler(services["postRegister"].(*application.PostRegister)),
        handlers.NewChangeEmailHandler(services["changeEmail"].(*application.ChangeEmail)),
        handlers.NewRefreshTokenHandler(services["refreshToken"].(*application.RefreshToken)),
        handlers.NewUpdatePasswordHandler(services["updatePassword"].(*application.UpdatePassword)),
        handlers.NewLogoutHandler(services["logout"].(*application.Logout)),
    )
}

func registerRoutes(e *echo.Echo, strictHandler openapi.ServerInterface, tokenBlacklist *auth.TokenBlacklist, secretKey string) {
    jwtMiddleware := middleware.NewJWTMiddleware(secretKey, tokenBlacklist)

    e.POST("/login", func(c echo.Context) error {
        return strictHandler.PostLogin(c)
    })

    e.POST("/register", func(c echo.Context) error {
        return strictHandler.PostRegister(c)
    })

    e.POST("/refresh-token", func(c echo.Context) error {
        return strictHandler.PostRefreshToken(c)
    })

    protected := e.Group("/auth")
    protected.Use(jwtMiddleware.Handler())

    protected.POST("/change-email", func(c echo.Context) error {
        return strictHandler.PostChangeEmail(c)
    })
    protected.POST("/update-password", func(c echo.Context) error {
        return strictHandler.PostUpdatePassword(c)
    })
    protected.POST("/logout", func(c echo.Context) error {
        params := openapi.PostLogoutParams{
            RefreshToken: c.Request().Header.Get("Refresh-Token"),
        }
        return strictHandler.PostLogout(c, params)
    })
}


func customHTTPErrorHandler(err error, c echo.Context) {
    code := http.StatusInternalServerError
    if he, ok := err.(*echo.HTTPError); ok {
        code = he.Code
    }
    if code == http.StatusNotFound {
        c.JSON(http.StatusNotFound, map[string]string{
            "message": "Not Found",
        })
    } else {
        c.JSON(code, map[string]string{
            "message": err.Error(),
        })
    }
}

func gracefulShutdown(e *echo.Echo) {
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    if err := e.Shutdown(ctx); err != nil {
        e.Logger.Fatal(err)
    }
}

JWT 中间件

package middleware

import (
    "context"
    "net/http"

    "auth-service/internal/customresponses"
    "auth-service/internal/services/auth"

    "github.com/golang-jwt/jwt/v5"
    echojwt "github.com/labstack/echo-jwt/v4"
    "github.com/labstack/echo/v4"
)

type JWTMiddleware struct {
    SecretKey      string
    TokenBlacklist *auth.TokenBlacklist
}

func NewJWTMiddleware(secretKey string, tokenBlacklist *auth.TokenBlacklist) *JWTMiddleware {
    return &JWTMiddleware{
        SecretKey:      secretKey,
        TokenBlacklist: tokenBlacklist,
    }
}

func (m *JWTMiddleware) Handler() echo.MiddlewareFunc {
    config := echojwt.Config{
        SigningKey: []byte(m.SecretKey),
        ErrorHandler: func(c echo.Context, err error) error {
            var message string
            if err == echojwt.ErrJWTMissing {
                message = "missing or malformed JWT"
            } else {
                message = "invalid token"
            }
            // Return the response and stop further execution
            return c.JSON(http.StatusUnauthorized, customresponses.CreateErrorResponse(http.StatusUnauthorized, message))
        },
        SuccessHandler: func(c echo.Context) {
            userToken, ok := c.Get("user").(*jwt.Token)
            if !ok {
                // Return the response and stop further execution
                c.JSON(http.StatusUnauthorized, customresponses.CreateErrorResponse(http.StatusUnauthorized, "invalid token"))
                c.Response().Flush()
                return
            }

            tokenString := userToken.Raw
            isBlacklisted, err := m.TokenBlacklist.IsBlacklisted(tokenString)
            if err != nil {
                // Return the response and stop further execution
                c.JSON(http.StatusInternalServerError, customresponses.CreateErrorResponse(http.StatusInternalServerError, "internal server error"))
                c.Response().Flush()
                return
            }
            if isBlacklisted {
                // Return the response and stop further execution
                c.JSON(http.StatusUnauthorized, customresponses.CreateErrorResponse(http.StatusUnauthorized, "invalid token"))
                c.Response().Flush()
                return
            }

            // Store the Echo context in the request context
            c.SetRequest(c.Request().WithContext(context.WithValue(c.Request().Context(), "echoContext", c)))
        },
        ContinueOnIgnoredError: false, // Ensure this is set to false to stop the request on error
    }

    return echojwt.WithConfig(config)
}

客户回复

package customresponses

import ( "auth-service/pkg/openapi" )

// CreateErrorResponse creates a structured error response func CreateErrorResponse(code int, message string) openapi.ErrorResponse { return openapi.ErrorResponse{ Code:    &code, Message: &message, } }

当我调用受保护的路由时,我收到两个 401 响应:一个来自中间件,另一个来自受保护的路由处理程序。看来中间件在发生错误时不会停止请求,从而允许路由处理程序也执行并返回另一个 401 错误。我尝试了各种解决方案,但问题仍然存在。如何确保中间件在检测到错误时正确停止执行流程?

我尝试过的事情

配置 JWT 中间件:

  • 将ContinueOnIgnoredError 设置为 false 以在出现错误时停止进一步执行。 使用自定义错误处理来区分丢失和无效的令牌。 确保中间件订单:

  • 在受保护的路由之前应用中间件。

正确的错误处理:

  • 尝试返回响应并停止错误处理程序中的进一步执行。 预期行为 当向受保护的路由发出包含无效或丢失 JWT 的请求时,中间件应返回 401 响应并停止进一步执行,以防止调用受保护的路由处理程序。

实际行为:

  • 中间件会针对无效或缺失的 JWT 返回 401 响应,但受保护的路由处理程序也会被调用并返回另一个 401 响应,从而导致单个请求出现多个 401 响应。

请帮助我,我是初学者,我真的很想解决中间件和受保护的路由处理程序都返回 401 响应的问题,从而导致单个请求出现多个 401 响应。

如何确保 JWT 中间件正确停止执行并防止在令牌无效或丢失时调用路由处理程序?任何见解或建议将不胜感激。

go openapi openapi-generator go-echo
1个回答
0
投票

文档说:

ErrorHandler 定义了一个函数,当所有查找完成并且没有一个通过 Validator 时执行该函数 功能。 ErrorHandler 在最后一个缺失 (ErrExtractionValueMissing) 或无效密钥的情况下执行。 它可用于定义自定义 JWT 错误。

注意:当错误处理程序吞下错误(返回 nil)时,中间件继续向处理程序执行处理程序链。 当您的网站/API 的一部分可公开访问并且为授权用户提供额外功能时,这非常有用 在这种情况下,您可以使用 ErrorHandler 设置默认的公共 JWT 令牌值来请求并继续处理程序链。

在你的

ErrorHandler
你:

// Return the response and stop further execution
return c.JSON(http.StatusUnauthorized, customresponses.CreateErrorResponse(http.StatusUnauthorized, message))
如果错误成功序列化,

c.JSON
将返回
nil
(这是我期望发生的情况);因此,您实际上正在接受错误(并且链式处理程序将运行)。要解决此问题,请不要返回
nil

© www.soinside.com 2019 - 2024. All rights reserved.