kiwi-user

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Kiwi-User Authentication Integration

Kiwi-User 认证集成方案

This skill defines how to integrate with the kiwi-user authentication service from both frontend (Next.js/React) and backend (Go/Gin) applications.
本方案定义了如何在前端(Next.js/React)与后端(Go/Gin)应用中集成 Kiwi-user 认证服务。

Architecture Overview

架构概览

Frontend (Next.js)                     Kiwi-User Service                    Backend (Go/Gin)
     |                                       |                                      |
     |---(1) OAuth/login ------------------>|                                      |
     |<--(2) {access_token, refresh_token}--|                                      |
     |                                       |                                      |
     |    [Store tokens in localStorage]     |                                      |
     |                                       |                                      |
     |---(3) API request (Bearer token) -----|-------------------------------------->|
     |                                       |<---(4) POST /v1/token/verify --------|
     |                                       |----(5) {success, user_info} -------->|
     |                                       |                                      |
     |---(6) POST /v1/token/refresh ------->| [When access token near expiry]      |
     |<--(7) New tokens --------------------|                                      |
Kiwi-user is the central auth service. Frontends authenticate users and store tokens. Backends verify tokens on every request via kiwi-user's API (or optionally via public key).
Frontend (Next.js)                     Kiwi-User Service                    Backend (Go/Gin)
     |                                       |                                      |
     |---(1) OAuth/login ------------------>|                                      |
     |<--(2) {access_token, refresh_token}--|                                      |
     |                                       |                                      |
     |    [Store tokens in localStorage]     |                                      |
     |                                       |                                      |
     |---(3) API request (Bearer token) -----|-------------------------------------->|
     |                                       |<---(4) POST /v1/token/verify --------|
     |                                       |----(5) {success, user_info} -------->|
     |                                       |                                      |
     |---(6) POST /v1/token/refresh ------->| [When access token near expiry]      |
     |<--(7) New tokens --------------------|                                      |
Kiwi-user 是核心认证服务。前端负责用户认证与 Token 存储,后端则通过 Kiwi-user 的 API(或可选的公钥方式)在每次请求时验证 Token。

JWT Token Structure

JWT Token 结构

Kiwi-user issues RS256-signed JWTs. The access token payload contains:
FieldJWT ClaimTypeDescription
UserID
sub
stringUser's unique ID
Application
iss
stringApplication name (issuer)
PersonalRole
roles
stringUser's role
Scopes
scopes
[]stringPermission scopes
DeviceType
device_type
stringDevice type
DeviceID
device_id
stringDevice identifier
OrganizationID
organization_id
stringCurrent org ID
Create
iat
int64Issued at (unix seconds)
Expire
exp
int64Expiration (unix seconds)
Default expiration: access token = 600s (10 min), refresh token = 86400s (24 hours).
Kiwi-user 签发基于 RS256 签名的 JWT。Access Token 的载荷包含以下字段:
字段JWT 声明类型描述
UserID
sub
string用户唯一ID
Application
iss
string应用名称(签发方)
PersonalRole
roles
string用户角色
Scopes
scopes
[]string权限范围
DeviceType
device_type
string设备类型
DeviceID
device_id
string设备标识
OrganizationID
organization_id
string当前组织ID
Create
iat
int64签发时间(Unix 时间戳,秒)
Expire
exp
int64过期时间(Unix 时间戳,秒)
默认过期时间:Access Token = 600秒(10分钟),Refresh Token = 86400秒(24小时)。

Environment Variables

环境变量

Frontend (Next.js)

前端(Next.js)

VariablePurposeExample
NEXT_PUBLIC_KIWI_USER_API_BASE_URL
Kiwi-user API base URL
https://user.example.com
NEXT_PUBLIC_APPLICATION_NAME
Application name for login requests
my-app
NEXT_PUBLIC_GOOGLE_CLIENT_ID
Google OAuth client ID
xxx.apps.googleusercontent.com
变量用途示例
NEXT_PUBLIC_KIWI_USER_API_BASE_URL
Kiwi-user API 基础地址
https://user.example.com
NEXT_PUBLIC_APPLICATION_NAME
登录请求使用的应用名称
my-app
NEXT_PUBLIC_GOOGLE_CLIENT_ID
Google OAuth 客户端ID
xxx.apps.googleusercontent.com

