本文介绍在 React + Node(Koa) 项目中接入阿里云号码认证服务(H5 一键登录)的完整流程,包含前端网页端 SDK 与后端 OpenAPI 的对接,以及常见坑点与解决方案。适合有一定前后端基础、希望为 H5 页面增加「本机号一键登录」能力的开发者。
一、什么是 H5 一键登录?
一键登录是指用户无需输入手机号和验证码 ,在授权页确认本机号码并同意协议后,由运营商(移动/联通/电信)返回该手机号,完成登录或注册。
在 H5 网页 里实现时,需要用到:
- 阿里云号码认证服务:提供 H5 能力与 OpenAPI。
- 网页端 SDK :
aliyun_numberauthsdk_web,负责鉴权、拉起授权页、获取spToken。 - 服务端 :调用阿里云 GetAuthToken 、GetPhoneWithToken 两个接口,拿到鉴权 Token 和最终手机号,并完成自家业务的登录/注册。
整体流程可以概括为:
- 前端向自家后端要鉴权 Token(accessToken、jwtToken)。
- 后端调用阿里云 GetAuthToken,把 Token 返回给前端。
- 前端用 SDK checkLoginAvailable 鉴权,通过后 getLoginToken 拉起授权页。
- 用户输入手机号中间 4 位、勾选协议并点击登录,SDK 返回 spToken。
- 前端把 spToken 交给后端,后端调用 GetPhoneWithToken 拿到手机号,再按业务做登录/注册,返回登录态。
下面按前置准备、后端、前端、注意事项四块说明,并给出本项目中的核心代码。
二、前置准备:阿里云控制台
-
开通 号码认证服务,并创建 H5 认证方案。
-
在方案中配置:
- 页面地址 :
协议 + // + 域名 + /,如http://yourdomain.com/ - 请求来源(源地址) :
协议 + // + 域名,如http://yourdomain.com(末尾不要/)
- 页面地址 :
-
记录 方案 Code (SceneCode),以及 AccessKey ID / AccessKey Secret(RAM 子账号即可,需具备号码认证相关权限)。
注意 :中国移动侧能力需在创建方案后的第 2 个工作日才能使用;若遇 105113「AppID 非法或为空」,可先核对页面地址/源地址是否与控制台完全一致,或等待生效后再试。
三、后端实现(Node.js + Koa)
技术栈:Koa + @alicloud/dypnsapi20170525 。
需要完成三件事:加载环境变量 、封装阿里云 GetAuthToken / GetPhoneWithToken 、提供两个业务接口。
3.1 依赖与环境变量
bash
npm install @alicloud/dypnsapi20170525 dotenv --save
在项目根目录新建 .env(不要提交到 Git),例如:
env
ALIYUN_ACCESS_KEY_ID=你的AccessKeyId
ALIYUN_ACCESS_KEY_SECRET=你的AccessKeySecret
PNVS_SCENE_CODE=FC000000012345678
PNVS_PAGE_URL=https://www.yourdomain.com/
PNVS_ORIGIN=https://www.yourdomain.com
在入口最先加载 dotenv(在 bin/www 或 app.js 最顶部):
javascript
// bin/www 最顶部
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') })
3.2 封装阿里云接口:numberAuth.js
在 services/numberAuth.js 中封装两个方法:getAuthToken 、getPhoneWithToken。
javascript
/**
* 阿里云号码认证服务(H5 一键登录)
* 文档:https://help.aliyun.com/zh/pnvs/developer-reference/h5-client-access
*/
const DypnsapiClient = require('@alicloud/dypnsapi20170525').default
const GetAuthTokenRequest = require('@alicloud/dypnsapi20170525').GetAuthTokenRequest
const GetPhoneWithTokenRequest = require('@alicloud/dypnsapi20170525').GetPhoneWithTokenRequest
const accessKeyId = process.env.ALIYUN_ACCESS_KEY_ID || ''
const accessKeySecret = process.env.ALIYUN_ACCESS_KEY_SECRET || ''
const sceneCode = process.env.PNVS_SCENE_CODE || ''
const defaultUrl = process.env.PNVS_PAGE_URL || 'https://example.com/'
const defaultOrigin = process.env.PNVS_ORIGIN || 'https://example.com'
function createClient() {
return new DypnsapiClient({
accessKeyId,
accessKeySecret,
endpoint: 'dypnsapi.aliyuncs.com',
})
}
/** 获取 H5 鉴权 Token,供前端 checkLoginAvailable 使用 */
async function getAuthToken(opts = {}) {
if (!accessKeyId || !accessKeySecret || !sceneCode) {
throw new Error('请配置 ALIYUN_ACCESS_KEY_ID、ALIYUN_ACCESS_KEY_SECRET、PNVS_SCENE_CODE')
}
const url = opts.url || defaultUrl
const origin = opts.origin || defaultOrigin
const client = createClient()
const request = new GetAuthTokenRequest({
url,
origin,
sceneCode,
bizType: 1, // 1:一键登录
})
const response = await client.getAuthToken(request)
const body = response.body
if (body.code !== 'OK') {
throw new Error(body.message || 'GetAuthToken 失败')
}
const tokenInfo = body.tokenInfo || {}
return {
accessToken: tokenInfo.accessToken || '',
jwtToken: tokenInfo.jwtToken || '',
}
}
/** 用 spToken 换取手机号(H5 一键登录取号) */
async function getPhoneWithToken(spToken) {
if (!accessKeyId || !accessKeySecret) throw new Error('请配置阿里云 AccessKey')
if (!spToken) throw new Error('spToken 不能为空')
const client = createClient()
const request = new GetPhoneWithTokenRequest({ spToken })
const response = await client.getPhoneWithToken(request)
const body = response.body
if (body.code !== 'OK') {
throw new Error(body.message || 'GetPhoneWithToken 失败')
}
const mobile = body.data && body.data.mobile
if (!mobile) throw new Error('未获取到手机号')
return mobile
}
module.exports = { getAuthToken, getPhoneWithToken }
3.3 业务接口:auth 路由
在 routes/auth.js 中增加两个接口。
1)获取鉴权 Token(给前端 SDK 用)
javascript
const { getAuthToken, getPhoneWithToken } = require('../services/numberAuth')
// POST /api/auth/number-auth-token
router.post('/number-auth-token', async (ctx) => {
const { url, origin } = ctx.request.body || {}
try {
const tokenInfo = await getAuthToken({ url, origin })
ctx.body = { code: 0, message: 'ok', data: tokenInfo }
} catch (e) {
console.error('number-auth-token error:', e.message)
ctx.status = 500
ctx.body = { code: 500, message: e.message || '获取认证失败' }
}
})
2)用 spToken 换手机号并登录/注册
javascript
// POST /api/auth/number-login
router.post('/number-login', async (ctx) => {
const { spToken } = ctx.request.body || {}
if (!spToken) {
ctx.status = 400
ctx.body = { code: 400, message: '缺少 spToken' }
return
}
try {
const phone = await getPhoneWithToken(spToken)
// 查库:已有用户则登录,否则自动注册(随机密码,后续可改密)
const [rows] = await db.query(
'SELECT id, phone, name, password FROM users WHERE phone = ? LIMIT 1',
[String(phone).trim()]
)
let user = rows[0]
if (!user) {
const name = `用户${phone.slice(-4)}`
const randomPassword = crypto.randomBytes(24).toString('base64')
const hash = await bcrypt.hash(randomPassword, 10)
const [insertResult] = await db.query(
'INSERT INTO users (phone, password, name) VALUES (?, ?, ?)',
[String(phone).trim(), hash, name]
)
user = {
id: insertResult.insertId,
phone: String(phone).trim(),
name,
}
}
const token = signToken(user.id)
ctx.body = {
code: 0,
message: '登录成功',
data: { token, user: { id: user.id, phone: user.phone, name: user.name } },
}
} catch (e) {
console.error('number-login error:', e.message)
ctx.status = 500
ctx.body = { code: 500, message: e.message || '一键登录失败' }
}
})
四、前端实现
4.1 安装依赖
bash
npm install aliyun_numberauthsdk_web --save
4.2 登录页:一键登录流程(Login.jsx 核心逻辑)
jsx
import { PhoneNumberServer } from 'aliyun_numberauthsdk_web'
import { authApi } from '../api/request'
// 1. 在组件内创建 SDK 实例(useRef,避免重复创建)
const phoneNumberServerRef = useRef(null)
useEffect(() => {
phoneNumberServerRef.current = new PhoneNumberServer()
return () => { phoneNumberServerRef.current = null }
}, [])
// 2. 一键登录点击处理
const handleOneClickLogin = async () => {
const phoneNumberServer = phoneNumberServerRef.current
if (!phoneNumberServer) return
// H5 一键登录要求走移动数据,WiFi 下运营商无法可靠取号
const netType = phoneNumberServer.getConnection?.()
if (netType === 'wifi') {
Toast.show({ content: '一键登录需使用移动数据网络,请关闭 Wi-Fi 后重试', icon: 'fail' })
return
}
setOneClickLoading(true)
try {
// Step1:向自家后端要 accessToken、jwtToken
const res = await authApi.getNumberAuthToken({
url: `${window.location.origin}/`,
origin: window.location.origin,
})
const { accessToken, jwtToken } = res.data || {}
// Step2:SDK 鉴权
phoneNumberServer.checkLoginAvailable({
accessToken,
jwtToken,
success: (res) => {
if (res.code !== 600000) {
setOneClickLoading(false)
Toast.show({ content: res.msg || '鉴权失败', icon: 'fail' })
return
}
// Step3:拉起授权页(用户输入中间 4 位、勾选协议、点登录)
phoneNumberServer.getLoginToken({
authPageOption: {
navText: '本机号码登录',
btnText: '立即登录',
privacyBefore: '我已阅读并同意',
privacyOne: ['《用户协议》', '/about'],
privacyTwo: ['《隐私政策》', '/about'],
isDialog: true,
manualClose: true,
},
success: async (tokenRes) => {
if (tokenRes.code !== 600000) {
setOneClickLoading(false)
Toast.show({ content: tokenRes.msg || '获取登录态失败', icon: 'fail' })
return
}
// Step4:把 spToken 交给后端,完成登录/注册
try {
const { data: loginData } = await authApi.loginWithSpToken(tokenRes.spToken)
phoneNumberServer.closeLoginPage?.()
localStorage.setItem('app_token', loginData.token)
localStorage.setItem('app_user', JSON.stringify(loginData.user))
setUser(loginData.user)
setIsLoggedIn(true)
Toast.show({ content: '登录成功', icon: 'success' })
navigate('/')
} catch (e) {
Toast.show({ content: e.message || '登录失败', icon: 'fail' })
} finally {
setOneClickLoading(false)
}
},
error: () => {
setOneClickLoading(false)
Toast.show({ content: '授权取消或失败', icon: 'fail' })
},
})
},
error: (err) => {
setOneClickLoading(false)
Toast.show({ content: err?.msg || '鉴权失败,请关闭 Wi-Fi 使用移动数据或使用账号密码登录', icon: 'fail' })
},
})
} catch (e) {
setOneClickLoading(false)
Toast.show({ content: `获取token失败:${e.message}`, icon: 'fail' })
}
}
// 页面上增加按钮
<Button block className={styles.btnOneClick} loading={oneClickLoading} onClick={handleOneClickLogin}>
一键登录
</Button>
要点小结:
url/origin必须与阿里云控制台里配置的页面地址、请求来源完全一致(含协议、域名、末尾斜杠)。- H5 场景下必须使用移动数据才能稳定取号,WiFi 下会提示用户关闭 Wi-Fi 或改用账号密码登录。
- 授权页的「登录」按钮文案需包含「登录」等字样,符合运营商与阿里云规范。
六、参考文档
**