跳到主要内容

在 Next.js 中接入 CloudBase 用户认证

一句话定义:用 @cloudbase/js-sdk 在 Next.js 14+ App Router 里做手机短信验证码 / 微信开放平台扫码登录,登录态用 httpOnly cookie 存,Server Component 通过 cookies() 拿身份,middleware 在 Edge Runtime 做轻校验拦未登录请求,重业务在 runtime: 'nodejs' 的 Route Handler 里跑。

预计耗时:35 分钟 | 难度:进阶

适用场景

  • 适用:Next.js 14 / 15 App Router 全栈应用,SSR 页面要拿用户身份做个性化渲染
  • 适用:已经用 add-auth-web-with-cloudbase-sdk 的 React SPA,想迁到 Next.js 拿到 SSR + middleware 拦截能力
  • 适用:用 middleware 做未登录拦截 / 灰度路由 / A-B 分流
  • 不适用:纯 Pages Router 项目,直接走 add-auth-web-with-cloudbase-sdk 的 SPA 模板即可,Pages Router 没有 Server Component / Server Action 的概念
  • 不适用:在 Node.js 端拿 admin 级登录态(那要走 @cloudbase/node-sdk 的 server token,不是本篇范畴)

环境要求

依赖版本
Node.js18.18.0
next14.2.x15.x
@cloudbase/js-sdk2.27.3(也可用 @cloudbase/js-sdk@next3.3.x)
react / react-dom^18.3.0(Next.js 15 用 ^19.0.0)

外部依赖:

  • 已开通的 CloudBase 环境 ID,地域选「上海」(短信验证码登录仅支持上海地域)
  • 控制台 → 身份认证 → 登录方式,把要用的方式开启:
    • 「短信验证码登录」
    • 「微信开放平台登录」,填入 微信开放平台 网站应用的 AppID 和 AppSecret
Next.js 15 cookies() 是 async

Next.js 15 把 cookies() / headers() / params / searchParams 都改成了 async,必须 await 再用。Next.js 14 是 sync 的,沿用旧写法即可。本篇按 Next.js 15 写,Next.js 14 把 await cookies() 改成 cookies() 即可。

第一步:Next.js 项目装 @cloudbase/js-sdk

npx create-next-app@latest my-cloudbase-app --typescript --app --tailwind=false --eslint --src-dir=false --import-alias="@/*"
cd my-cloudbase-app
npm i @cloudbase/js-sdk

新建 lib/cloudbase.ts(纯客户端用,SDK 用了 crypto / localStorage):

'use client';

import cloudbase from '@cloudbase/js-sdk';

export const app = cloudbase.init({
env: 'your-env-id',
// 不传 region 默认上海;短信登录只在上海支持
});

export const auth = app.auth({ persistence: 'local' });

第二步:Client Component 写登录页(短信验证码 + 微信扫码)

app/login/page.tsx(标 'use client' 因为要用 SDK):

'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { auth } from '@/lib/cloudbase';
import { setSessionCookie } from './actions';

export default function LoginPage() {
const router = useRouter();
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);
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 (e: React.FormEvent) => {
e.preventDefault();
setErr('');
try {
await auth.signInWithSms({
verificationInfo,
verificationCode: code,
phoneNum: phone,
});
// 登录成功,拿 access token 写到 httpOnly cookie
const loginState = await auth.getLoginState();
if (!loginState) throw new Error('登录态为空');
await setSessionCookie(loginState.accessToken);
router.push('/');
} catch (e: any) {
setErr(e.message || '登录失败');
}
};

return (
<form onSubmit={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>
);
}

要点:

  • auth.signInWithSms 走的是「getVerificationsignInWithSms」两步,verificationInfogetVerification 返回的对象,原样传回去
  • 客户端登录成功后,通过 Server Action setSessionCookieaccessToken 写进 httpOnly cookie,这样 Server Component 和 middleware 才能拿到
  • 微信扫码登录走 OAuth 跳转,流程见 add-auth-web-with-cloudbase-sdk 第五步,在 Next.js 里只需把回调页换成 app/login/wechat-callback/page.tsx 即可,逻辑不变

