package pub import ( "context" "errors" "fmt" "strings" "time" "perms-system-server/internal/consts" authHelper "perms-system-server/internal/logic/auth" userModel "perms-system-server/internal/model/user" "perms-system-server/internal/response" "perms-system-server/internal/svc" "perms-system-server/internal/types" "github.com/zeromicro/go-zero/core/limit" "github.com/zeromicro/go-zero/core/logx" ) type RefreshTokenLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewRefreshTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RefreshTokenLogic { return &RefreshTokenLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } // RefreshToken 刷新令牌。使用有效的 refreshToken 换取新的 accessToken/refreshToken 令牌对,旧令牌即时失效(单会话轮转)。 // 路由层已挂载 RefreshTokenRateLimit 做 IP 维度限流;本处再叠加 per-user 限流,形成"IP + 用户"双层防护。 func (l *RefreshTokenLogic) RefreshToken(req *types.RefreshTokenReq) (resp *types.LoginResp, err error) { tokenStr := strings.TrimPrefix(req.Authorization, "Bearer ") if tokenStr == "" || tokenStr == req.Authorization { return nil, response.ErrUnauthorized("refreshToken格式错误") } claims, err := authHelper.ParseRefreshToken(tokenStr, l.svcCtx.Config.Auth.RefreshSecret) if err != nil { return nil, response.ErrUnauthorized("refreshToken无效或已过期") } productCode := claims.ProductCode if req.ProductCode != "" && req.ProductCode != productCode { return nil, response.ErrBadRequest("刷新令牌不允许切换产品") } ud, err := l.svcCtx.UserDetailsLoader.Load(l.ctx, claims.UserId, productCode) if err != nil { return nil, response.NewCodeError(503, "服务暂时不可用,请稍后重试") } if ud.Username == "" { return nil, response.ErrUnauthorized("用户不存在或已被删除") } if ud.Status != consts.StatusEnabled { return nil, response.ErrForbidden("账号已被冻结") } if productCode != "" && ud.ProductStatus != consts.StatusEnabled { return nil, response.ErrForbidden("该产品已被禁用") } if productCode != "" && !ud.IsSuperAdmin && ud.MemberType == "" { return nil, response.ErrForbidden("您已不是该产品的成员") } if claims.TokenVersion != ud.TokenVersion { return nil, response.ErrUnauthorized("登录状态已失效,请重新登录") } if l.svcCtx.TokenOpLimiter != nil { code, _ := l.svcCtx.TokenOpLimiter.Take(fmt.Sprintf("refresh:%d", claims.UserId)) if code == limit.OverQuota { return nil, response.ErrTooManyRequests("刷新操作过于频繁,请稍后再试") } } // 审计 M-3:把签名放在 CAS 之前,让"签名失败"不再污染 tokenVersion。原顺序是 // CAS → Clean → 签 access → 签 refresh // 一旦签名失败 tokenVersion 已+1,但客户端没收到新 refreshToken,下一次带旧 version 来 // 会被 "登录状态已失效" 踢掉,变成"签名 bug → 用户被强制登出"的放大效应。新顺序: // 试签 access → 试签 refresh → CAS → Clean // 签名走不通直接 500,DB/缓存都不动;CAS 赢家才推进 tokenVersion 并 Clean 缓存。 predictedVersion := claims.TokenVersion + 1 accessToken, err := authHelper.GenerateAccessToken( l.svcCtx.Config.Auth.AccessSecret, l.svcCtx.Config.Auth.AccessExpire, ud.UserId, ud.Username, ud.ProductCode, ud.MemberType, predictedVersion, ) if err != nil { return nil, err } newRefreshToken, err := authHelper.GenerateRefreshTokenWithExpiry( l.svcCtx.Config.Auth.RefreshSecret, claims.ExpiresAt.Time, ud.UserId, ud.ProductCode, predictedVersion, ) if err != nil { return nil, err } newVersion, err := l.svcCtx.SysUserModel.IncrementTokenVersionIfMatch(l.ctx, claims.UserId, ud.Username, claims.TokenVersion) if err != nil { if errors.Is(err, userModel.ErrTokenVersionMismatch) { return nil, response.ErrUnauthorized("登录状态已失效,请重新登录") } return nil, err } if newVersion != predictedVersion { logx.WithContext(l.ctx).Errorw("refresh token version prediction mismatch", logx.Field("audit", "refresh_token_version_mismatch"), logx.Field("userId", claims.UserId), logx.Field("claimed", claims.TokenVersion), logx.Field("predicted", predictedVersion), logx.Field("actual", newVersion), ) return nil, response.ErrUnauthorized("登录状态已失效,请重新登录") } l.svcCtx.UserDetailsLoader.Clean(l.ctx, claims.UserId) return &types.LoginResp{ AccessToken: accessToken, RefreshToken: newRefreshToken, Expires: time.Now().Unix() + l.svcCtx.Config.Auth.AccessExpire, UserInfo: types.UserInfo{ UserId: ud.UserId, Username: ud.Username, Nickname: ud.Nickname, Avatar: ud.Avatar, Email: ud.Email, Phone: ud.Phone, IsSuperAdmin: ud.IsSuperAdminRaw, MustChangePassword: ud.MustChangePwdRaw, MemberType: ud.MemberType, Perms: ud.Perms, }, }, nil }