Backend (Go)

后端(Go)

Config FieldPurposeExample
cfg.User.BaseURL
Kiwi-user API base URL
https://user.example.com
配置字段用途示例
cfg.User.BaseURL
Kiwi-user API 基础地址
https://user.example.com

Frontend Integration

前端集成

See references/frontend-auth.md for complete code patterns.
完整代码示例请参考 references/frontend-auth.md

Login Flows

登录流程

Kiwi-user supports multiple login methods. All return the same
LoginResponse
:
typescript
interface LoginResponse {
  refresh_token: string;
  refresh_token_expires_at: number;  // unix seconds
  access_token: string;
  access_token_expires_at: number;   // unix seconds
  type: string;
  device_type: string;
  device_id: string;
  user_id: string;
}
MethodEndpointFlow
Google OAuth (Web)
POST /v1/login/google/web
Redirect to Google -> callback with code -> exchange for tokens
Email
POST /v1/login/email
Send verification code ->
POST /v1/login/email/verify_code
Phone
POST /v1/login/phone
Send verification code ->
POST /v1/login/phone/verify_code
WeChat Mini Program
POST /v1/login/wechat/miniprogram
WeChat SDK login
WeChat Web
POST /v1/login/wechat/web
WeChat OAuth flow
Password
POST /v1/login/password
Direct username/password
Kiwi-user 支持多种登录方式,所有方式均返回相同的
LoginResponse
结构:
typescript
interface LoginResponse {
  refresh_token: string;
  refresh_token_expires_at: number;  // unix seconds
  access_token: string;
  access_token_expires_at: number;   // unix seconds
  type: string;
  device_type: string;
  device_id: string;
  user_id: string;
}
方式接口流程
Google OAuth(网页端)
POST /v1/login/google/web
重定向至 Google -> 携带授权码回调 -> 兑换 Token
邮箱登录
POST /v1/login/email
发送验证码 -> 调用
POST /v1/login/email/verify_code
验证
手机号登录
POST /v1/login/phone
发送验证码 -> 调用
POST /v1/login/phone/verify_code
验证
微信小程序登录
POST /v1/login/wechat/miniprogram
通过微信 SDK 登录
微信网页登录
POST /v1/login/wechat/web
微信 OAuth 流程
密码登录
POST /v1/login/password
直接提交用户名/密码

Token Storage

Token 存储

Store all auth data in
localStorage
with these keys:
KeyValue
kiwi_access_token
JWT access token string
kiwi_refresh_token
Refresh token string
kiwi_access_token_expires_at
Expiration (unix seconds, string)
kiwi_refresh_token_expires_at
Expiration (unix seconds, string)
kiwi_user_id
User ID string
kiwi_device_type
Device type string
kiwi_device_id
Device identifier string
将所有认证数据存储在
localStorage
中,使用以下键名:
键名对应值
kiwi_access_token
JWT Access Token 字符串
kiwi_refresh_token
Refresh Token 字符串
kiwi_access_token_expires_at
过期时间(Unix 时间戳,字符串格式)
kiwi_refresh_token_expires_at
过期时间(Unix 时间戳,字符串格式)
kiwi_user_id
用户ID字符串
kiwi_device_type
设备类型字符串
kiwi_device_id
设备标识字符串

Token Refresh Strategy

Token 刷新策略

