
这是一个极其聪明、完全合法、且被无数非官方工具(如各种游戏战绩查询网、第三方社区)广泛采用的“曲线救国”型 OAuth 替代方案。
在没有官方 API 授权的情况下,“随机验证码 + 修改公开签名/简介 + 爬虫验证” 确实是证明账号归属权的唯一可靠方法。

1234567)。GTAMODX-8A9B2C)。1234567 的当前签名。GTAMODX-8A9B2C,则验证通过!我们需要在 User 表中增加 B站信息的冗余字段,同时为了支持“解绑、重新绑定、多平台绑定”的未来扩展,建议建立一张独立的 第三方账号绑定表 (SocialAccount)。
// lib/schema.ts
model User {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
// ... 其他字段 ...
// 关联的第三方账号
socialAccounts SocialAccount[] @relation("UserSocialAccounts")
}
// --- 第三方账号绑定表 (SocialAccounts) ---
model SocialAccount {
id String @id @default(uuid()) @db.Uuid
userId String @db.Uuid
user User @relation("UserSocialAccounts", fields: [userId], references: [id], onDelete: Cascade)
// 平台类型 (例如: 'BILIBILI', 'STEAM', 'DISCORD')
platform String @db.VarChar(20)
// 第三方平台的唯一 ID (例如 B站的 UID: '1234567')
platformId String @db.VarChar(100)
// (可选) 冗余存储第三方平台的用户名/昵称,方便展示
platformName String? @db.VarChar(100)
// (可选) 冗余存储第三方平台的头像
platformAvatar String? @db.Text
// 绑定时间
boundAt DateTime @default(now())
// 联合唯一约束:
// 1. 一个用户在一个平台只能绑定一个账号
@@unique([userId, platform])
// 2. 一个第三方账号只能被一个平台用户绑定 (防止多个人绑同一个 B站号)
@@unique([platform, platformId])
@@index([userId])
@@map("social_accounts")
}
千万不要把验证码存进 PostgreSQL! 这种临时性的 Challenge 数据,5 分钟后就失效了,存进数据库只会产生大量垃圾。用 Redis 是最完美的。
bind:bilibili:{userId} (例如: bind:bilibili:uuid-1234){ "uid": "1234567", "code": "GTAMODX-8A9B2C" } (JSON 字符串)我们需要两个 Action:一个生成验证码,一个执行爬虫验证。
// app/actions/bilibili.ts
'use server';
import { redis } from '@/lib/redis';
import { getSession } from '@/lib/session';
import { randomBytes } from 'crypto';
export async function requestBilibiliBind(bilibiliUid: string) {
const session = await getSession();
if (!session.user) throw new Error("未登录");
const userId = session.user.id;
// 1. 检查这个 UID 是否已经被别人绑了 (防碰撞)
const existing = await prisma.socialAccount.findUnique({
where: { platform_platformId: { platform: 'BILIBILI', platformId: bilibiliUid } }
});
if (existing) return { error: "该 B站账号已被其他用户绑定" };
// 2. 生成 6 位随机大写字母+数字的验证码
const code = `GTAMODX-${randomBytes(3).toString('hex').toUpperCase()}`;
// 3. 存入 Redis (5分钟过期)
await redis.setex(
`bind:bilibili:${userId}`,
300,
JSON.stringify({ uid: bilibiliUid, code })
);
return { success: true, code };
}
你需要调用 B 站的公开 API。B站获取用户信息的免登录(或者只需要随意一点 Headers)接口通常是:
https://api.bilibili.com/x/space/wbi/acc/info?mid={UID}
(注意:B站 API 有风控,如果请求太频繁可能会被拦截 412。但在用户手动触发绑定的低频场景下,通常是安全的。建议在 fetch 里加上模拟浏览器的 User-Agent。)
// app/actions/bilibili.ts
export async function verifyBilibiliBind() {
const session = await getSession();
if (!session.user) throw new Error("未登录");
const userId = session.user.id;
// 1. 从 Redis 取出用户正在尝试绑定的 UID 和 Code
const dataStr = await redis.get(`bind:bilibili:${userId}`);
if (!dataStr) return { error: "验证码已过期,请重新获取" };
const { uid, code } = JSON.parse(dataStr);
try {
// 2. 调用 B站公开 API 获取用户信息
const res = await fetch(`https://api.bilibili.com/x/space/wbi/acc/info?mid=${uid}`, {
headers: {
// 伪装一下 UA,防止被 B站 WAF 直接拦截
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
// 'Referer': 'https://space.bilibili.com/' // 有时候需要带上 Referer
}
});
const bData = await res.json();
if (bData.code !== 0) {
return { error: "无法获取 B站用户信息,请检查 UID 是否正确" };
}
// 3. 获取 B站个性签名 (sign)
const sign = bData..;
bName = bData..;
bAvatar = bData..;
(!sign || !sign.(code)) {
{ : };
}
prisma.$transaction( (tx) => {
exists = tx..({
: { : { : , : uid } }
});
(exists) ();
tx..({
: {
userId,
: ,
: uid,
: bName,
: bAvatar
}
});
});
redis.();
{ : , : };
} (: ) {
.(, error);
{ : error. || };
}
}
SocialAccount):比在 User 表里加 bilibili_uid 字段要好一万倍。未来你想加 Steam 绑定、Discord 绑定,表结构一行都不用改,只需要在业务层加个枚举就行。@@unique([platform, platformId]) 数据库级死锁,彻底杜绝了两个人绑定同一个 B站账号刷奖励的漏洞。