快速开始
1. 创建 App
扫码登录开发者中心,在“应用管理”创建 App。App Secret 只显示一次;丢失后只能重置。
2. 请求登录二维码
调用 POST /api/open/login/qrcode。请求头:
| 请求头 | 值 |
|---|---|
X-CL-App-Id | App ID |
X-CL-Timestamp | Unix 秒,允许前后 5 分钟 |
X-CL-Nonce | 16–128 位随机字符串,10 分钟内不可重复 |
X-CL-Signature | HMAC-SHA256 十六进制小写签名 |
签名原文依次拼接并用换行分隔:HTTP 方法、路径、规范化查询串、timestamp、nonce、原始请求体 SHA-256。
以下示例使用 Node.js 18 或更高版本,无需安装第三方依赖。先设置三个环境变量:
COOL_LOGIN_BASE_URL=https://login.example.com
COOL_LOGIN_APP_ID=cl_app_xxx
COOL_LOGIN_APP_SECRET=cl_secret_xxx
创建 cool-login.mjs,加入生成签名请求头的公共代码:
import { createHash, createHmac, randomBytes } from 'node:crypto'
const baseUrl = process.env.COOL_LOGIN_BASE_URL
const appId = process.env.COOL_LOGIN_APP_ID
const appSecret = process.env.COOL_LOGIN_APP_SECRET
if (!baseUrl || !appId || !appSecret) {
throw new Error('请设置 COOL_LOGIN_BASE_URL、COOL_LOGIN_APP_ID 和 COOL_LOGIN_APP_SECRET')
}
function encodeRfc3986(value) {
return encodeURIComponent(value).replace(/[!'()*]/g, character =>
`%${character.charCodeAt(0).toString(16).toUpperCase()}`,
)
}
function canonicalizeQuery(searchParams) {
return Array.from(searchParams.entries())
.filter(([key]) => key !== 'signature')
.map(([key, value]) => [encodeRfc3986(key), encodeRfc3986(value)])
.sort(([leftKey, leftValue], [rightKey, rightValue]) =>
leftKey.localeCompare(rightKey) || leftValue.localeCompare(rightValue),
)
.map(([key, value]) => `${key}=${value}`)
.join('&')
}
function createSignedHeaders(method, url, body = '') {
const timestamp = Math.floor(Date.now() / 1000).toString()
const nonce = randomBytes(16).toString('hex')
const bodyHash = createHash('sha256').update(body, 'utf8').digest('hex')
const canonicalRequest = [
method.toUpperCase(),
url.pathname,
canonicalizeQuery(url.searchParams),
timestamp,
nonce,
bodyHash,
].join('\n')
const signature = createHmac('sha256', appSecret)
.update(canonicalRequest, 'utf8')
.digest('hex')
return {
'X-CL-App-Id': appId,
'X-CL-Timestamp': timestamp,
'X-CL-Nonce': nonce,
'X-CL-Signature': signature,
}
}
接着请求登录二维码。该接口没有请求参数,请求体为空字符串:
async function createLoginQrCode() {
const method = 'POST'
const url = new URL('/api/open/login/qrcode', baseUrl)
const response = await fetch(url, {
method,
headers: createSignedHeaders(method, url),
})
const result = await response.json()
if (!response.ok || !result.success) {
throw new Error(result.message || `请求二维码失败:HTTP ${response.status}`)
}
return result.data
}
const { ticket, qrCodeBase64, expiresIn } = await createLoginQrCode()
console.log({ ticket, qrCodeBase64, expiresIn })
成功响应同时包含 ticket、qrCodeBase64 和 expiresIn。直接把 Base64 Data URL 放到图片元素中展示。
3. 轮询结果
对 GET /api/open/login/status?ticket=... 使用同样的签名规则。状态为 success 时同时返回:
在同一个 cool-login.mjs 中继续加入轮询代码。查询参数必须先写入 URL,再参与签名;每次轮询也必须重新生成 timestamp 和 nonce:
const wait = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
async function getLoginStatus(ticket) {
const method = 'GET'
const url = new URL('/api/open/login/status', baseUrl)
url.searchParams.set('ticket', ticket)
const response = await fetch(url, {
method,
headers: createSignedHeaders(method, url),
})
const result = await response.json()
if (!response.ok || !result.success) {
throw new Error(result.message || `查询登录状态失败:HTTP ${response.status}`)
}
return result.data
}
async function waitForLogin(ticket) {
while (true) {
const result = await getLoginStatus(ticket)
if (result.status === 'success') {
return {
unionUserId: result.unionUserId,
appUserId: result.appUserId,
}
}
if (result.status === 'expired') {
throw new Error('登录二维码已过期,请重新获取')
}
if (result.status === 'failed') {
throw new Error(`登录失败:${result.errorCode || 'unknown'}`)
}
await wait(1500)
}
}
const user = await waitForLogin(ticket)
console.log('登录成功', user)
| 字段 | 稳定范围 | 用途 |
|---|---|---|
unionUserId | 当前开发者名下的所有 App | 在自己的多个 App 之间识别并共享同一用户 |
appUserId | 当前 App | 建立只属于当前 App 的独立用户记录 |
{
"success": true,
"data": {
"ticket": "cl_xxx",
"status": "success",
"unionUserId": "cl_union_v1_xxx",
"appUserId": "cl_app_user_v1_xxx"
}
}
两个字段均由 Cool Login 对微信身份进行不可逆保护后生成,不是微信原始 OpenID 或 UnionID。同一微信用户登录同一开发者的不同 App 时,unionUserId 相同而 appUserId 不同。结果窗口为 30 秒,之后返回 expired。
不要在浏览器里生成签名或暴露 App Secret。轮询必须由你的服务端完成。
使用托管登录页
如果不想自行展示二维码和维护轮询页面,可以使用 Portal 便捷登录。Cool Login 会完成二维码展示和扫码状态确认,再携带原 ticket 返回你的应用;你的服务端对现有状态接口查询一次即可。