Access tokens expire quickly (default 10 min). Refresh BEFORE expiration using a buffer:
typescript
function isAccessTokenExpired(): boolean {
  const expiresAt = localStorage.getItem("kiwi_access_token_expires_at");
  if (!expiresAt) return true;
  const bufferSeconds = 60; // 1-minute buffer
  return Date.now() / 1000 >= parseInt(expiresAt) - bufferSeconds;
}
The
getAccessToken()
function auto-refreshes transparently:
typescript
async function getAccessToken(): Promise<string | null> {
  if (!isAccessTokenExpired()) {
    return localStorage.getItem("kiwi_access_token");
  }
  // Refresh the token
  const refreshed = await refreshAccessToken();
  if (!refreshed) {
    clearAuthData(); // Refresh token also expired
    return null;
  }
  return localStorage.getItem("kiwi_access_token");
}
Refresh calls
POST /v1/token/refresh
with
{user_id, refresh_token, device}
. Both tokens are replaced.
Access Token 过期较快(默认10分钟),需在过期前提前刷新,建议设置缓冲时间:
typescript
function isAccessTokenExpired(): boolean {
  const expiresAt = localStorage.getItem("kiwi_access_token_expires_at");
  if (!expiresAt) return true;
  const bufferSeconds = 60; // 1分钟缓冲
  return Date.now() / 1000 >= parseInt(expiresAt) - bufferSeconds;
}
getAccessToken()
函数会自动透明地完成刷新操作:
typescript
async function getAccessToken(): Promise<string | null> {
  if (!isAccessTokenExpired()) {
    return localStorage.getItem("kiwi_access_token");
  }
  // 刷新Token
  const refreshed = await refreshAccessToken();
  if (!refreshed) {
    clearAuthData(); // Refresh Token 也已过期
    return null;
  }
  return localStorage.getItem("kiwi_access_token");
}
刷新操作调用
POST /v1/token/refresh
接口,携带参数
{user_id, refresh_token, device}
,刷新后新旧Token会被替换。

API Client Pattern

API 客户端实现模式

Every authenticated API request MUST use
getAccessToken()
and set the
Authorization
header:
typescript
async function apiRequest<T>(path: string, options: RequestInit = {}): Promise<T> {
  const token = await getAccessToken();
  if (!token) {
    window.location.href = "/"; // Redirect to login
    throw new Error("Not authenticated");
  }
  const response = await fetch(`${API_BASE_URL}${path}`, {
    ...options,
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
      ...options.headers,
    },
  });
  if (response.status === 401) {
    // Token rejected by backend - clear and redirect
    clearAuthData();
    window.location.href = "/";
    throw new Error("Unauthorized");
  }
  const data = await response.json();
  return data.data as T;
}
所有需要认证的API请求必须调用
getAccessToken()
并设置
Authorization
请求头:
typescript
async function apiRequest<T>(path: string, options: RequestInit = {}): Promise<T> {
  const token = await getAccessToken();
  if (!token) {
    window.location.href = "/"; // 重定向至登录页
    throw new Error("Not authenticated");
  }
  const response = await fetch(`${API_BASE_URL}${path}`, {
    ...options,
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
      ...options.headers,
    },
  });
  if (response.status === 401) {
    // Token 被后端拒绝 - 清除认证数据并重定向
    clearAuthData();
    window.location.href = "/";
    throw new Error("Unauthorized");
  }
  const data = await response.json();
  return data.data as T;
}

React Auth Hook

React 认证 Hook

Use a
useAuth
hook and
AuthProvider
context to manage auth state:
typescript
function useAuth() {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [user, setUser] = useState<UserInfo | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    async function checkAuth() {
      const authed = isAuthenticated(); // checks refresh token validity
      setIsAuthenticated(authed);
      if (authed) {
        const userInfo = await getUserInfo();
        setUser(userInfo);
      }
      setIsLoading(false);
    }
    checkAuth();
  }, []);

  return { isAuthenticated, isLoading, user, logout };
}
使用
useAuth
Hook 和
AuthProvider
上下文管理认证状态:
typescript
function useAuth() {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [user, setUser] = useState<UserInfo | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    async function checkAuth() {
      const authed = isAuthenticated(); // 检查Refresh Token有效性
      setIsAuthenticated(authed);
      if (authed) {
        const userInfo = await getUserInfo();
        setUser(userInfo);
      }
      setIsLoading(false);
    }
    checkAuth();
  }, []);

  return { isAuthenticated, isLoading, user, logout };
}

User Info Fetching

用户信息获取

GET /v1/user/info
with Bearer token. Implement request deduplication and caching:
typescript
let userInfoPromise: Promise<UserInfo> | null = null;
let cachedUserInfo: { data: UserInfo; timestamp: number } | null = null;
const CACHE_DURATION = 5000; // 5 seconds

