Compare commits

..
Author SHA1 Message Date
zhangwenjian be3c4452e3 test✅: pin the jwt handler being retrievable and shared
Two properties the previous shape broke silently: GetHandlerFunc must
report ok for the JwtToken key, and every module must read back the same
instance. Reverting the registration to the unbound method expression
still compiles and turns the first of these red, which is the failure
this pins.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:26:54 +08:00
zhangwenjian 5b01c9ada8 fix🐛: build the shared jwt middleware instance once in InitMiddleware
Four modules each called AuthInit and built their own instance, so which
one Runtime handed back was decided by whichever module initialised last.
The JwtToken key was also registered as an unbound method expression,
which GetHandlerFunc's type assertion can never match - the key was
registered and unusable at the same time.

The instance is now built once here and registered as a bound closure.
Modules read it back through GetAuthMiddleware, which is fatal rather
than nil when called before InitMiddleware has run: a process without a
JWT middleware should not reach the point of serving a request.

Only one call site needs the instance itself rather than the handler
(admin's /login, for LoginHandler); the thirty-odd MiddlewareFunc() call
sites are unchanged.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:26:54 +08:00
wenjianzhang ffd82a6a10 Merge pull request #898 from go-admin-team/feat/006-shim
refactor🎨: 契约包改为 core 的薄壳
2026-09-05 10:26:52 +08:00
7 changed files with 147 additions and 25 deletions
+3 -5
View File
@@ -25,11 +25,9 @@ func InitRouter() {
os.Exit(-1)
}
// the jwt middleware
authMiddleware, err := common.AuthInit()
if err != nil {
log.Fatalf("JWT Init Error, %s", err.Error())
}
// the jwt middleware: shared instance InitMiddleware built at startup,
// not one built here per module (see common/middleware.GetAuthMiddleware).
authMiddleware := common.GetAuthMiddleware()
// 注册系统路由
InitSysRouter(r, authMiddleware)
+3 -5
View File
@@ -33,11 +33,9 @@ func InitRouter() {
os.Exit(-1)
}
// the jwt middleware
authMiddleware, err := common.AuthInit()
if err != nil {
log.Fatalf("JWT Init Error, %s", err.Error())
}
// the jwt middleware: shared instance InitMiddleware built at startup,
// not one built here per module (see common/middleware.GetAuthMiddleware).
authMiddleware := common.GetAuthMiddleware()
// 注册业务路由
InitBusinessRouter(r, authMiddleware)
+3 -4
View File
@@ -26,10 +26,9 @@ func InitRouter() {
os.Exit(-1)
}
authMiddleware, err := common.AuthInit()
if err != nil {
log.Fatalf("JWT Init Error, %s", err.Error())
}
// the jwt middleware: shared instance InitMiddleware built at startup,
// not one built here per module (see common/middleware.GetAuthMiddleware).
authMiddleware := common.GetAuthMiddleware()
// 注册业务路由
initRouter(r, authMiddleware)
+3 -4
View File
@@ -25,10 +25,9 @@ func InitRouter() {
os.Exit(-1)
}
// the jwt middleware
authMiddleware, err := common.AuthInit()
if err != nil {
log.Fatalf("JWT Init Error, %s", err.Error())
}
// the jwt middleware: shared instance InitMiddleware built at startup,
// not one built here per module (see common/middleware.GetAuthMiddleware).
authMiddleware := common.GetAuthMiddleware()
// 注册业务路由
// TODO: 这里可存放业务路由,里边并无实际路由只有演示代码
+29 -2
View File
@@ -3,11 +3,19 @@ package middleware
import (
"time"
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
log "github.com/go-admin-team/go-admin-core/v2/logger"
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
"go-admin/common/middleware/handler"
)
// authMiddleware is the single JWT middleware instance the whole process
// shares. InitMiddleware builds it once, before any module registers its
// routes; GetAuthMiddleware is how a module gets it back instead of calling
// AuthInit itself and building another, functionally-equivalent-but-distinct
// instance.
var authMiddleware *jwt.GinJWTMiddleware
// AuthInit jwt验证new
func AuthInit() (*jwt.GinJWTMiddleware, error) {
timeout := time.Hour
@@ -33,4 +41,23 @@ func AuthInit() (*jwt.GinJWTMiddleware, error) {
TimeFunc: time.Now,
})
}
}
// GetAuthMiddleware returns the shared JWT middleware instance InitMiddleware
// built at startup. Application modules (app/admin, app/jobs, app/other,
// app/demo) call this instead of AuthInit so their router chains - which
// still need the instance itself for authMiddleware.MiddlewareFunc() and
// authMiddleware.LoginHandler, not just the bound closure registered under
// sdk.Runtime's JwtTokenCheck key - end up using the same instance the host
// registered, rather than one each.
//
// It fails loudly instead of returning nil: an InitRouter that runs before
// InitMiddleware has a real startup-ordering bug, not a case to paper over
// with a nil *jwt.GinJWTMiddleware that would panic much further down the
// call chain with a far less useful stack trace.
func GetAuthMiddleware() *jwt.GinJWTMiddleware {
if authMiddleware == nil {
log.Fatal("JWT middleware not initialized; InitMiddleware must run before any module's InitRouter")
}
return authMiddleware
}
+29 -5
View File
@@ -2,15 +2,20 @@ package middleware
import (
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
log "github.com/go-admin-team/go-admin-core/v2/logger"
"github.com/go-admin-team/go-admin-core/v2/sdk"
"github.com/go-admin-team/go-admin-core/v2/sdk/runtime"
"go-admin/common/actions"
)
// These alias core's own constants (see sdk/runtime.GetHandlerFunc's contract
// doc, section 9) rather than redeclaring the same three strings, so a typo
// here can no longer split registration and lookup into two different keys
// that both happen to compile.
const (
JwtTokenCheck string = "JwtToken"
RoleCheck string = "AuthCheckRole"
PermissionCheck string = "PermissionAction"
JwtTokenCheck = runtime.JwtTokenCheck
RoleCheck = runtime.RoleCheck
PermissionCheck = runtime.PermissionCheck
)
func InitMiddleware(r *gin.Engine) {
@@ -29,7 +34,26 @@ func InitMiddleware(r *gin.Engine) {
r.Use(Secure)
// 链路追踪
//r.Use(middleware.Trace())
sdk.Runtime.SetMiddleware(JwtTokenCheck, (*jwt.GinJWTMiddleware).MiddlewareFunc)
// Build the shared JWT middleware instance here, before any module
// registers routes (initRouter runs ahead of runStartupHooks, which is
// what invokes each module's InitRouter - see cmd/api/server.go). Doing
// it once here, instead of once per module via AuthInit, is what makes
// GetAuthMiddleware and sdk.Runtime.GetHandlerFunc(JwtTokenCheck) both
// resolve to a single, meaningful instance instead of "whichever module
// happened to initialize last".
//
// SetMiddleware must be given a bound closure (authMiddleware.MiddlewareFunc()),
// not the unbound method expression (*jwt.GinJWTMiddleware).MiddlewareFunc:
// the latter has no receiver bound to it, so GetHandlerFunc's type
// assertion to gin.HandlerFunc always fails for it.
var err error
authMiddleware, err = AuthInit()
if err != nil {
// A process with no JWT middleware must not start serving requests.
log.Fatalf("JWT Init Error, %s", err.Error())
}
sdk.Runtime.SetMiddleware(JwtTokenCheck, authMiddleware.MiddlewareFunc())
sdk.Runtime.SetMiddleware(RoleCheck, AuthCheckRole())
sdk.Runtime.SetMiddleware(PermissionCheck, actions.PermissionAction())
}
+77
View File
@@ -0,0 +1,77 @@
package middleware
import (
"testing"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/v2/sdk"
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
"github.com/go-admin-team/go-admin-core/v2/sdk/runtime"
)
// freshRuntime hands the test its own Runtime and puts the old one back, the
// same pattern cmd/api/server_test.go uses: sdk.Runtime is a process-wide
// singleton, and a test that registers into it would otherwise leak state
// into every other test in the binary.
func freshRuntime(t *testing.T) {
t.Helper()
previous := sdk.Runtime
t.Cleanup(func() { sdk.Runtime = previous })
sdk.Runtime = runtime.NewConfig()
}
// TestInitMiddlewareRegistersUsableJwtHandlerFunc is the reverse proof for
// hoisting the JWT instance's construction into InitMiddleware:
// sdk.Runtime.GetHandlerFunc(JwtTokenCheck) must hand back ok=true and a
// non-nil gin.HandlerFunc, not just something GetMiddleware can return as an
// untyped interface{}.
//
// Before this change, InitMiddleware registered the unbound method
// expression (*jwt.GinJWTMiddleware).MiddlewareFunc under this key - a value
// with no receiver bound to it, which is not a gin.HandlerFunc no matter how
// a caller asserts its type. Reverting the registration below to that
// expression makes GetHandlerFunc report ok=false; it does not fail to
// compile, because (*jwt.GinJWTMiddleware).MiddlewareFunc has a well-formed,
// unrelated method-expression type that SetMiddleware's interface{} param
// happily accepts.
func TestInitMiddlewareRegistersUsableJwtHandlerFunc(t *testing.T) {
freshRuntime(t)
previousSecret := config.JwtConfig.Secret
config.JwtConfig.Secret = "test-secret-key"
t.Cleanup(func() { config.JwtConfig.Secret = previousSecret })
gin.SetMode(gin.TestMode)
InitMiddleware(gin.New())
h, ok := sdk.Runtime.GetHandlerFunc(JwtTokenCheck)
if !ok {
t.Fatal("GetHandlerFunc(JwtTokenCheck) reported ok=false after InitMiddleware ran")
}
if h == nil {
t.Fatal("GetHandlerFunc(JwtTokenCheck) reported ok=true but returned a nil handler")
}
}
// TestInitMiddlewareBuildsOneSharedJwtInstance locks down the fix for the
// four-instances problem: GetAuthMiddleware must return the very instance
// InitMiddleware built and handed to sdk.Runtime, not a lookalike built
// separately by whichever caller asks first.
func TestInitMiddlewareBuildsOneSharedJwtInstance(t *testing.T) {
freshRuntime(t)
previousSecret := config.JwtConfig.Secret
config.JwtConfig.Secret = "test-secret-key"
t.Cleanup(func() { config.JwtConfig.Secret = previousSecret })
gin.SetMode(gin.TestMode)
InitMiddleware(gin.New())
shared := GetAuthMiddleware()
if shared == nil {
t.Fatal("GetAuthMiddleware returned nil after InitMiddleware ran")
}
if shared != authMiddleware {
t.Error("GetAuthMiddleware did not return the package-level instance InitMiddleware built")
}
}