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("刷新操作过于频繁,请稍后再试") } } // 审计 L-R11-5:把"试签 → CAS → Clean → forensic 比对"收敛进 authHelper.RotateRefreshToken; // HTTP / gRPC 两条路径共享同一段 tokenVersion 语义,未来任何 CAS/签名链契约变动只改一处。 tokens, err := authHelper.RotateRefreshToken(l.ctx, l.svcCtx, claims, ud) if err != nil { if errors.Is(err, userModel.ErrTokenVersionMismatch) { return nil, response.ErrUnauthorized("登录状态已失效,请重新登录") } return nil, err } return &types.LoginResp{ AccessToken: tokens.AccessToken, RefreshToken: tokens.RefreshToken, 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 }