async function getUserInfo(): Promise<UserInfo> {
  // Return cached if fresh
  if (cachedUserInfo && Date.now() - cachedUserInfo.timestamp < CACHE_DURATION) {
    return cachedUserInfo.data;
  }
  // Deduplicate concurrent requests
  if (userInfoPromise) return userInfoPromise;
  userInfoPromise = fetchUserInfo();
  try {
    const info = await userInfoPromise;
    cachedUserInfo = { data: info, timestamp: Date.now() };
    return info;
  } finally {
    userInfoPromise = null;
  }
}
On 401 response: retry once after refreshing access token. If still 401, logout.
调用
GET /v1/user/info
接口并携带Bearer Token,需实现请求去重与缓存机制:
typescript
let userInfoPromise: Promise<UserInfo> | null = null;
let cachedUserInfo: { data: UserInfo; timestamp: number } | null = null;
const CACHE_DURATION = 5000; // 5秒

async function getUserInfo(): Promise<UserInfo> {
  // 如果缓存未过期则返回缓存
  if (cachedUserInfo && Date.now() - cachedUserInfo.timestamp < CACHE_DURATION) {
    return cachedUserInfo.data;
  }
  // 去重并发请求
  if (userInfoPromise) return userInfoPromise;
  userInfoPromise = fetchUserInfo();
  try {
    const info = await userInfoPromise;
    cachedUserInfo = { data: info, timestamp: Date.now() };
    return info;
  } finally {
    userInfoPromise = null;
  }
}
如果返回401响应:先尝试刷新Access Token后重试一次,若仍返回401则执行登出操作。

Backend Integration

后端集成

See references/backend-auth.md for complete code patterns.
完整代码示例请参考 references/backend-auth.md

Method 1: Token Verification via API (Recommended)

方式1:通过API验证Token(推荐)

Call kiwi-user's
POST /v1/token/verify
endpoint:
go
type Client struct {
    baseURL    string
    httpClient *http.Client
}

func (c *Client) VerifyToken(ctx context.Context, token string) (string, error) {
    reqBody := map[string]string{"access_token": token}
    body, _ := json.Marshal(reqBody)

    req, _ := http.NewRequestWithContext(ctx, "POST",
        c.baseURL+"/v1/token/verify", bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")

    resp, err := c.httpClient.Do(req)
    // ... error handling, parse response ...

    // Response: {status: "success", data: {success: true, user_info: {id, name, ...}}}
    return response.Data.UserInfo.ID, nil
}
调用Kiwi-user的
POST /v1/token/verify
接口:
go
type Client struct {
    baseURL    string
    httpClient *http.Client
}

func (c *Client) VerifyToken(ctx context.Context, token string) (string, error) {
    reqBody := map[string]string{"access_token": token}
    body, _ := json.Marshal(reqBody)

    req, _ := http.NewRequestWithContext(ctx, "POST",
        c.baseURL+"/v1/token/verify", bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")

    resp, err := c.httpClient.Do(req)
    // ... 错误处理、响应解析 ...

    // 响应格式: {status: "success", data: {success: true, user_info: {id, name, ...}}}
    return response.Data.UserInfo.ID, nil
}

Method 2: Public Key Verification (Self-Contained)

方式2:公钥验证(独立验证)

Fetch the public key from
GET /v1/token/publickey
and verify the JWT locally:
go
// 1. Fetch public key (cache it - it rarely changes)
// GET /v1/token/publickey -> {public_key: "-----BEGIN PUBLIC KEY-----\n..."}

// 2. Parse and verify the JWT
// Split token into head.payload.signature
// Verify signature with RSA public key + SHA256
// Decode payload: base64url -> JSON -> AccessPayload
// Check exp > now

// 3. Extract user info from payload
// payload.sub = user ID
// payload.iss = application
// payload.roles = personal role
// payload.organization_id = org ID
This avoids a network call per request but requires managing key rotation.
GET /v1/token/publickey
接口获取公钥,然后本地验证JWT:
go
// 1. 获取公钥(缓存该公钥 - 极少变更)
// GET /v1/token/publickey -> {public_key: "-----BEGIN PUBLIC KEY-----\n..."}

// 2. 解析并验证JWT
// 将Token拆分为head.payload.signature三部分
// 使用RSA公钥 + SHA256验证签名
// 解析载荷: base64url解码 -> JSON解析 -> AccessPayload
// 检查exp字段是否大于当前时间

// 3. 从载荷中提取用户信息
// payload.sub = 用户ID
// payload.iss = 应用名称
// payload.roles = 用户角色
// payload.organization_id = 组织ID
该方式避免了每次请求的网络调用,但需要自行管理公钥轮换。

Auth Middleware Pattern (Gin)

Gin 认证中间件实现模式

go
const UserIDKey = "user_id"

func AuthMiddleware(kiwiUserClient *kiwiuser.Client) gin.HandlerFunc {
    return func(c *gin.Context) {
        auth := c.GetHeader("Authorization")
        parts := strings.SplitN(auth, " ", 2)
        if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
            c.AbortWithStatusJSON(401, BaseResponse{Status: "error", Error: "missing token"})
            return
        }

        userID, err := kiwiUserClient.VerifyToken(c.Request.Context(), parts[1])
        if err != nil {
            c.AbortWithStatusJSON(401, BaseResponse{Status: "error", Error: err})
            return
        }

        c.Set(UserIDKey, userID)
        c.Next()
    }
}
go
const UserIDKey = "user_id"

func AuthMiddleware(kiwiUserClient *kiwiuser.Client) gin.HandlerFunc {
    return func(c *gin.Context) {
        auth := c.GetHeader("Authorization")
        parts := strings.SplitN(auth, " ", 2)
        if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
            c.AbortWithStatusJSON(401, BaseResponse{Status: "error", Error: "missing token"})
            return
        }

        userID, err := kiwiUserClient.VerifyToken(c.Request.Context(), parts[1])
        if err != nil {
            c.AbortWithStatusJSON(401, BaseResponse{Status: "error", Error: err})
            return
        }

        c.Set(UserIDKey, userID)
        c.Next()
    }
}

