在 Web React 项目中接入 Cloudbase 用户认证
一句话定义:用
@cloudbase/js-sdk在 Vite + React 18 项目里接 Cloudbase 认证,不依赖任何 UI 包,自己写组件;覆盖三种登录方式 — 手机短信验证码 / 邮箱验证码 / 微信开放平台扫码,带登录态持久化和react-router-dom路由守卫。预计耗时:50 分钟 | 难度:进阶
适用场景
- 适用:Web 端 React 项目(Next.js / Vite / CRA 都可),想接 Cloudbase 用户体系
- 适用:已经有 add-auth-wechat-miniprogram 的小程序端,Web 端想做端对端共用一套用户表(同一个 Cloudbase 环境的 auth 即可)
- 不适用:用现成的
@cloudbase/login-ui-react等 UI 组件包(本篇不引这类未稳的 UI 包,纯 SDK 自己写组件,可控性更高) - 不适用:服务端渲染场景下要在 Node.js 端拿登录态(那要走
@cloudbase/node-sdk的 server token,不是@cloudbase/js-sdk的范畴)
环境要求
| 依赖 | 版本 |
|---|---|
| Node.js | ≥ 18 |
@cloudbase/js-sdk | 2.27.3 |
react / react-dom | ^18.2.0 |
react-router-dom | ^6.x |
| 构建工具 | Vite ^5.x / Next.js / CRA 任选 |
另外需要:
- 已开通的 Cloudbase 环境 ID,地域选「上海」(短信验证码登录仅支持上海地域)
- 控制台 → 身份认证 → 登录方式,把要用的几种方式开启:
- 「短信验证码登录」
- 「邮箱验证码登录」(顺手开「邮件代发」零配置最快)
- 「微信开放平台登录」,填入 微信开放平台 申请的网站应用 AppID 和 AppSecret
控制台只能在上海地域开启短信登录。Cloudbase 在 init 时不传 region 默认就是上海;广州 / 北京环境用不了短信。
第一步:初始化 SDK
npm i @cloudbase/js-sdk react-router-dom
src/cloudbase.ts:
import cloudbase from '@cloudbase/js-sdk';
export const app = cloudbase.init({
env: 'your-env-id',
// 不传 region 默认上海;短信登录只在上海支持
});
export const auth = app.auth({
persistence: 'local', // Web 端登录态保留 30 天,显式 signOut 才会清
});
@cloudbase/js-sdk@2.x Web 端只支持 persistence: 'local',1.x 的 session / none 已经不支持了。
第二步:登录态 Context
src/auth-context.tsx:
import {
createContext,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { auth } from './cloudbase';
type LoginState = {
uid: string;
customUserId?: string;
loginType?: string;
} | null;
const AuthContext = createContext<{
user: LoginState;
loading: boolean;
}>({
user: null,
loading: true,
});
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<LoginState>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// hasLoginState 是同步读 localStorage,初始化时先看一下
const cached = auth.hasLoginState();
if (cached) {
setUser(auth.currentUser as any);
}
setLoading(false);
// 监听登录态变化
const handler = (params: any) => {
const eventType = params?.data?.eventType;
switch (eventType) {
case 'sign_in':
setUser(auth.currentUser as any);
break;
case 'sign_out':
case 'credentials_error':
setUser(null);
break;
}
};
auth.onLoginStateChanged(handler);
// 注意:onLoginStateChanged 当前没有显式的「移除监听」,卸载时不需要手动 cleanup
}, []);
const value = useMemo(() => ({ user, loading }), [user, loading]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export const useAuth = () => useContext(AuthContext);
第三步:短信验证码登录
短信登录是「两步走」:getVerification 拿 verificationInfo,用户填验证码后用 signInWithSms 登录。
src/login-sms.tsx:
import { useState } from 'react';
import { auth } from './cloudbase';
export function LoginBySms() {
const [phone, setPhone] = useState('');
const [code, setCode] = useState('');
const [verificationInfo, setVerificationInfo] = useState<any>(null);
const [countdown, setCountdown] = useState(0);
const [err, setErr] = useState('');
const sendCode = async () => {
setErr('');
if (!/^\+86\s?\d{11}$/.test(phone)) {
setErr('手机号格式应为 +86 13800000000');
return;
}
try {
const info = await auth.getVerification({ phone_number: phone });
setVerificationInfo(info);
// 60s 倒计时
setCountdown(60);
const t = setInterval(() => {
setCountdown((c) => {
if (c <= 1) {
clearInterval(t);
return 0;
}
return c - 1;
});
}, 1000);
} catch (e: any) {
setErr(e.message || '发送失败');
}
};
const submit = async () => {
setErr('');
try {
await auth.signInWithSms({
verificationInfo,
verificationCode: code,
phoneNum: phone,
});
// 登录成功,onLoginStateChanged 会触发 Context 更新
} catch (e: any) {
setErr(e.message || '登录失败');
}
};
return (
<form onSubmit={(e) => { e.preventDefault(); submit(); }}>
<input
placeholder="+86 13800000000"
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
<button type="button" onClick={sendCode} disabled={countdown > 0}>
{countdown > 0 ? `${countdown}s 后重发` : '获取验证码'}
</button>
<input
placeholder="6 位验证码"
value={code}
onChange={(e) => setCode(e.target.value)}
/>
<button type="submit">登录</button>
{err && <div style={{ color: 'red' }}>{err}</div>}
</form>
);
}
要点:
- 手机号要带区号,Cloudbase 接口要求
+86 13800000000这个格式(空格可有可无,以 SDK 当前接受的为准) getVerification返回的verificationInfo要原样传给signInWithSms,内含一次性 token- 同号 30s 内只能发 1 次,日上限默认 30 条/天,这个限额在控制台「身份认证 → 登录方式 → 短信验证码」里能调
第四步:邮箱验证码登录
接口形式和短信一致,只是 getVerification 入参换成 email,登录用 signInWithEmail。
src/login-email.tsx:
import { useState } from 'react';
import { auth } from './cloudbase';
export function LoginByEmail() {
const [email, setEmail] = useState('');
const [code, setCode] = useState('');
const [verificationInfo, setVerificationInfo] = useState<any>(null);
const [err, setErr] = useState('');
const sendCode = async () => {
setErr('');
try {
const info = await auth.getVerification({ email });
setVerificationInfo(info);
} catch (e: any) {
setErr(e.message);
}
};
const submit = async () => {
setErr('');
try {
await auth.signInWithEmail({
verificationInfo,
verificationCode: code,
email,
});
} catch (e: any) {
setErr(e.message);
}
};
return (
<form onSubmit={(e) => { e.preventDefault(); submit(); }}>
<input
placeholder="email@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="button" onClick={sendCode}>获取验证码</button>
<input
placeholder="6 位验证码"
value={code}
onChange={(e) => setCode(e.target.value)}
/>
<button type="submit">登录</button>
{err && <div style={{ color: 'red' }}>{err}</div>}
</form>
);
}
控制台「邮箱验证码」面板里推荐开「内置邮件代发」,无需配 SMTP,1 分钟开通(详见 email-login)。
第五步:微信开放平台扫码登录
Web 端的微信扫码走的是 OAuth 跳转流程,不是 signInWithCustomTicket(那是小程序内的)。三步:
- 前端调
genProviderRedirectUri生成微信授权页 URL,跳过去 - 用户扫码 + 授权,微信回调到
provider_redirect_uri,query 里带code和state - 回调页前端用
grantProviderToken换provider_token,再signInWithProvider登录
src/login-wechat.tsx:
import { useEffect, useState } from 'react';
import { auth } from './cloudbase';
export function LoginByWechat() {
// 入口:跳到微信授权页
const startLogin = async () => {
const state = Math.random().toString(36).slice(2);
sessionStorage.setItem('wx_state', state);
const { uri } = await auth.genProviderRedirectUri({
provider_id: 'wx_open',
provider_redirect_uri: `${location.origin}/login/wechat-callback`,
state,
});
location.href = uri;
};
return <button onClick={startLogin}>微信扫码登录</button>;
}
// 回调页:挂在 /login/wechat-callback 路由上
export function WechatCallback() {
const [err, setErr] = useState('');
useEffect(() => {
handle();
}, []);
async function handle() {
const params = new URLSearchParams(location.search);
const code = params.get('code');
const state = params.get('state');
// 校验 state 防 CSRF
const expected = sessionStorage.getItem('wx_state');
if (!code || !state || state !== expected) {
setErr('state 校验失败');
return;
}
try {
const { provider_token } = await auth.grantProviderToken({
provider_id: 'wx_open',
provider_redirect_uri: `${location.origin}/login/wechat-callback`,
provider_code: code,
});
try {
await auth.signInWithProvider({ provider_token });
// 成功,跳回首页
location.href = '/';
} catch (e: any) {
if (e.error === 'not_found') {
// 第一次用微信登录的用户,先注册 / 绑定再登录
// 简化做法:跳到一个引导页让用户用其他方式注册,然后调用 bindWithProvider
setErr('未关联,请先用其他方式注册并绑定微信');
} else {
throw e;
}
}
} catch (e: any) {
setErr(e.message);
}
}
return <div>{err ? <div style={{ color: 'red' }}>{err}</div> : '登录中...'}</div>;
}
要点:
provider_redirect_uri必须和微信开放平台「网站应用」里配的「授权回调域」一致(只比对域名不比对路径)state用来防 CSRF,登录前存,回调时校验- 第一次用微信登录的新用户会拿到
not_found错误,这时要走「先用其他方式注册 →bindWithProvider绑定 → 再次扫码登录」的两步流程,详见 wechat-login 注册流程一节
第六步:路由守卫
用 react-router-dom v6:
src/app.tsx:
import {
BrowserRouter,
Routes,
Route,
Navigate,
useLocation,
} from 'react-router-dom';
import { AuthProvider, useAuth } from './auth-context';
import { LoginBySms } from './login-sms';
import { LoginByEmail } from './login-email';
import { LoginByWechat, WechatCallback } from './login-wechat';
function RequireAuth({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) return <div>加载中...</div>;
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}
export function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/login/wechat-callback" element={<WechatCallback />} />
<Route
path="/"
element={
<RequireAuth>
<Home />
</RequireAuth>
}
/>
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
function LoginPage() {
return (
<div>
<h2>登录</h2>
<LoginBySms />
<hr />
<LoginByEmail />
<hr />
<LoginByWechat />
</div>
);
}
<RequireAuth> 没登录就导到 /login,登录态在 <AuthProvider> 里维护。
第七步:退出登录
import { auth } from './cloudbase';
async function handleSignOut() {
await auth.signOut();
// onLoginStateChanged 会触发 sign_out,Context 自动清,路由守卫把用户踢回 /login
}
signOut() 会清掉本地的登录态。token 自动刷新由 SDK 内部维护,业务侧不需要手动续期。
运行验证
npm run dev起 Vite,打开浏览器访问/,应该被重定向到/login- 用真实手机号走「短信验证码」流程,收到验证码后登录,Console 不应有错;成功后回到
/ - 控制台 → 身份认证 → 用户管理,能看到这次登录的用户记录,类型是手机号
- 退出后用同一手机号再登 录,应该是同一个 uid(不会重复创建用户)
- 邮箱流程:换邮箱试一次,同样能创建并登录
- 微信流程:点「微信扫码登录」跳到微信开放平台的扫码页,扫码授权后回到
/login/wechat-callback,完成登录
常见错误
| 错误码 / 现象 | 原因 | 修复 |
|---|---|---|
| 短信发送报「该地域不支持短信」 | Cloudbase 环境不在上海 | 短信登录仅支持上海地域,换上海环境或者改用其他登录方式 |
phone_number format invalid | 没带 +86 区号 | 中国大陆号统一传 +86 13800000000 格式 |
| 验证码 30 秒内重发提示频繁 | 单号码 30s 限发 1 条 | 客户端做 60s 倒计时 |
微信扫码回调进 /login/wechat-callback 后报错 | provider_redirect_uri 和开放平台「授权回调域」不一致 | 开放平台后台只校验域名,确认你的 location.origin 域名已添加 |
signInWithProvider 报 not_found | 这个微信账号在 Cloudbase 还没关联用户 | 走「先用手机号 / 邮箱注册 → bindWithProvider 绑定 → 再扫码」流程 |
| 退出登录后还是显示已登录 | signOut 异步,前端 setState 没触发 | 监听 onLoginStateChanged 的 sign_out 事件,在 Context 里清 user |
| 跨域 / CORS 报错 | Cloudbase Web SDK 默认走腾讯域名,不应该有 CORS;检查是不是自己反代了 | 不要反代 SDK 的请求,直接让浏览器调腾讯域名即可 |
token expired 后登录态丢失 | 离线时间太长(超过 30 天 persistence) | 让用户重新走完整登录流程 |
错误码定义参考 error-code。
相关文档
- SDK 初始化 —
cloudbase.init/auth()参数 - 短信验证码登录 — 接口流程
- 邮箱验证码登录 — 内置邮件代发 / 自定义 SMTP
- 微信开放平台登录 — OAuth 跳转 + bind 流程
- Web SDK auth 完整 API — 所有 Auth.* 签名
- add-auth-wechat-miniprogram — 端对照:小程序端怎么做
下一步
- 同一套用户体系下读写数据库:add-database-wechat-miniprogram 数据库部分对 Web 端通用
- 给 Web 端做实时通知:add-realtime-notifications-database-watch
- 多租户场景的安全规则:secure-database-multi-tenant-rules