* fix:修复响应污染问题 * fix:修改存储相关文件为中文注释 * fix:修复二次退款问题 * fix:修复多个考虑不全面问题 1.误删用户数据 2.R2存储默认区域 3.删除失败导致孤儿记录 4.网络抖动事变时重试 5.前端无限次请求获取图片 6.文件管理权限 * fix:修复字符串截断异常问题和文件下载超时问题 * fix:修复多图问题 * fix:异步生成图片时未配置oss提示错误信息 * fix:修复关闭异步后同步生成出错问题 * feat:新增文件存储测试功能,防止设置后不生效 * fix:修复同步图片转异步后计费错误 * fix:修复无下限扣款问题 * fix:修复结算重复扣款问题 * fix:修复计费出错导致的0费用结算 * fix:修复回调双重履约 * fix:修复预扣事务 * fix:修复费用字段类型问题 * fix:修复缓存问题 * fix:修复安全问题 * fix:修复数据库操作事务问题 * refactor:重构事务使用方式,改为官方推荐的dao形式 * fix:修复余额更新隐患 * fix:修复N+1查询问题 * fix:修复密钥安全问题 * fix:结算失败时打印错误日志 * improve:优化扣款 * fix:修复用户接口越权 --------- Co-authored-by: chengqian <wuchengqian@kkidc.com>
230 lines
6.0 KiB
Go
230 lines
6.0 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
|
|
var user *entity.SysAdminUsers
|
|
err = dao.SysAdminUsers.Ctx(r.Context()).
|
|
Where("id", claims.UserID).
|
|
Fields("status").
|
|
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, claims.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")
|
|
}
|