yumi-golang/internal/router/middleware.go
2026-07-18 15:32:58 +08:00

210 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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())
}