feat: rbac迁移完成,并已部署至dev服务器
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jwtUtil "sundynix-micro-go/common/utils/jwt"
|
||||
|
||||
jwtv5 "github.com/golang-jwt/jwt/v5"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
// RefreshTokenHeader 续期后新 Token 放在此响应头里,前端读取后静默替换
|
||||
const RefreshTokenHeader = "X-Refresh-Token"
|
||||
|
||||
// AuthMiddleware 网关鉴权 + 自动续期中间件
|
||||
type AuthMiddleware struct {
|
||||
jwtSecret string
|
||||
whitelist map[string]bool
|
||||
}
|
||||
|
||||
func NewAuthMiddleware(jwtSecret string, whitelist []string) *AuthMiddleware {
|
||||
wl := make(map[string]bool, len(whitelist))
|
||||
for _, p := range whitelist {
|
||||
wl[p] = true
|
||||
}
|
||||
return &AuthMiddleware{
|
||||
jwtSecret: jwtSecret,
|
||||
whitelist: wl,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 适配 http.Handler 链(自定义 gateway 使用 http.Handler 链式调用)
|
||||
func (m *AuthMiddleware) Handle(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// OPTIONS 预检直接放行
|
||||
if r.Method == http.MethodOptions {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 白名单路径放行(支持精确匹配和 /* 前缀通配)
|
||||
if m.isWhitelisted(r.URL.Path) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 Authorization 头
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
writeUnauthorized(w, "缺少 Authorization 请求头")
|
||||
return
|
||||
}
|
||||
tokenStr := jwtUtil.GetTokenFromHeader(authHeader)
|
||||
if tokenStr == "" {
|
||||
writeUnauthorized(w, "Token 格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
j := jwtUtil.NewJWT(m.jwtSecret)
|
||||
claims, err := j.ParseToken(tokenStr)
|
||||
if err != nil {
|
||||
logx.Infof("[gateway] JWT 解析失败: %v, path: %s", err, r.URL.Path)
|
||||
writeUnauthorized(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息透传到上游,避免上游重复解析 JWT
|
||||
r.Header.Set("X-User-Id", claims.BaseClaims.ID)
|
||||
r.Header.Set("X-User-Account", claims.BaseClaims.Account)
|
||||
|
||||
// ---- 滑动窗口续期 ----
|
||||
if newToken, ok := m.tryRefresh(j, claims); ok {
|
||||
w.Header().Set(RefreshTokenHeader, newToken)
|
||||
logx.Infof("[gateway] Token 已续期, userId: %s", claims.BaseClaims.ID)
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// tryRefresh 判断是否需要续期,需要则签发新 Token 并返回
|
||||
// 续期规则:剩余有效时间 < BufferTime → 以原始有效时长(ExpiresAt - NotBefore)重新签发
|
||||
func (m *AuthMiddleware) tryRefresh(j *jwtUtil.JWT, claims *jwtUtil.CustomClaims) (string, bool) {
|
||||
bufferTime := time.Duration(claims.BufferTime) * time.Second
|
||||
expiresAt := claims.RegisteredClaims.ExpiresAt.Time
|
||||
remaining := time.Until(expiresAt)
|
||||
|
||||
// 未进入缓冲窗口,无需续期
|
||||
if remaining >= bufferTime {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// 计算原始有效时长:ExpiresAt - NotBefore ≈ 当初登录时配置的 activeTimeout
|
||||
notBefore := claims.RegisteredClaims.NotBefore.Time
|
||||
originalDuration := expiresAt.Sub(notBefore)
|
||||
|
||||
newClaims := jwtUtil.CustomClaims{
|
||||
BaseClaims: claims.BaseClaims,
|
||||
BufferTime: claims.BufferTime,
|
||||
RegisteredClaims: jwtv5.RegisteredClaims{
|
||||
Audience: claims.RegisteredClaims.Audience,
|
||||
Issuer: claims.RegisteredClaims.Issuer,
|
||||
NotBefore: jwtv5.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwtv5.NewNumericDate(time.Now().Add(originalDuration)),
|
||||
},
|
||||
}
|
||||
|
||||
newToken, err := j.CreateToken(newClaims)
|
||||
if err != nil {
|
||||
logx.Errorf("[gateway] Token 续期失败: %v", err)
|
||||
return "", false
|
||||
}
|
||||
return newToken, true
|
||||
}
|
||||
|
||||
// isWhitelisted 支持精确匹配和 /* 前缀通配
|
||||
func (m *AuthMiddleware) isWhitelisted(path string) bool {
|
||||
if m.whitelist[path] {
|
||||
return true
|
||||
}
|
||||
for p := range m.whitelist {
|
||||
if strings.HasSuffix(p, "/*") && strings.HasPrefix(path, strings.TrimSuffix(p, "*")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// writeUnauthorized 返回统一的 401 响应
|
||||
func writeUnauthorized(w http.ResponseWriter, msg string) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"code": 401,
|
||||
"msg": msg,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user