外观
手机号登录方案
1. 概述
为 CodeNote 系统增加手机号登录能力,综合利用阿里云号码认证服务(PNVS) 的三种能力:
| PNVS 能力 | 产品形态 | 用途 | 优先级 |
|---|---|---|---|
| 一键登录 | Android SDK + 后端 getMobile | 用户点击「一键登录」按钮后取号并弹运营商授权页 | 首选 |
| 本机号码校验 | Android SDK + 后端 verifyMobile | 用户中心绑定手机号,校验输入的号码是否为本机 | 首选 |
| 短信认证 | 后端 sendSmsVerifyCode / checkSmsVerifyCode | 一键登录失败时的降级方案 | 降级 |
1.1 各端支持方式
| 端 | 一键登录 | 验证码 | 密码登录 | 设备免注册 |
|---|---|---|---|---|
| Android | ✅ 首选(自动弹运营商授权页) | ✅ 降级 | ✅ 手机号/用户名+密码 | ✅ 设备身份免注册 |
| 管理后台 | ❌ | ❌ | ✅ 手机号+密码 | ❌ |
2. 系统流程
2.1 登录流程(Android)
登录页为左右滑动的 4 页布局,比例 1:3:1(品牌区/登录区/注册链接),所有登录表单共用 primaryContainer 背景色。
┌──────────────────────────────┐
│ CodeNote │ ← 1 份,品牌区垂直居中
│ 智能相机管理 ... │
├──────────────────────────────┤
│ │
│ ┌─ 左右滑动 4 页 ───────┐ │ ← 3 份,登录区
│ │ ① 一键登录 ② 验证码 │ │
│ │ ③ 免注册 ④ 账号密码 │ │ ← primaryContainer 背景
│ └────────────────────────┘ │
│ 一键登录 验证码 免注册 ... │ ← 底部标签,贴卡片底部
├──────────────────────────────┤
│ 还没有账号?立即注册 │ ← 1 份,注册链接垂直居中
└──────────────────────────────┘
各页面说明:
① 一键登录
→ 进入页面自动调用 getLoginToken() 弹出运营商授权页
→ 用户授权 → TokenResultListener 解析 JSON → code=600000 提取 token → 后端登录
→ 失败 → 卡片内显示错误 + 「重新获取」按钮 + 「使用验证码登录」链接
② 验证码登录
→ 手机号 + 验证码,纯验证码无密码切换
→ 验证码通过后端 sendSmsVerifyCode / checkSmsVerifyCode(PNVS 短信认证)
③ 免注册
→ 使用设备身份 deviceId 一键登录/注册
→ 按钮触发 deviceUserManager.initializeDeviceUser()
④ 账号密码
→ 支持手机号/用户名自动识别(11 位数字 → phonePasswordLogin,否则 → login)2.2 绑定手机号流程(Android)
用户中心 → 绑定手机号
│
├─ 本机号码校验 → SDK 取掩码号码(151****6600)
│ ├─ 用户确认 → 后端 verifyMobile(token) → 校验通过 → 绑定
│ └─ 失败 → 降级到短信验证码
│
└─ 验证码绑定 → 输入手机号 → 获取验证码 → 确认绑定3. PNVS 一键登录与本机号码校验
3.1 原理
一键登录和本机号码校验依赖 Android 端集成 PNVS 的原生客户端 SDK(非 API 调用):
一键登录流程:
┌──────────────┐ ① SDK取号 ┌──────────────┐
│ Android App │ ────────────────→ │ PNVS SDK │
│ │ ←──────────────── │ (预取token) │
│ │ 返回掩码号码 └──────────────┘
│ │ 151****6600
│ 用户点击确认 │
│ ↓ │
│ phone+token │ ② 后端校验 ┌──────────────┐
│ ────────────→│ ────────────────→ │ 后端 Server │
│ │ │ getMobile() │
│ │ ←─────────────── │ 返回完整手机号 │
│ │ 返回完整号码 └──────────────┘
│ ↓ │
│ 自动登录/注册│
└──────────────┘3.2 所需 SDK 集成
| 平台 | 集成方式 |
|---|---|
| Android | 3 个 AAR:auth_number_product(主模块)+ main(基础组件)+ logger(日志) |
| 后端 | SDK 已支持 getMobile / verifyMobile API(dypnsapi20170525) |
Android AAR 清单:
| AAR | 说明 |
|---|---|
auth_number_product-2.14.23-log-online-standard-cuum-release.aar | 号码认证主模块,含运营商 SDK(Unicom/CMIC) |
main-2.2.3-release.aar | 基础功能组件,含 com.nirvana.tools.core |
logger-2.2.2-release.aar | 日志组件 |
Android 配置要点:
AndroidManifest.xml必须声明 3 个 Activity:LoginAuthActivity、AuthWebVeiwActivity、PrivacyDialogActivitysetAuthSDKInfo()传入认证方案密钥(非方案 Code),从 PNVS 控制台获取- 使用
fileTree(dir: 'libs', include: ['*.aar'])引入所有 AAR
3.3 后端新增 API
一键登录
POST /api/auth/sms/oneclick-login
Content-Type: application/json
Request:
{
"sdkToken": "xxx", // Android PNVS SDK 预取到的 token
"deviceId": "xxx",
"deviceName": "Xiaomi 14"
}
Response 200:
{
"code": 200,
"data": {
"token": "***",
"refreshToken": "***",
"expiresIn": 604800,
"userId": 42,
"username": "user_13800138000",
"phone": "13800138000",
"isNewUser": true
}
}后端逻辑:
1. pnvsClient.getMobile(sdkToken) → 从 PNVS 获取真实手机号
2. userRepository.findByPhone(phone)
├─ 存在 → 登录
└─ 不存在 → 自动注册(username=null,自动生成昵称,写 PHONE identity)
3. 创建 UserSession,记录 login_record (oneclick_login / oneclick_register)本机号码校验(绑定用,改造为一键授权取号)
POST /api/auth/sms/verify-mobile
Authorization: Bearer <token>
Content-Type: application/json
Request:
{
"sdkAccessToken": "xxx" // Android PNVS SDK 一键授权登录 token
}
// 说明:不再接收 phone。服务端用 getMobile(token) 取号后自动绑定当前账号,
// 前端「使用本机号码绑定」点击后弹运营商授权页,无需手动输入手机号。
Response 200:
{
"code": 200,
"message": "手机号校验通过",
"data": null
}后端逻辑:
1. pnvsClient.verifyMobile(phone, sdkAccessToken) → PNVS 确认手机号匹配
2. 校验通过 → user.phone = phone
3. 记录 login_record (verify_mobile_bind)4. 数据库变更
4.1 手机身份存储(身份分离后)
手机号存 user_identities 表(provider='PHONE',identifier=手机号,verified=true),不再存 users 表:
sql
-- user_identities 表
provider VARCHAR(32) NOT NULL COMMENT 'PHONE',
identifier VARCHAR(255) NOT NULL COMMENT '手机号',
verified BOOLEAN NOT NULL DEFAULT FALSE,
UNIQUE KEY uk_provider_identifier (provider, identifier)4.2 sms_codes 表(Dev 模式 + SMS 降级记录)
sql
CREATE TABLE sms_codes (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
phone VARCHAR(20) NOT NULL COMMENT '手机号',
code VARCHAR(4) NOT NULL COMMENT '4位验证码(Dev模式有效)',
type VARCHAR(20) NOT NULL COMMENT '验证码类型: LOGIN / BIND',
status VARCHAR(20) NOT NULL DEFAULT 'UNUSED' COMMENT 'UNUSED / USED / EXPIRED / SENT',
expired_at DATETIME NOT NULL COMMENT '过期时间',
biz_id VARCHAR(64) DEFAULT NULL COMMENT 'PNVS 发送流水号',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_phone_type (phone, type),
INDEX idx_expired_at (expired_at)
) COMMENT '短信验证码表';5. API 接口(完整清单)
5.1 一键登录(主入口)
| 端点 | 方法 | 说明 | 优先级 |
|---|---|---|---|
/api/auth/sms/oneclick-login | POST | Android SDK 一键登录(自动取号+登录/注册) | 首选 |
5.2 短信验证码登录(降级)
| 端点 | 方法 | 说明 | 优先级 |
|---|---|---|---|
/api/auth/sms/send-code | POST | 发送登录验证码 | 降级 |
/api/auth/phone-login | POST | 验证码登录(自动注册) | 降级 |
5.3 手机号+密码登录
| 端点 | 方法 | 说明 |
|---|---|---|
/api/auth/phone-password-login | POST | 手机号+密码登录 |
5.4 绑定手机号
| 端点 | 方法 | 说明 | 优先级 |
|---|---|---|---|
/api/auth/sms/verify-mobile | POST | 本机号码校验绑定(一键授权取号,不传 phone) | 首选 |
/api/auth/sms/bind-code | POST | 发送绑定验证码 | 降级 |
/api/auth/phone-bind | POST | 验证码绑定 | 降级 |
6. 服务层设计
6.1 PnvsClient 扩展
kotlin
@Component
@ConditionalOnProperty("aliyun.pnvs.access-key-id")
class PnvsClient(
private val pnvsProperties: PnvsProperties
) {
// ====== 短信认证(降级用) ======
fun sendSmsVerifyCode(phone: String, code: String): String { ... }
fun checkSmsVerifyCode(phone: String, code: String): Boolean { ... }
// ====== 一键登录(主入口) ======
/**
* 通过 PNVS SDK 预取 token 获取完整手机号
* @param sdkToken Android SDK 返回的认证 token
* @return 完整手机号
*/
fun getMobile(sdkToken: String): String {
val request = GetMobileRequest()
.setAccessToken(sdkToken)
val resp = client.getMobile(request)
if (resp.body?.code != "OK") {
throw BusinessException(message = "一键登录失败: ${resp.body?.message}")
}
return resp.body?.model?.mobile ?: throw BusinessException(message = "获取手机号失败")
}
// ====== 本机号码校验(绑定用) ======
/**
* 校验用户输入的手机号是否为本机号码
* @param phone 用户输入的手机号
* @param accessToken Android SDK 返回的校验 token
* @return true=校验通过
*/
fun verifyMobile(phone: String, accessToken: String): Boolean {
val request = VerifyMobileRequest()
.setPhoneNumber(phone)
.setAccessToken(accessToken)
val resp = client.verifyMobile(request)
val body = resp.body
return body?.code == "OK" && body.model?.verifyResult == "true"
}
}6.2 AuthService 新增方法
kotlin
@Transactional
fun oneClickLogin(request: OneClickLoginRequest): PhoneLoginResponse {
// 1. PNVS 获取手机号
val phone = pnvsClient.getMobile(request.sdkToken)
// 2. 查用户/自动注册
val user = userRepository.findByPhone(phone) ?: run {
val newUser = User(
username = "user_$phone",
phone = phone,
createdAt = System.currentTimeMillis(),
updatedAt = System.currentTimeMillis()
)
userRepository.save(newUser)
}
// 3. 创建 session,返回
return buildPhoneLoginResponse(user, isNewUser = user.username == null, ...)
}
@Transactional
fun verifyMobileBind(userId: Long, request: VerifyMobileRequest) {
val ok = pnvsClient.verifyMobile(request.phone, request.sdkAccessToken)
if (!ok) throw BusinessException(message = "手机号校验不通过,请确认使用本机号码")
val user = userRepository.findById(userId).orElseThrow { ... }
user.phone = request.phone
user.updatedAt = System.currentTimeMillis()
userRepository.save(user)
}7. Android 端集成要点
7.1 依赖
3 个 AAR 引入方式(core/data/build.gradle.kts + app/build.gradle.kts):
kotlin
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))7.2 登录页 UI
登录页为左右滑动的 4 页布局,比例 1:3:1。底部分页标签支持点击切换。
┌──────────────────────────────┐
│ CodeNote │
│ 智能相机管理 ... │
├──────────────────────────────┤
│ ┌────────────────────────┐ │
│ │ 📱 │ │
│ │ 一键登录 │ │ ← 进入自动弹运营商授权页
│ │ 使用本机号码快速登录 │ │
│ │ [ 一 键 登 录 ] │ │
│ └────────────────────────┘ │
│ 一键登录 验证码 免注册 ... │
├──────────────────────────────┤
│ 还没有账号?立即注册 │
└──────────────────────────────┘四个页面:
| 页 | 标题 | 说明 |
|---|---|---|
| 0 | 一键登录 | 进入自动调 getLoginToken() 弹运营商页,失败显示错误+重试 |
| 1 | 验证码登录 | 手机号+验证码输入,审核码通过 PNVS 短信 API 发送 |
| 2 | 免注册 | 设备身份 deviceId 一键登录,无需账号 |
| 3 | 账号密码 | 支持手机号/用户名自动识别 |
7.3 绑定手机号 UI(本机号码校验)
┌────────────────────────────┐
│ 绑定手机号 │
│ │
│ ┌────────────────────┐ │
│ │ 本机号码: 151****6600│ │ ← SDK 预取
│ │ [使用本机号码绑定] │ │ ← 首选
│ └────────────────────┘ │
│ │
│ ─── 或手动输入 ──────── │
│ 手机号: [____________] │
│ 验证码: [____] [获取] │ ← 降级
│ [确认绑定] │
└────────────────────────────┘8. Android 端降级策略
| 场景 | 行为 |
|---|---|
| getLoginToken 失败 | 一键卡片内显示错误按钮 + 使用验证码登录链接 |
| 无 SIM 卡 / 数据网络 | SDK 返回失败,卡片显示错误 |
| getMobile API 失败 | 后端异常,前端卡片显示错误 |
| verifyMobile 校验失败 | 提示用户请使用本机号码 |
9. 登录页 UI 设计
9.1 Android 登录页
左右滑动的 4 页布局,登录区统一 primaryContainer 背景色。底部标签可点击切换。
| 页面 | 说明 |
|---|---|
| 一键登录 | 📱 卡片,进入自动弹运营商授权页 |
| 验证码 | 手机号+验证码输入框 |
| 免注册 | 设备身份说明卡片 |
| 账号密码 | 手机号/用户名+密码输入框 |
9.2 管理后台登录页
仅支持手机号+密码,无一键登录和验证码登录。
入口:设置 → 手机号码
┌─ 首选:本机号码校验 ─────────┐
│ 本机号码: 151****6600 │ ← SDK 预取掩码
│ [ 使用本机号码绑定 ] │
│ │
│ ── 或手动输入 ── │
│ 手机号: [__________] │
│ 验证码: [____] [获取] │
│ [ 确认绑定 ] │
└──────────────────────────────┘11. 安全说明
| 防护层 | 方式 |
|---|---|
| 一键登录 | 运营商网络通道加密,SDK 预取 token 一次性有效 |
| 本机号码校验 | SDK token 绑定设备,防伪造 |
| 短信验证码 | 60s 发送间隔 + 日上限 + USED 标记防重放 |
| 定时清理 | SessionCleanupJob 清理过期记录 |
12. 文件变更清单(新增)
| 文件 | 变更 | 说明 |
|---|---|---|
PnvsClient.kt | 修改 | 增加 getMobile() / verifyMobile() |
PnvsService.kt | 新建 | 封装一键登录本机校验逻辑 |
AuthDto.kt | 修改 | 新增 OneClickLoginRequest / VerifyMobileRequest |
AuthService.kt | 修改 | 新增 oneClickLogin() / verifyMobileBind() |
AuthController.kt | 修改 | 新增 2 个端点 |
Android build.gradle.kts | 修改 | 增加 PNVS SDK 依赖 |
| Android SDK 集成 | 新建 | PnsManager.kt 封装 SDK 初始化+预取+回调 |
13. 实施步骤
| Phase | 内容 |
|---|---|
| 1 后端 API | PnvsClient 扩展 getMobile/verifyMobile + AuthService 新方法 + 2 个端点 |
| 2 Android SDK | 集成 3 个 AAR(auth + main + logger),实现 PnsManager 预取+一键登录+本机校验 |
| 3 Android UI | 登录页增加一键登录入口 + 绑定页增加本机号码校验入口 + 降级逻辑 |
| 4 全流程联调 | 一键登录登录→绑定→密码登录全路径测试 |
附录:账户设置补充说明
- 自动注册不生成 user_xxx 用户名:phoneLogin / oneClickLogin 自动注册 username=null,自动生成昵称,写 PHONE identity(verified=true)
- 绑定手机(phone-bind / verify-mobile):
(PHONE, phone)全局占用校验(排除自身);换绑 = 删旧插新;成功后 verified=true;verify-mobile 由前端传 phone 改为服务端 getMobile 取号 - 新增解绑手机:POST /api/auth/phone/unbind(登录态)→ 删除 PHONE identity;若仅剩 DEVICE 且无密码,响应提示绑定可找回方式
- 忘记密码支持手机凭证:forgot-code 11 位数字自动识别为手机;重置后 tokenVersion+1 全端登出
- 修改密码支持手机验证码:change-code channel=PHONE 发到当前用户已绑定手机(防轰炸)
- 凭证归一化:新增 CredentialNormalizer.normalizePhone(去空格、+86/0 前缀,仅 11 位国内号),发送/校验/查询统一调用
- 设置页「手机号码」入口移至账户设置 hub(R5)