React SPA + Go 后端 SDK 配套接入
本文档给出当前最推荐的完整接法:
- 前端:
@codebird/react - 后端:
codebird-go-sdk
这是第三方系统最常见的组合,也是目前最稳定、最容易排查问题的架构。
目标
按本文接入后,完整链路应该是:
- React SPA 发起登录
- callback 页面完成授权码交换
- React SDK 负责本地登录态恢复与 Token 自动续期
- React 前端向 Go 后端请求业务 API
- Go 后端验证 Access Token,并基于
AuthContext做鉴权 - 需要最新数据库状态时,前后端调用实时会话上下文
推荐架构
text
React SPA
-> CodeBirdProvider
-> CodeBirdAuthGuard
-> auth.signIn()
-> callback
-> auth.getAccessToken()
-> Authorization: Bearer <token>
-> Go API
-> ParseBearerToken()
-> VerifyAccessToken()
-> AuthContext第 1 步:前端初始化 React SDK
推荐从业务后端运行时接口读取认证配置。
tsx
import { useEffect, useState } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { CodeBirdProvider } from '@codebird/react';
import App from './App';
type RuntimeAuthConfig = {
issuer: string;
appId: string;
redirectUri: string;
postLogoutRedirectUri: string;
resource: string;
organizationId?: string;
configVersion: string;
scopes: string[];
};
export default function AuthBootstrap() {
const [config, setConfig] = useState<RuntimeAuthConfig | null>(null);
useEffect(() => {
void fetch('/api/v1/admin/app/init')
.then((response) => response.json())
.then((payload) => setConfig(payload.result));
}, []);
if (!config) {
return <div>Loading...</div>;
}
return (
<CodeBirdProvider
endpoint={config.issuer}
appId={config.appId}
redirectUri={config.redirectUri}
postLogoutRedirectUri={config.postLogoutRedirectUri}
defaultResource={config.resource}
defaultOrganizationId={config.organizationId}
configVersion={config.configVersion}
scopes={config.scopes}
automaticSilentRenew={false}
>
<BrowserRouter>
<App />
</BrowserRouter>
</CodeBirdProvider>
);
}第 2 步:前端配置 callback 和官方守卫
tsx
import { Routes, Route } from 'react-router-dom';
import { CodeBirdAuthGuard, CodeBirdCallback, useCodeBirdAuth } from '@codebird/react';
function ProtectedApp() {
const auth = useCodeBirdAuth();
return (
<CodeBirdAuthGuard
loadingFallback={<div>Loading...</div>}
unauthenticatedFallback={<div>Redirecting...</div>}
onUnauthenticated={() => auth.signIn()}
>
<Routes>
<Route path="/" element={<DashboardPage />} />
</Routes>
</CodeBirdAuthGuard>
);
}
export default function App() {
return (
<Routes>
<Route path="/callback" element={<CodeBirdCallback />} />
<Route path="/*" element={<ProtectedApp />} />
</Routes>
);
}这个模式的好处是:
- callback、恢复登录态、未认证跳登录统一收口在 SDK
- refresh token 失效后,Guard 会稳定进入未认证逻辑
- 第三方不用自己维护复杂的认证守卫时序
第 3 步:前端请求业务 API
tsx
import { useCodeBirdAuth } from '@codebird/react';
export default function DashboardPage() {
const auth = useCodeBirdAuth();
async function loadProfile() {
const token = await auth.getAccessToken();
const response = await fetch('/api/me', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const result = await response.json();
console.log(result);
}
return <button onClick={() => void loadProfile()}>获取当前用户</button>;
}当前推荐原因
因为 React SDK 已经统一处理:
- token 是否存在
- token 的
aud是否匹配defaultResource - token 是否需要续期
- refresh token 失效后的本地状态清理
业务代码不要再自己做一层。
第 4 步:Go 后端验证 Token
go
package main
import (
"encoding/json"
"log"
"net/http"
codebird "github.com/lshaofan/codebird-go-sdk"
)
func main() {
verifier, err := codebird.NewVerifier(codebird.Config{
Issuer: "https://auth.codebird.cloud",
Audience: "https://api.example.com",
})
if err != nil {
log.Fatalf("failed to create verifier: %v", err)
}
http.HandleFunc("/api/me", func(w http.ResponseWriter, r *http.Request) {
token, err := codebird.ParseBearerToken(r.Header.Get("Authorization"))
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
authCtx, err := verifier.VerifyAccessToken(r.Context(), token)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{
"user_id": authCtx.Subject,
"username": authCtx.Username,
"organization_id": authCtx.OrganizationID,
"organization_roles": authCtx.OrganizationRoles,
"organization_is_admin": authCtx.OrganizationIsAdmin,
})
})
log.Fatal(http.ListenAndServe(":8081", nil))
}第 5 步:前后端必须对齐的配置
| 概念 | 前端 | 后端 |
|---|---|---|
| 认证中心地址 | endpoint | Issuer |
| 业务 API 资源标识 | defaultResource | Audience |
| 组织上下文 | defaultOrganizationId 或 signIn({ organizationId }) | 从 token / session context 读取 |
| 实时租户标识 | tenant.slug | 同样读取 tenant.slug |
最关键的一条:
text
React defaultResource == Go Audience如果不一致,典型表现是:
- 前端拿到的 token 无法调用后端
- 后端验签通过但 audience 校验失败
- 前端出现重复 refresh 或错误的 token 使用
第 6 步:实时上下文的分工
只需要轻量 claims 时
前端或后端可以直接用 Access Token / AuthContext:
- 当前用户是谁
- 当前组织是谁
- 当前组织角色有哪些
需要数据库最新状态时
请使用实时上下文:
- 前端:
auth.getSessionContext() - 后端:调用
GET /api/session/context
适用场景:
- 组织成员关系可能实时变化
- 页面需要最新组织列表
- 需要最新
organization.is_admin - 需要
tenant.slug拼接租户化入口
第 7 步:refresh token 过期时应该怎样工作
当前推荐行为是:
- React SDK 发现 Access Token 需要续期
- 如果 Refresh Token 仍有效,自动刷新
- 如果 Refresh Token 已过期,SDK 清理本地登录态
CodeBirdAuthGuard进入未认证分支- 第三方自动重新调用
auth.signIn()
正确的用户体验应该是:
- 用户重新进入登录流程
- 而不是停留在一条技术错误信息上
如果你们的页面直接显示错误消息,说明业务项目没有正确处理未认证态。
第 8 步:推荐的业务判断方式
Go 后端拿到 AuthContext 后,推荐直接用 helper:
go
if !authCtx.HasOrganization("org_123") {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if !authCtx.HasOrganizationRole("org_123", "admin") {
http.Error(w, "forbidden", http.StatusForbidden)
return
}不要在业务代码里自己手拆原始 claims。
当前最常见错误
错误 1:前端自己控制 token 刷新
不推荐。
请让 React SDK 统一处理。
错误 2:前端没用官方 Guard
表现通常是:
- 刷新 token 过期后页面停在错误消息
- 本地状态清理后没有重新发起登录
错误 3:后端只看旧 claims,不看实时上下文
当组织关系或管理员状态发生变化时,容易出现权限判断滞后。
联调清单
上线前至少做这几组测试:
场景 A:Access Token 过期,Refresh Token 仍有效
预期:
- 前端无感续期
- 业务页面不跳登录
场景 B:Refresh Token 过期
预期:
- SDK 清理本地登录态
- Guard 自动重新发起登录
场景 C:组织管理员状态变化
预期:
- claims 可能还是旧值
- 实时会话上下文能反映最新数据库状态
