我正在开发一个基于 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 以在出现错误时停止进一步执行。 使用自定义错误处理来区分丢失和无效的令牌。 确保中间件订单:
在受保护的路由之前应用中间件。
正确的错误处理:
实际行为:
请帮助我,我是初学者,我真的很想解决中间件和受保护的路由处理程序都返回 401 响应的问题,从而导致单个请求出现多个 401 响应。
如何确保 JWT 中间件正确停止执行并防止在令牌无效或丢失时调用路由处理程序?任何见解或建议将不胜感激。
文档说:
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
。