Extracting User ID in Handlers

处理器中提取用户ID

Use the
RequireUserHandler
pattern to get a type-safe user ID:
go
// Handler wrapper that extracts user ID
func RequireUserHandler[T any](f func(*gin.Context, string) (T, *facade.Error)) gin.HandlerFunc {
    return func(c *gin.Context) {
        userID, exists := c.Get(middleware.UserIDKey)
        if !exists {
            c.AbortWithStatusJSON(403, BaseResponse{Status: "error", Error: "user ID not found"})
            return
        }
        userIDStr := userID.(string)
        data, err := f(c, userIDStr)
        if err != nil {
            responseError(c, err)
            return
        }
        c.JSON(200, BaseResponse{Status: "success", Data: data})
    }
}

// Usage in route registration
router.GET("/v1/profile", RequireUserHandler(handler.GetProfile))

// Handler receives userID directly
func (h *Handler) GetProfile(c *gin.Context, userID string) (*Profile, *facade.Error) {
    return h.service.GetProfile(c, userID)
}
使用
RequireUserHandler
模式获取类型安全的用户ID:
go
// 处理器包装函数,用于提取用户ID
func RequireUserHandler[T any](f func(*gin.Context, string) (T, *facade.Error)) gin.HandlerFunc {
    return func(c *gin.Context) {
        userID, exists := c.Get(middleware.UserIDKey)
        if !exists {
            c.AbortWithStatusJSON(403, BaseResponse{Status: "error", Error: "user ID not found"})
            return
        }
        userIDStr := userID.(string)
        data, err := f(c, userIDStr)
        if err != nil {
            responseError(c, err)
            return
        }
        c.JSON(200, BaseResponse{Status: "success", Data: data})
    }
}

// 路由注册时的用法
router.GET("/v1/profile", RequireUserHandler(handler.GetProfile))

// 处理器直接接收userID参数
func (h *Handler) GetProfile(c *gin.Context, userID string) (*Profile, *facade.Error) {
    return h.service.GetProfile(c, userID)
}

Route Registration Pattern

路由注册模式

Apply auth middleware to route groups:
go
func RegisterRoutes(router *gin.Engine, authMW gin.HandlerFunc, handler *Handler) {
    v1 := router.Group("/v1")

    // Public routes (no auth)
    public := v1.Group("/public")
    public.GET("/health", handler.Health)

    // Authenticated routes
    authed := v1.Group("")
    authed.Use(authMW)
    authed.GET("/profile", RequireUserHandler(handler.GetProfile))
    authed.POST("/settings", RequireUserHandler(handler.UpdateSettings))
}
将认证中间件应用到路由组:
go
func RegisterRoutes(router *gin.Engine, authMW gin.HandlerFunc, handler *Handler) {
    v1 := router.Group("/v1")

    // 公开路由(无需认证)
    public := v1.Group("/public")
    public.GET("/health", handler.Health)

    // 需认证路由
    authed := v1.Group("")
    authed.Use(authMW)
    authed.GET("/profile", RequireUserHandler(handler.GetProfile))
    authed.POST("/settings", RequireUserHandler(handler.UpdateSettings))
}

