B站的整个登录流程围绕 RSA 非对称加密与 极验(GeeTest)验证码两大核心技术构建。这套多层校验机制的目的是确保传输安全 并阻断机器自动化攻击
B站登录时,主站、风控、跨域SSO这三个系统是如何接力完成用户认证的。结合源码和抓包数据,画了张流程图,能更清晰地展示这个完整的调用链。
合作站点 (biligame/huasheng等) B站主站 (passport.bilibili.com) 浏览器 用户 合作站点 (biligame/huasheng等) B站主站 (passport.bilibili.com) 浏览器 用户 登录验证阶段 弹出极验验证码,用户完成滑块/点选 用户明文密码: hash + password,用RSA公钥加密 跨域同步阶段 登录完成 输入账号密码,点击登录 GET /x/passport-login/captcha 返回极验参数 (gt, challenge) GET /x/passport-login/web/key 返回RSA公钥和动态hash POST /x/passport-login/web/login (加密密码+极验凭证) 登录成功,返回 refresh_token GET /x/passport-login/web/sso/list 返回SSO列表(ticket数组) 触发请求 /x/passport-login/web/sso/set?ticket=... Set-Cookie (在合作站点种下登录态) 前端保存 refresh_token,用于后续token续期
==================================获取极验参数===================================
https://passport.bilibili.com/x/passport-login/captcha?source=main-fe-header&t=0.5270644240646253&web_location=333.1007&x-bili-locale-json=%7B%22c_locale%22:%7B%22language%22:%22zh%22,%22region%22:%22CN%22%7D,%22always_translate%22:true%7D
{
"code": 0,
"message": "OK",
"ttl": 1,
"data": {
"type": "geetest",
"token": "ce1ab1exxxxxxxxxd4ce6",
"geetest": {
"challenge": "972e4d7xxxxxxxxxx4b0388e9",
"gt": "ac597a4506fexxxxxxxxxxxx66dd4fe"
},
"tencent": {
"appid": ""
}
}
}
==============================获取hash和RSA公钥==================================
https://passport.bilibili.com/x/passport-login/web/key?_=177xxxxxxxx51&web_location=333.1007&x-bili-locale-json=%7B%22c_locale%22:%7B%22language%22:%22zh%22,%22region%22:%22CN%22%7D,%22always_translate%22:true%7D
{
"code": 0,
"message": "OK",
"ttl": 1,
"data": {
"hash": "80xxxxxx2b",
"key": "-----BEGIN PUBLIC KEY-----xxxxxxxxxxxxxxxxxxxxxxxxx-----END PUBLIC KEY-----\n"
}
}
=======================================登录==================================
https://passport.bilibili.com/x/passport-login/web/login?x-bili-locale-json=%7B%22c_locale%22:%7B%22language%22:%22zh%22,%22region%22:%22CN%22%7D,%22always_translate%22:true%7D&b_ret=9Qxxxxxxgg%3D%3DD%2F%2Fxxxxxxxxxxxxxx%3D
{
"code": 0,
"message": "OK",
"ttl": 1,
"data": {
"is_new": false,
"status": 0,
"message": "",
"url": "https://passport.biligame.com/x/passport-login/web/crossDomain?DedeUserID=47xxxxxxx03&DedeUserID__ckMd5=9dxxxxxxxx2&Expires=1793796288&SESSDATA=94b8xxxxxxxxx0&gourl=https%3A%2F%2Fwww.bilibili.com%2Fvideo%2FxxxxxxxxxLQ%2F%3Fspm_id_from%3D333.337.search-card.all.click%26vd_source%3Dxxxxxxxxxxxxa17a&first_domain=.bilibili.com",
"refresh_token": "fe4xxxxxxxxxxxxxxa51",
"timestamp": 1778244288968,
"hint": "",
"in_reg_audit": 0
}
}
===================================跨域同步=====================================
https://passport.bilibili.com/x/passport-login/web/sso/list?x-bili-locale-json=%7B%22c_locale%22:%7B%22language%22:%22zh%22,%22region%22:%22CN%22%7D,%22always_translate%22:true%7D
{
"code": 0,
"message": "OK",
"ttl": 1,
"data": {
"sso": [
"https://passport.biligame.com/x/passport-login/web/sso/set?ticket=06287xxxxxxxxxxxxc608",
"https://passport.bilibili.cn/x/passport-login/web/sso/set?ticket=dde9cc97xxxxxxxxxxxxxxxxxxxed48b821",
"https://passport.huasheng.cn/x/passport-login/web/sso/set?ticket=31dec9f566xxxxxxxxxxxxxxxxx48667"
]
}
}
https://passport.biligame.com/x/passport-login/web/sso/set?ticket=062872exxxxxxxx608&x-bili-locale-json=%7B%22c_locale%22:%7B%22language%22:%22zh%22,%22region%22:%22CN%22%7D,%22always_translate%22:true%7D&b_ret=9QAAxxxxxxxggg%3D%3DD%2F%2FxxxxxxxxxxxxxxxxxxxCYII%3D
https://passport.bilibili.cn/x/passport-login/web/sso/set?ticket=dde9cc97xxxxxxxxxxxxxxxxxxxed48b821&x-bili-locale-json=%7B%22c_locale%22:%7B%22language%22:%22zh%22,%22region%22:%22CN%22%7D,%22always_translate%22:true%7D&b_ret=9QAAxxxxxxxggg%3D%3DD%2F%2FxxxxxxxxxxxxxxxxxxxCYII%3D
https://passport.huasheng.cn/x/passport-login/web/sso/set?ticket=31dec9f566xxxxxxxxxxxxxxxxx48667&x-bili-locale-json=%7B%22c_locale%22:%7B%22language%22:%22zh%22,%22region%22:%22CN%22%7D,%22always_translate%22:true%7D&b_ret=9QAAxxxxxxxggg%3D%3DD%2F%2FxxxxxxxxxxxxxxxxxxxCYII%3D
🤖 请求顺序详解
整个流程的核心在于,B站登录不只是在一个站点完成认证,它需要在一个复杂的跨域生态环境中同步你的登录状态。
- 1. 验证码预请求 (
/captcha) :登录的第一步是带着source等参数向此接口发起GET请求,获取验证码服务的类型(geetest)和会话token;随后前端会初始化极验验证码,完成滑块或点选等挑战后,生成一次性验证凭证。 - 2. 获取加密公钥 (
/web/key) :为保护密码安全,请求此接口获取一个动态的hash(加盐值)和一个RSA公钥key。 - 3. 提交密码登录 (
/web/login) :客户端用公钥加密hash + 明文密码后,带上用户名、加密后的密码以及极验凭证(如validate,seccode,challenge)等信息,向此接口发送POST请求。成功后,会返回长效的refresh_token,需要妥善保管。 - 4. 跨域同步登录态 (
/web/sso/list与.../sso/set?ticket=...) :为了让你在游戏中心等网站也是登录状态,B站会先获取SSO列表,里面包含了一系列票据;然后浏览器带着这些票据去各家兄弟网站签到,各家网站会在响应头里设置Cookie,这样你的登录状态就扩散开来了。
🧩 其他关键参数与错误处理
除了核心的加密凭证,登录请求中还包含两个关键参数:用于指定登录来源(如main-fe-header)和目标跳转地址(如go_url)的source,同时请求必须携带从cookie中获取的bili_jct作为csrf token;另一个关键参数是b_ret,它是环境风控指纹。此外,如果登录失败且返回了特定错误码(如86667, 86669, 86670),通常意味着需要进行手机短信验证,此时应引导用户切换到短信登录流程
B站 miniLogin SDK 的登录加密设计是分层防护的:
- 密码加密 :使用 RSA 公钥加密
hash + password,确保传输过程无法被中间人还原。 - 人机验证:集成极验(GeeTest)第三代验证,防止自动化攻击。
- 环境指纹 :通过 WASM 模块收集浏览器特征,生成
b_ret/b_wet用于后端风控判断。 - 请求完整性:csrf token 防止跨站伪造,元数据头保证请求上下文一致。
如果你需要模拟登录,关键难点在于极验验证码的通过 (validate/seccode 的生成)以及环境 token 的模拟 (SecureCollectSDK 内部逻辑极其复杂)。密码本身的加密是标准 RSA,只需实时获取 key 即可。
hash 和 key (公钥) 通可以通过向B站的 https://passport.bilibili.com/x/passport-login/web/key 接口发送 GET 请求来获取。
📝 获取流程
- 请求接口 :向
https://passport.bilibili.com/x/passport-login/web/key发送一个GET请求。 - 解析响应 :服务器会返回一个 JSON 对象,其中包含
hash和key字段。 - 提取信息 :
hash就是加密所需的"盐";key是 RSA 公钥。
🔄 从旧接口更新
B站的这个接口经历过一次更新:
- 旧接口 :
https://passport.bilibili.com/login?act=getkey - 当前接口 :
https://passport.bilibili.com/x/passport-login/web/key
python
import requests
import json
def get_encryption_key():
"""
从B站获取用于密码加密的 hash 和公钥。
Returns:
tuple: (hash_value, public_key) 或 (None, None)
"""
url = "https://passport.bilibili.com/x/passport-login/web/key"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status() # 检查请求是否成功
data = response.json()
if data.get('code') == 0:
hash_value = data['data']['hash']
public_key = data['data']['key']
print("成功获取 hash 和公钥")
return hash_value, public_key
else:
print(f"接口返回错误: {data.get('message')}")
return None, None
except requests.exceptions.RequestException as e:
print(f"请求发生异常: {e}")
return None, None
# 示例调用
hash_str, key_str = get_encryption_key()
if hash_str and key_str:
print(f"获取到 hash: {hash_str}")
print(f"获取到 key: {key_str}")
python
def encrypt_bilibili_password(hash_value, public_key_pem, plain_password):
"""
模拟B站对密码进行RSA加密。
:param hash_value: 从 /login?act=getkey 接口获取的 hash
:param public_key_pem: 从 /login?act=getkey 接口获取的 PEM 格式公钥
:param plain_password: 用户输入的明文密码
:return: Base64 编码的加密密文
"""
# 1. 将 hash 和密码明文进行拼接
message = hash_value + plain_password
# 2. 导入 PEM 格式的公钥。注意:传入的 key 必须是字节串(bytes)
rsa_key = RSA.import_key(public_key_pem.encode('utf-8'))
# 3. 使用 PKCS#1 v1.5 填充模式创建加密器(B站前端JSEncrypt默认使用PKCS#1 v1.5填充)
cipher = PKCS1_v1_5.new(rsa_key)
# 4. 加密消息,并转换为 Base64 字符串
encrypted_message = cipher.encrypt(message.encode('utf-8'))
encrypted_base64 = base64.b64encode(encrypted_message).decode('utf-8')
return encrypted_base64
🔗 与登录流程集成
拿到 hash 和 key 后,你就可以将它们集成到完整的登录流程中了,类似于这样:
python
# 1. 获取 hash 和 key
hash_str, key_str = get_encryption_key()
if hash_str and key_str:
# 2. 加密密码
password = "用户的明文密码"
encrypted_password = encrypt_bilibili_password(hash_str, key_str, password)
# 3. 构造登录请求
login_data = {
'username': 'your_username',
'password': encrypted_password,
# ... 其他验证码等参数
}
# 4. 发送登录请求
# login_response = requests.post('https://passport.bilibili.com/web/login', data=login_data)
风控环境参数 (b_ret / b_wet)
在代码中有一个叫 RISK_ENV_TOKEN 的中间件(yo),用于收集设备的风险环境指纹,生成一个 token 添加到请求参数中。
js
// 动态加载 SecureCollectSDK
loadScript("//s1.hdslb.com/bfs/seed/jinkela/short/minntaki-wasm-sdk/bili-sc-sdk.umd.js", { lib: "SecureCollectSDK" })
.then(function(SecureCollectSDK) {
if (typeof SecureCollectSDK.getEnvToken === "function") {
var token = SecureCollectSDK.getEnvToken(raw); // raw = true/false
if (token) {
var key = raw ? "b_ret" : "b_wet"; // raw 为 true 时用 b_ret
request.params[key] = token;
}
}
});
说明:
SecureCollectSDK是一个 WebAssembly 模块,用于收集浏览器指纹、Canvas、WebGL 等信息。- 生成的环境 token 在请求参数中可能命名为
b_ret(原始模式)或b_wet。 - 登录时会在
/x/passport-login/web/login中携带该参数,作为风控检测的一部分。
核心生成逻辑与调用关系
b_ret 的生成发生在 RISK_ENV_TOKEN 中间件,具体逻辑如下:
- 加载SDK :B站动态注入
bili-sc-sdk.umd.js,该文件会初始化SecureCollectSDK这个 WASM 模块。 - 调用方法 :核心方法是
SecureCollectSDK.getEnvToken(raw),该方法内部执行浏览器环境的数据采集、指纹计算和加密,并返回最终的 Token。 - 参数控制 :方法接受的布尔参数
raw决定了输出格式:当raw = true时生成b_ret;当raw = false时生成b_wet。
具体是怎么拼出来的
- 生成 Canvas 指纹 :利用
Fingerprint2库在<canvas>上绘制特定文本和图形,然后用toDataURL导出 Base64 图片数据。从完整的 Canvas 指纹字符串中截取最后 20 个字符 作为canvas。 - 生成 WebGL 指纹 :获取 WebGL 的渲染器、供应商等信息,拼接成一个长字符串。从完整的 WebGL 指纹字符串中截取最后 50 个字符 作为
webgl_str。 - 拼接 :最终参数
b_ret = canvas + webgl_str,即完全是这两个截取片段的简单黏合。
结论与验证
你可以直接用下面的代码在浏览器控制台验证,它破解了指纹采集过程,直接拿到 canvas 和 webgl_str,然后像 SDK 一样拼接,得到的就是 b_ret。
javascript
js
// 模拟 canvas 指纹截取(需要 Fingerprint2 或自行绘制)
// 这里直接用 Fingerprint2 示例 (需在 B 站页面环境)
Fingerprint2.get({ excludes: {} }, (components) => {
const canvasValue = components.find(c => c.key === 'canvas').value;
const canvasStr = Array.isArray(canvasValue) ? String(canvasValue[1]).slice(-20) : '';
const webglValue = components.find(c => c.key === 'webgl').value;
const webglStr = Array.isArray(webglValue) ? String(webglValue[0]).slice(-50) : '';
const b_ret = canvasStr + webglStr;
console.log('b_ret:', b_ret);
});
这就完全拆解了 b_ret 的生成过程。它就是一个典型的、基于前端 Fingerprint 拼接的环境校验参数,目的不是为了加密,而是为了给后端提供一份稳定的浏览器/设备特征签名。
Uncaught ReferenceError: Fingerprint2 is not defined at :5:1
在 getEnvironmentInfo 中,它返回一个 promise,内部使用了 b.get (Fingerprint2.get)。但是 Fingerprint2 是在函数内部通过 var b = f(p.exports) 引用的,没有暴露到全局,所以我们无法直接调用 Fingerprint2.get。但我们可以调用 SecureCollectSDK.getEnvironmentInfo() 这个方法。
那么控制台代码可以这样写:
js
// 确保 SecureCollectSDK 已初始化
window.SecureCollectSDK.getEnvironmentInfo().then(env => {
const canvasStr = String(env.canvas || '');
const webglStr = String(env.webgl_str || '');
const b_ret = canvasStr + webglStr;
console.log('b_ret:', b_ret);
});
你可以在控制台运行下面这行代码,直接异步拿到 b_ret 的值:
js
window.SecureCollectSDK.getEnvToken(true).then(console.log)
这行代码调用了 SDK 的公开方法,它会:
- 异步采集 Canvas 和 WebGL 指纹
- 因为参数为
true,直接把这两个指纹片段拼接起来返回给你
其他安全参数
