Files
team-api/internal/middleware/admin_auth.go
T

234 lines
6.5 KiB
Go

package middleware
import (
"context"
"strings"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/model/entity"
"github.com/qianfree/team-api/internal/consts"
"github.com/qianfree/team-api/internal/logic/common"
"github.com/qianfree/team-api/internal/response"
)
// AuthContextKey is the key used to store auth info in context.
const (
CtxKeyUserID = "userId"
CtxKeyUserType = "userType"
CtxKeyRole = "role"
CtxKeyTenantID = "tenantId"
CtxKeySessionID = "sessionId"
CtxKeyJti = "jti"
CtxKeyApiKeyID = "apiKeyId"
CtxKeyProjectID = "projectId"
CtxKeyApiKeyRateLimitQps = "apiKeyRateLimitQps"
CtxKeyApiKeyRateLimitConcurrency = "apiKeyRateLimitConcurrency"
CtxKeyApiKeyIpWhitelist = "apiKeyIpWhitelist"
CtxKeyApiKeyTotalQuota = "apiKeyTotalQuota"
CtxKeyApiKeyUsedQuota = "apiKeyUsedQuota"
)
// adminPublicPaths lists admin routes that skip JWT auth.
// Keep in sync with api/admin/v1/ structs tagged group:"public" middleware:"-".
var adminPublicPaths = map[string]bool{
"/api/admin/auth/login": true,
"/api/admin/auth/refresh": true,
"/api/admin/auth/2fa/verify": true,
}
func isAdminPublicPath(path string) bool {
return adminPublicPaths[path]
}
// AdminAuth is JWT authentication middleware for admin backend.
func AdminAuth(r *ghttp.Request) {
// g.Meta middleware:"-" only skips service middleware, not group middleware.
// Public endpoints must be checked explicitly here.
if isAdminPublicPath(r.URL.Path) {
r.Middleware.Next()
return
}
tokenStr := extractBearerToken(r)
if tokenStr == "" {
response.ErrorMsg(r, consts.CodeUnauthorized, consts.MsgUnauthorized)
return
}
claims, err := common.ParseAccessToken(tokenStr)
if err != nil {
response.ErrorMsg(r, consts.CodeUnauthorized, consts.MsgUnauthorized)
return
}
// Verify user type
if claims.UserType != "admin" {
response.ErrorMsg(r, consts.CodeForbidden, consts.MsgForbidden)
return
}
// Check if session is revoked by jti (JWT ID).
// Note: tokens without jti cannot be revoked — this is by design for backward compat.
if claims.ID != "" && common.IsSessionRevoked(r.Context(), claims.ID) {
response.ErrorWithCode(r, consts.CodeUnauthorized, consts.CodeTokenRevoked, consts.MsgTokenRevoked)
return
}
// Verify user still exists and is active.
// role 一并取出:super_admin 会短路整套鉴权(HasPermission 直接放行、
// assertSuperAdmin 与 superAdminOnlyRules 都只看它),是全站最高权级的判据,
// 因此必须以数据库为准,不能沿用签发时写进 token 的 claims.Role —— 否则给某人
// 临时提为超管再降回来后,其旧 token 在有效期内仍是超管(降权无法即时生效)。
var user *entity.SysAdminUsers
err = dao.SysAdminUsers.Ctx(r.Context()).
Where("id", claims.UserID).
Fields("status, role").
Scan(&user)
if err != nil {
response.ErrorMsg(r, consts.CodeUnauthorized, consts.MsgUnauthorized)
return
}
if user == nil || user.Status != "active" {
response.ErrorMsg(r, consts.CodeUnauthorized, consts.MsgUnauthorized)
return
}
// Set auth context
r.SetCtxVar(CtxKeyUserID, claims.UserID)
r.SetCtxVar(CtxKeyUserType, claims.UserType)
r.SetCtxVar(CtxKeyRole, user.Role)
r.SetCtxVar(CtxKeySessionID, claims.SessionID)
r.SetCtxVar(CtxKeyJti, claims.ID)
r.Middleware.Next()
}
// GetUserID extracts user ID from context.
func GetUserID(ctx context.Context) int64 {
val := ctx.Value(CtxKeyUserID)
if val == nil {
return 0
}
if id, ok := val.(int64); ok {
return id
}
return 0
}
// GetUserRole extracts user role from context.
func GetUserRole(ctx context.Context) string {
val := ctx.Value(CtxKeyRole)
if val == nil {
return ""
}
if role, ok := val.(string); ok {
return role
}
return ""
}
// GetUserType extracts user type from context.
func GetUserType(ctx context.Context) string {
val := ctx.Value(CtxKeyUserType)
if val == nil {
return ""
}
if t, ok := val.(string); ok {
return t
}
return ""
}
// GetSessionID extracts session ID from context.
func GetSessionID(ctx context.Context) int64 {
val := ctx.Value(CtxKeySessionID)
if val == nil {
return 0
}
if id, ok := val.(int64); ok {
return id
}
return 0
}
// GetJti extracts the JWT ID (jti) from context.
func GetJti(ctx context.Context) string {
val := ctx.Value(CtxKeyJti)
if val == nil {
return ""
}
if jti, ok := val.(string); ok {
return jti
}
return ""
}
// GetTenantID extracts tenant ID from context.
func GetTenantID(ctx context.Context) int64 {
val := ctx.Value(CtxKeyTenantID)
if val == nil {
return 0
}
if id, ok := val.(int64); ok {
return id
}
return 0
}
// GetApiKeyID extracts API Key ID from context.
func GetApiKeyID(ctx context.Context) int64 {
val := ctx.Value(CtxKeyApiKeyID)
if val == nil {
return 0
}
if id, ok := val.(int64); ok {
return id
}
return 0
}
// GetProjectID extracts Project ID from context.
func GetProjectID(ctx context.Context) int64 {
val := ctx.Value(CtxKeyProjectID)
if val == nil {
return 0
}
if id, ok := val.(int64); ok {
return id
}
return 0
}
// extractBearerToken extracts the Bearer token from the Authorization header.
// 仅在 WebSocket 握手请求上回退到 "token" query 参数——浏览器无法为 WebSocket 握手
// 自定义 Authorization 头,只能用 query 传 token(如 /v1/realtime)。普通 REST 请求
// 不走此回退,避免 token 泄漏进访问日志/代理日志/浏览器历史/Referer。
func extractBearerToken(r *ghttp.Request) string {
authHeader := r.GetHeader("Authorization")
if authHeader != "" {
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
return strings.TrimSpace(parts[1])
}
}
// 仅 WebSocket 升级握手才允许 query 参数回退(浏览器无法为 WS 握手设置请求头)
if isWebSocketUpgrade(r) {
if token := r.Get("token").String(); token != "" {
return token
}
}
return ""
}
// isWebSocketUpgrade 判断请求是否为 WebSocket 升级握手。
// 浏览器发起 WebSocket 握手时会自动带上 "Upgrade: websocket" 头(无法禁用),
// 据此把 query token 回退严格限定在 WS 场景,普通 HTTP 请求一律不生效。
func isWebSocketUpgrade(r *ghttp.Request) bool {
return strings.EqualFold(r.GetHeader("Upgrade"), "websocket")
}