package middleware import ( "errors" "net/http" "strconv" "strings" "time" "kra/internal/conf" "kra/internal/server/httpx" "kra/internal/service" "kra/pkg/adminauth" "github.com/gin-gonic/gin" "golang.org/x/sync/singleflight" ) const claimsKey = "admin_claims" var refreshTokens singleflight.Group type refreshedToken struct { token string expiresAt int64 } func Auth(runtime *conf.Runtime, security *service.SecurityService, tokens *service.TokenService) gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("x-token") if token == "" { token, _ = c.Cookie("x-token") } if token == "" { httpx.NoAuth(c, "未登录或非法访问,请登录") return } secret := "" config := runtime.Admin() if config != nil && config.Jwt != nil { secret = config.Jwt.SigningKey } claims, err := adminauth.Parse(token, secret) if err != nil { message := "无法处理此token" switch { case errors.Is(err, adminauth.ErrTokenExpired): message = "登录已过期,请重新登录" case errors.Is(err, adminauth.ErrTokenMalformed): message = "这不是一个token" case errors.Is(err, adminauth.ErrTokenSignatureInvalid): message = "无效签名" case errors.Is(err, adminauth.ErrTokenNotValidYet): message = "token尚未激活" } httpx.SetTokenCookie(c, "", -1) httpx.NoAuth(c, message) return } if disabled, checkErr := tokens.IsTokenDisabled(c.Request.Context(), token); checkErr != nil || disabled { httpx.SetTokenCookie(c, "", -1) httpx.NoAuth(c, "您的帐户异地登陆或令牌失效") return } if claims.ExpiresAt != nil && claims.BufferTime > 0 && time.Until(claims.ExpiresAt.Time) < time.Duration(claims.BufferTime)*time.Second { expires, buffer := 7*24*time.Hour, time.Duration(claims.BufferTime)*time.Second issuer := claims.Issuer config = runtime.Admin() if config != nil && config.Jwt != nil { if config.Jwt.ExpiresTime != nil { expires = config.Jwt.ExpiresTime.AsDuration() } if config.Jwt.BufferTime != nil { buffer = config.Jwt.BufferTime.AsDuration() } if config.Jwt.Issuer != "" { issuer = config.Jwt.Issuer } } value, refreshErr, _ := refreshTokens.Do(token, func() (any, error) { newToken, newClaims, generateErr := adminauth.Generate(secret, issuer, expires, buffer, claims.ID, claims.AuthorityID, claims.UUID, claims.Username, claims.NickName, claims.MustChangePwd) if generateErr != nil { return nil, generateErr } if rotateErr := security.RotateActiveToken(c.Request.Context(), claims.Username, token, newToken, expires); rotateErr != nil { return nil, rotateErr } return refreshedToken{token: newToken, expiresAt: newClaims.ExpiresAt.Unix()}, nil }) if refreshErr == nil { refreshed := value.(refreshedToken) c.Header("new-token", refreshed.token) c.Header("new-expires-at", strconv.FormatInt(refreshed.expiresAt, 10)) httpx.SetTokenCookie(c, refreshed.token, int(expires.Seconds())) } } c.Set(claimsKey, claims) c.Next() } } func Claims(c *gin.Context) *adminauth.Claims { value, _ := c.Get(claimsKey) claims, _ := value.(*adminauth.Claims) return claims } func MustChangePassword() gin.HandlerFunc { return func(c *gin.Context) { claims := Claims(c) if claims == nil || !claims.MustChangePwd { c.Next() return } path := strings.TrimSuffix(c.Request.URL.Path, "/") if strings.HasSuffix(path, "/user/changePassword") || strings.HasSuffix(path, "/user/getUserInfo") || strings.HasSuffix(path, "/jwt/jsonInBlacklist") { c.Next() return } c.AbortWithStatusJSON(http.StatusConflict, httpx.Response{Code: httpx.CodePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "密码已过期,请先修改密码"}) } }