app/login/actions.ts(标 'use server'):

'use server';

import { cookies } from 'next/headers';

export async function setSessionCookie(accessToken: string) {
const cookieStore = await cookies();
cookieStore.set('cloudbase_session', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24 * 30, // 30 天,和 SDK 的 persistence: 'local' 对齐
});
}

export async function clearSessionCookie() {
const cookieStore = await cookies();
cookieStore.delete('cloudbase_session');
}

要点:

  • httpOnly: true 让 JS 读不到 cookie,XSS 拿不走 token
  • sameSite: 'lax' 是默认推荐;不要写 'strict',跨站跳转(比如微信扫码回调)就不发 cookie 了
  • secure: true 生产环境必开,本地开发改成 process.env.NODE_ENV === 'production',本地 HTTP 也能用
  • Server Component 不能直接 set/delete cookie,只能读;写操作必须放在 Server Action 或 Route Handler 里

第四步:Server Component 通过 cookies() 拿身份

app/page.tsx(默认就是 Server Component,不要'use client'):

import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export default async function HomePage() {
const cookieStore = await cookies(); // Next.js 15 必须 await
const accessToken = cookieStore.get('cloudbase_session')?.value;

if (!accessToken) {
redirect('/login');
}

// 这里如果只需要展示「已登录」状态,有 cookie 就够了
// 要拿用户详细信息,通过 Route Handler 调 CloudBase 服务端 API,或者把 uid 一起塞进 cookie
return (
<div>
<h1>欢迎</h1>
<p>当前已登录(token 长度 {accessToken.length})</p>
</div>
);
}

如果要在 Server Component 里拿到用户 uid、手机号这些信息,有两种做法:

  1. 第二步登录成功时,除了写 accessToken,再写一个 cloudbase_uid cookie,Server Component 直接读这个
  2. Server Component 调一个内部 Route Handler(runtime: 'nodejs'),Route Handler 用 accessToken 调 CloudBase 服务端接口换详细信息(见第六步)

第五步:middleware.ts 拦未登录请求

middleware.ts(放项目根目录,不是 app/ 下):

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
const session = request.cookies.get('cloudbase_session');
const { pathname } = request.nextUrl;

// 已登录用户访问 /login,推回首页
if (session && pathname.startsWith('/login')) {
return NextResponse.redirect(new URL('/', request.url));
}

// 未登录用户访问受保护路由,踢到 /login
if (!session && !pathname.startsWith('/login')) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname);
return NextResponse.redirect(loginUrl);
}

return NextResponse.next();
}

export const config = {
matcher: ['/((?!login|_next/static|_next/image|favicon.ico|api/public).*)'],
};

要点:

  • middleware 跑在 Edge Runtime,@cloudbase/js-sdk 用了 Node API(cryptofs),不能在 middleware 里直接 import
  • middleware 只做轻校验:cookie 是否存在、格式是否合法。token 是否真有效、是否过期、对应哪个 uid,这些放 Server Component 或 Route Handler
  • matcher 用否定 lookahead (?!...) 排除不需要拦截的路径;遗漏 _next/static 会让首屏 chunk 也被重定向,页面直接白屏
  • 已登录用户访问 /login 推回首页,体验上避免「点登录但已登录」的死循环

第六步:Server Action 调 CloudBase API(写数据)

业务里要用 accessToken 调 CloudBase 服务端接口(读数据库 / 调云函数),建议放 Server Action 或 Route Handler,而且要 runtime: 'nodejs'

app/api/profile/route.ts:

import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

// 关键:强制 Node Runtime,@cloudbase/js-sdk 不能跑在 Edge
export const runtime = 'nodejs';