Local Development

本地开发环境配置

In local environment, skip token verification and use a test user:
go
if cfg.Server.Env == "local" {
    c.Set(UserIDKey, "test_user")
    c.Next()
    return
}
在本地开发环境中,可以跳过Token验证,使用测试用户ID:
go
if cfg.Server.Env == "local" {
    c.Set(UserIDKey, "test_user")
    c.Next()
    return
}

Logout Flow

登出流程

Frontend

前端

typescript
async function logout(): Promise<void> {
  const userId = localStorage.getItem("kiwi_user_id");
  const refreshToken = localStorage.getItem("kiwi_refresh_token");
  // Notify kiwi-user to invalidate tokens
  await fetch(`${KIWI_USER_API_BASE_URL}/v1/user/logout`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      user_id: userId,
      refresh_token: refreshToken,
      device: { device_type: getDeviceType(), device_id: getDeviceId() },
    }),
  });
  // Clear all local auth data
  clearAuthData();
}
typescript
async function logout(): Promise<void> {
  const userId = localStorage.getItem("kiwi_user_id");
  const refreshToken = localStorage.getItem("kiwi_refresh_token");
  // 通知Kiwi-user失效Token
  await fetch(`${KIWI_USER_API_BASE_URL}/v1/user/logout`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      user_id: userId,
      refresh_token: refreshToken,
      device: { device_type: getDeviceType(), device_id: getDeviceId() },
    }),
  });
  // 清除本地所有认证数据
  clearAuthData();
}

API Reference

API 参考文档

See references/kiwi-user-api.md for complete endpoint documentation with request/response types.
完整接口文档(含请求/响应类型定义)请参考 references/kiwi-user-api.md

Checklist

检查清单

Before shipping auth integration, verify:
  • NEXT_PUBLIC_KIWI_USER_API_BASE_URL
    and
    NEXT_PUBLIC_APPLICATION_NAME
    are configured
  • Tokens stored in localStorage with correct keys (
    kiwi_access_token
    , etc.)
  • Access token refresh uses 1-minute buffer before actual expiration
  • getAccessToken()
    called before every API request (auto-refreshes)
  • API client sets
    Authorization: Bearer <token>
    header
  • 401 responses trigger token refresh retry, then logout on second failure
  • getUserInfo()
    has request deduplication and short-lived cache
  • Backend auth middleware extracts Bearer token from Authorization header
  • Backend calls
    POST /v1/token/verify
    (or uses public key verification)
  • User ID set in gin context with
    c.Set("user_id", userID)
  • Handlers use
    RequireUserHandler
    pattern for type-safe user ID access
  • Local dev mode skips verification with
    test_user
  • Logout calls
    /v1/user/logout
    AND clears all localStorage keys
在发布认证集成功能前,请验证以下内容:
  • 已配置
    NEXT_PUBLIC_KIWI_USER_API_BASE_URL
    NEXT_PUBLIC_APPLICATION_NAME
    环境变量
  • Token 已使用正确的键名存储在 localStorage 中(如
    kiwi_access_token
    等)
  • Access Token 刷新逻辑在过期前1分钟缓冲时间触发
  • 所有API请求前均调用
    getAccessToken()
    (自动完成刷新)
  • API客户端已设置
    Authorization: Bearer <token>
    请求头
  • 401响应会触发Token刷新重试,若再次失败则执行登出
  • getUserInfo()
    已实现请求去重与短时间缓存
  • 后端认证中间件已从Authorization头中提取Bearer Token
  • 后端已调用
    POST /v1/token/verify
    (或使用公钥验证方式)
  • 用户ID已通过
    c.Set("user_id", userID)
    存入Gin上下文
  • 处理器使用
    RequireUserHandler
    模式获取类型安全的用户ID
  • 本地开发模式已跳过验证,使用
    test_user
    作为用户ID
  • 登出操作已调用
    /v1/user/logout
    接口并清除所有localStorage键值对