package router import ( "context" "crypto/subtle" "net/http" "strings" "time" "chatapp3-golang/internal/common" "chatapp3-golang/internal/integration" "github.com/gin-gonic/gin" ) const authUserContextKey = "auth_user" type authGateway interface { AuthenticateToken(ctx context.Context, token string) (integration.UserCredential, error) AuthenticateConsoleToken(ctx context.Context, authorization string) (integration.ConsoleAccount, error) } type consolePermissionGateway interface { ListConsoleButtonAliases(ctx context.Context, authorization string) (map[string]struct{}, error) } type consoleMenuGateway interface { HasConsoleMenuAlias(ctx context.Context, authorization, requiredAlias string) (bool, error) } // authMiddleware 校验用户 token,并把认证后的用户信息写入上下文。 func authMiddleware(javaClient authGateway) gin.HandlerFunc { return func(c *gin.Context) { authorization := strings.TrimSpace(c.GetHeader("Authorization")) if authorization == "" { writeError(c, common.NewAppError(http.StatusUnauthorized, "missing_authorization", "authorization is required")) c.Abort() return } token := trimBearer(authorization) ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second) defer cancel() credential, err := javaClient.AuthenticateToken(ctx, token) if err != nil { writeError(c, common.NewAppError(http.StatusUnauthorized, "invalid_token", err.Error())) c.Abort() return } c.Set(authUserContextKey, common.AuthUser{ UserID: int64(credential.UserID), SysOrigin: credential.SysOrigin, Token: token, Authorization: authorization, }) c.Next() } } // consoleAuthMiddleware 校验后台 console token,供后台管理类 Go 路由使用。 func consoleAuthMiddleware(javaClient authGateway) gin.HandlerFunc { return func(c *gin.Context) { authorization := strings.TrimSpace(c.GetHeader("Authorization")) if authorization == "" { writeError(c, common.NewAppError(http.StatusUnauthorized, "missing_authorization", "authorization is required")) c.Abort() return } ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second) defer cancel() if _, err := javaClient.AuthenticateConsoleToken(ctx, authorization); err != nil { writeError(c, common.NewAppError(http.StatusUnauthorized, "invalid_console_token", err.Error())) c.Abort() return } c.Next() } } // consolePermissionMiddleware 在写接口执行前读取 Java Console 的真实按钮权限,禁止只依赖前端隐藏按钮。 func consolePermissionMiddleware(javaClient authGateway, requiredAlias string) gin.HandlerFunc { return func(c *gin.Context) { gateway, ok := javaClient.(consolePermissionGateway) if !ok { writeError(c, common.NewAppError(http.StatusServiceUnavailable, "permission_gateway_unavailable", "console permission gateway is unavailable")) c.Abort() return } ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second) defer cancel() aliases, err := gateway.ListConsoleButtonAliases(ctx, strings.TrimSpace(c.GetHeader("Authorization"))) if err != nil { writeError(c, common.NewAppError(http.StatusForbidden, "permission_check_failed", err.Error())) c.Abort() return } if _, allowed := aliases[requiredAlias]; !allowed { writeError(c, common.NewAppError(http.StatusForbidden, "permission_denied", "required permission: "+requiredAlias)) c.Abort() return } c.Next() } } // consoleMenuMiddleware 校验页面菜单权限,所有管理端 GET 都不能只依赖前端路由可见性。 func consoleMenuMiddleware(javaClient authGateway, requiredAlias string) gin.HandlerFunc { return func(c *gin.Context) { gateway, ok := javaClient.(consoleMenuGateway) if !ok { writeError(c, common.NewAppError(http.StatusServiceUnavailable, "menu_gateway_unavailable", "console menu gateway is unavailable")) c.Abort() return } ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second) defer cancel() allowed, err := gateway.HasConsoleMenuAlias(ctx, strings.TrimSpace(c.GetHeader("Authorization")), requiredAlias) if err != nil { writeError(c, common.NewAppError(http.StatusForbidden, "menu_permission_check_failed", err.Error())) c.Abort() return } if !allowed { writeError(c, common.NewAppError(http.StatusForbidden, "permission_denied", "required menu: "+requiredAlias)) c.Abort() return } c.Next() } } // internalSecretMiddleware 校验内部回调密钥。 func internalSecretMiddleware(secret string) gin.HandlerFunc { return func(c *gin.Context) { if strings.TrimSpace(secret) == "" { c.Next() return } token := strings.TrimSpace(c.GetHeader("X-Internal-Token")) if token != secret { writeError(c, common.NewAppError(http.StatusUnauthorized, "invalid_internal_token", "internal token is invalid")) c.Abort() return } c.Next() } } // requiredInternalSecretMiddleware 用于会触发资金/奖励副作用的内部入口。 // 与兼容历史调用的 internalSecretMiddleware 不同,服务端未配置密钥时必须 fail-closed, // 否则错误的部署配置会把无鉴权请求直接放进结算状态机。 func requiredInternalSecretMiddleware(secret string) gin.HandlerFunc { configuredSecret := strings.TrimSpace(secret) return func(c *gin.Context) { if configuredSecret == "" { writeError(c, common.NewAppError(http.StatusServiceUnavailable, "internal_secret_not_configured", "internal callback secret is not configured")) c.Abort() return } token := strings.TrimSpace(c.GetHeader("X-Internal-Token")) if subtle.ConstantTimeCompare([]byte(token), []byte(configuredSecret)) != 1 { writeError(c, common.NewAppError(http.StatusUnauthorized, "invalid_internal_token", "internal token is invalid")) c.Abort() return } c.Next() } } // mustAuthUser 从 Gin 上下文读取已经通过鉴权的用户。 func mustAuthUser(c *gin.Context) common.AuthUser { value, exists := c.Get(authUserContextKey) if !exists { return common.AuthUser{} } user, _ := value.(common.AuthUser) return user } // trimBearer 去掉 Authorization 里的 Bearer 前缀。 func trimBearer(authorization string) string { if strings.HasPrefix(strings.ToLower(authorization), "bearer ") { return strings.TrimSpace(authorization[7:]) } return authorization } // resolveClientIP 解析请求来源 IP。 func resolveClientIP(c *gin.Context) string { if realIP := strings.TrimSpace(c.GetHeader("X-Real-IP")); realIP != "" { return realIP } if forwarded := strings.TrimSpace(c.GetHeader("X-Forwarded-For")); forwarded != "" { parts := strings.Split(forwarded, ",") if len(parts) > 0 { return strings.TrimSpace(parts[0]) } } return strings.TrimSpace(c.ClientIP()) }