export async function GET() {
const cookieStore = await cookies();
const accessToken = cookieStore.get('cloudbase_session')?.value;
if (!accessToken) {
return NextResponse.json({ error: 'unauthenticated' }, { status: 401 });
}

// 在 Node Runtime 里,可以正常 import 和用 SDK / 调 HTTP
// 用 accessToken 直接调 CloudBase 服务端 OpenAPI,或者用 @cloudbase/node-sdk
// 本例只演示直接转发 cookie
return NextResponse.json({ ok: true, hasSession: true });
}

要点:

  • 任何要 import '@cloudbase/js-sdk'import '@cloudbase/node-sdk' 的服务端代码,必须 export const runtime = 'nodejs'
  • 默认 Route Handler 在 Next.js 里会跟随父级 route.ts 的配置,显式声明最稳

退出登录

'use client';

import { auth } from '@/lib/cloudbase';
import { clearSessionCookie } from '@/app/login/actions';
import { useRouter } from 'next/navigation';

export function SignOutButton() {
const router = useRouter();

const handleSignOut = async () => {
await auth.signOut(); // 清掉 SDK 的本地登录态
await clearSessionCookie(); // Server Action 删 cookie
router.push('/login');
};

return <button onClick={handleSignOut}>退出登录</button>;
}

signOut() 清 SDK 端,clearSessionCookie() 清服务端;两步都要做,缺一会出现「客户端已登出但 SSR 还显示已登录」。

运行验证

  1. npm run dev 起服务,浏览器访问 http://localhost:3000,应该被 middleware 重定向到 /login?from=/
  2. 用真实手机号走短信验证码流程,登录成功后回到 /,Server Component 渲染「欢迎」页
  3. 控制台 → 身份认证 → 用户管理,能看到这次登录的用户记录,类型是手机号
  4. 浏览器 DevTools → Application → Cookies,能看到 cloudbase_session 这个 cookie,HttpOnly 列是 ✓,JavaScript 在 Console 里 document.cookie 读不到它
  5. 直接刷新页面(F5),Server Component 仍然认得登录态,不会被踢回 /login(SSR 拿到了 cookie)
  6. 点退出登录,cookie 被清,刷新后重新被踢到 /login

常见错误

错误码 / 现象原因修复
cookies().get is not a functionProperty 'get' does not existNext.js 15 把 cookies() 改成 async,直接调用拿到的是 Promise改成 const cookieStore = await cookies(); cookieStore.get(...)
middleware 里 import cloudbase from '@cloudbase/js-sdk'crypto is not definedModule not found: Can't resolve 'fs'middleware 跑在 Edge Runtime,SDK 用了 Node APImiddleware 不要 import SDK,只读 cookie 做存在性判断;真校验放 runtime: 'nodejs' 的 Route Handler
Server Component 里调 cookieStore.set(...)Cookies can only be modified in a Server Action or Route HandlerRSC 流式渲染时 response header 已发,改不了 cookieset / delete 操作放进 'use server' 的 Server Action 里
cookieStore.set 加了 sameSite: 'strict' 后,微信扫码回调不带 cookiestrict 模式跨站跳转不发 cookie改成 sameSite: 'lax'(默认推荐)
middleware 配了 matcher: '/' 之后 /about 没拦到matcher 写成单字符串只精确匹配 /,不是前缀用否定 lookahead 写法 ['/((?!login|_next).*)' ],或多条规则
短信发送报「该地域不支持短信」CloudBase 环境不在上海短信登录仅支持上海地域,换上海环境或者改用其他登录方式
phone_number format invalid没带 +86 区号中国大陆号统一传 +86 13800000000
登录成功后刷新页面又被踢回 /loginServer Action 写 cookie 时漏了 path: '/',默认只在当前 path 下生效cookieStore.set 显式加 path: '/'
部署到 Vercel / CloudBase 托管后 cookie 不生效生产环境 secure: false 时浏览器不存,或 secure: true 但站点是 HTTP生产必须 HTTPS + secure: true;本地 HTTP 用 secure: process.env.NODE_ENV === 'production'

错误码定义参考 error-code

相关文档