跳到主要内容

在 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.js18
@cloudbase/js-sdk2.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(那是小程序内的)。三步:

  1. 前端调 genProviderRedirectUri 生成微信授权页 URL,跳过去
  2. 用户扫码 + 授权,微信回调到 provider_redirect_uri,query 里带 codestate
  3. 回调页前端用 grantProviderTokenprovider_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 内部维护,业务侧不需要手动续期。

运行验证

  1. npm run dev 起 Vite,打开浏览器访问 /,应该被重定向到 /login
  2. 用真实手机号走「短信验证码」流程,收到验证码后登录,Console 不应有错;成功后回到 /
  3. 控制台 → 身份认证 → 用户管理,能看到这次登录的用户记录,类型是手机号
  4. 退出后用同一手机号再登录,应该是同一个 uid(不会重复创建用户)
  5. 邮箱流程:换邮箱试一次,同样能创建并登录
  6. 微信流程:点「微信扫码登录」跳到微信开放平台的扫码页,扫码授权后回到 /login/wechat-callback,完成登录

常见错误

错误码 / 现象原因修复
短信发送报「该地域不支持短信」Cloudbase 环境不在上海短信登录仅支持上海地域,换上海环境或者改用其他登录方式
phone_number format invalid没带 +86 区号中国大陆号统一传 +86 13800000000 格式
验证码 30 秒内重发提示频繁单号码 30s 限发 1 条客户端做 60s 倒计时
微信扫码回调进 /login/wechat-callback 后报错provider_redirect_uri 和开放平台「授权回调域」不一致开放平台后台只校验域名,确认你的 location.origin 域名已添加
signInWithProvidernot_found这个微信账号在 Cloudbase 还没关联用户走「先用手机号 / 邮箱注册 → bindWithProvider 绑定 → 再扫码」流程
退出登录后还是显示已登录signOut 异步,前端 setState 没触发监听 onLoginStateChangedsign_out 事件,在 Context 里清 user
跨域 / CORS 报错Cloudbase Web SDK 默认走腾讯域名,不应该有 CORS;检查是不是自己反代了不要反代 SDK 的请求,直接让浏览器调腾讯域名即可
token expired 后登录态丢失离线时间太长(超过 30 天 persistence)让用户重新走完整登录流程

错误码定义参考 error-code

相关文档

下一步