在 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> 里维护。