docs(spec): 添加多方式登录与第三方登录设计规格 (已批准)
This commit is contained in:
@@ -0,0 +1,852 @@
|
|||||||
|
# 多方式登录与第三方登录设计文档
|
||||||
|
|
||||||
|
> **日期**: 2026-06-07
|
||||||
|
> **状态**: 已批准,待实现
|
||||||
|
> **作者**: AI Assistant
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
### 1.1 现状
|
||||||
|
|
||||||
|
当前系统仅支持用户名密码登录(`grant_type=password`),且 `PasswordAuthenticationConverter` 只提取 `username` 参数,无法支持手机号、邮箱登录。微信、支付宝、短信登录的 `Converter` 和 `Provider` 均为空实现。
|
||||||
|
|
||||||
|
### 1.2 目标
|
||||||
|
|
||||||
|
1. **扩展密码登录**:支持用户名、手机号、邮箱三种账号类型登录
|
||||||
|
2. **实现短信登录**:框架结构先行,验证码逻辑后续填充
|
||||||
|
3. **实现微信登录**:支持微信授权码换取用户信息并自动创建账号
|
||||||
|
4. **实现支付宝登录**:支持支付宝授权码换取用户信息并自动创建账号
|
||||||
|
5. **第三方账号管理**:存储 openId/unionId,支持 unionId 优先查询
|
||||||
|
6. **手机号为主键**:系统以手机号作为用户唯一标识,第三方登录自动创建新用户
|
||||||
|
7. **字段迁移**:将 `email` 从 `uc_user_detail` 迁移到 `uc_user` 表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 核心设计原则
|
||||||
|
|
||||||
|
1. **独立授权模式**:每种登录方式使用独立的 `grant_type`,符合 OAuth2 扩展规范
|
||||||
|
2. **手机号唯一性**:手机号是系统用户的唯一标识,第三方登录时优先用手机号创建/查找用户
|
||||||
|
3. **自动创建用户**:第三方登录无手机号时,自动生成 `userNo` 作为用户名,后续用户可自行修改
|
||||||
|
4. **unionId 优先**:查询第三方用户信息时,优先使用 unionId,其次使用 openId
|
||||||
|
5. **向后兼容**:保留现有 `password` 模式的 `username` 参数,同时新增 `account` + `accountType` 参数
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 架构设计
|
||||||
|
|
||||||
|
### 3.1 整体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
前端调用
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
POST /oauth2/token
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ DelegatingAuthenticationConverter │
|
||||||
|
│ ┌─────────┐ ┌─────┐ ┌─────────┐ │
|
||||||
|
│ │ Password│ │ Sms │ │ Wechat │ │
|
||||||
|
│ │ Converter│ │Converter│ │Converter│ │
|
||||||
|
│ └─────────┘ └─────┘ └─────────┘ │
|
||||||
|
│ ┌─────────┐ │
|
||||||
|
│ │ Alipay │ │
|
||||||
|
│ │Converter│ │
|
||||||
|
│ └─────────┘ │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ AuthenticationProvider 链 │
|
||||||
|
│ ┌─────────┐ ┌─────┐ ┌─────────┐ │
|
||||||
|
│ │ Password│ │ Sms │ │ Wechat │ │
|
||||||
|
│ │ Provider│ │Provider│ │Provider│ │
|
||||||
|
│ └─────────┘ └─────┘ └─────────┘ │
|
||||||
|
│ ┌─────────┐ │
|
||||||
|
│ │ Alipay │ │
|
||||||
|
│ │Provider │ │
|
||||||
|
│ └─────────┘ │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ 用户查找 / 创建 / 绑定 │
|
||||||
|
│ • 根据手机号/用户名/邮箱查找用户 │
|
||||||
|
│ • 第三方登录:调平台API获取用户信息 │
|
||||||
|
│ • 自动创建新用户(手机号或userNo) │
|
||||||
|
│ • 记录第三方绑定关系 │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ 生成 OAuth2 Token │
|
||||||
|
│ Access Token + Refresh Token │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 登录方式对照表
|
||||||
|
|
||||||
|
| 登录方式 | grant_type | 必填参数 | 可选参数 | 说明 |
|
||||||
|
|---------|-----------|---------|---------|------|
|
||||||
|
| 用户名密码 | `password` | `username`, `password` | - | 兼容现有方式 |
|
||||||
|
| 手机号密码 | `password` | `account`, `accountType=PHONE`, `password` | - | 扩展方式 |
|
||||||
|
| 邮箱密码 | `password` | `account`, `accountType=EMAIL`, `password` | - | 扩展方式 |
|
||||||
|
| 短信验证码 | `sms` | `phone`, `code` | - | 框架先行 |
|
||||||
|
| 微信登录 | `wechat` | `code` | `phone` | 授权码模式 |
|
||||||
|
| 支付宝登录 | `alipay` | `code` | `phone` | 授权码模式 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 数据库设计
|
||||||
|
|
||||||
|
### 4.1 新增表:`rui_uc_user_social`
|
||||||
|
|
||||||
|
存储用户与第三方平台的绑定关系。
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE rui_uc_user_social (
|
||||||
|
id BIGINT NOT NULL,
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||||
|
provider VARCHAR(20) NOT NULL COMMENT '平台 wechat/alipay',
|
||||||
|
union_id VARCHAR(100) DEFAULT NULL COMMENT 'unionId(微信开放平台)',
|
||||||
|
open_id VARCHAR(100) NOT NULL COMMENT 'openId',
|
||||||
|
extra JSON DEFAULT NULL COMMENT '扩展信息(昵称、头像等)',
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_user_provider (user_id, provider),
|
||||||
|
UNIQUE KEY uk_provider_openid (provider, open_id),
|
||||||
|
INDEX idx_union_id (union_id),
|
||||||
|
INDEX idx_user_id (user_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户第三方账号关联';
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**:
|
||||||
|
- `provider`: 平台标识,`wechat` 或 `alipay`
|
||||||
|
- `union_id`: 微信开放平台统一标识,同一主体下的不同应用 unionId 相同
|
||||||
|
- `open_id`: 各应用内的唯一标识
|
||||||
|
- `extra`: JSON 格式,存储第三方平台的额外信息(昵称、头像、性别等)
|
||||||
|
|
||||||
|
**索引设计**:
|
||||||
|
- `uk_user_provider`: 一个用户在同一平台只能绑定一个账号
|
||||||
|
- `uk_provider_openid`: 同一平台的 openId 唯一
|
||||||
|
- `idx_union_id`: 支持 unionId 查询
|
||||||
|
|
||||||
|
### 4.2 修改表:`rui_uc_user`
|
||||||
|
|
||||||
|
新增 `email` 字段:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 在 rui_uc_user 表中添加 email 字段
|
||||||
|
ALTER TABLE rui_uc_user ADD COLUMN email VARCHAR(100) DEFAULT NULL COMMENT '邮箱' AFTER phone;
|
||||||
|
ALTER TABLE rui_uc_user ADD UNIQUE KEY uk_email (tenant_id, email);
|
||||||
|
```
|
||||||
|
|
||||||
|
修改后的表结构:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE rui_uc_user (
|
||||||
|
id BIGINT NOT NULL,
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID 0:系统级',
|
||||||
|
username VARCHAR(100) NOT NULL COMMENT '用户名',
|
||||||
|
phone VARCHAR(20) DEFAULT NULL COMMENT '手机号',
|
||||||
|
email VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
|
||||||
|
user_no VARCHAR(50) DEFAULT NULL COMMENT '用户编号(短编码,前端展示用)',
|
||||||
|
password VARCHAR(255) NOT NULL COMMENT '密码(BCrypt加密)',
|
||||||
|
user_type TINYINT NOT NULL DEFAULT 1 COMMENT '用户类型 1:普通用户 2:管理员 3:系统用户',
|
||||||
|
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0:禁用 1:启用',
|
||||||
|
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除 0:正常 1:已删',
|
||||||
|
created_by BIGINT DEFAULT NULL COMMENT '创建者ID',
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_by BIGINT DEFAULT NULL COMMENT '更新者ID',
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_username (tenant_id, username),
|
||||||
|
UNIQUE KEY uk_phone (tenant_id, phone),
|
||||||
|
UNIQUE KEY uk_email (tenant_id, email),
|
||||||
|
INDEX idx_tenant (tenant_id),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 修改表:`rui_uc_user_detail`
|
||||||
|
|
||||||
|
删除 `email` 字段:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 从 rui_uc_user_detail 表中删除 email 字段
|
||||||
|
ALTER TABLE rui_uc_user_detail DROP COLUMN email;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 登录日志扩展
|
||||||
|
|
||||||
|
`rui_sys_login_log` 表的 `login_type` 字段已有定义:
|
||||||
|
- `1`: 密码登录
|
||||||
|
- `2`: 短信登录
|
||||||
|
- `3`: 微信登录
|
||||||
|
- `4`: 支付宝登录
|
||||||
|
|
||||||
|
**无需修改**,但需要在代码中确保所有登录方式都正确记录类型。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 核心流程设计
|
||||||
|
|
||||||
|
### 5.1 密码登录流程(扩展)
|
||||||
|
|
||||||
|
```
|
||||||
|
前端请求
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
POST /oauth2/token
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
Authorization: Basic {client_credentials}
|
||||||
|
|
||||||
|
# 方式1:用户名密码(兼容现有)
|
||||||
|
grant_type=password
|
||||||
|
&username=admin
|
||||||
|
&password=123456
|
||||||
|
|
||||||
|
# 方式2:手机号密码(新增)
|
||||||
|
grant_type=password
|
||||||
|
&account=13800138000
|
||||||
|
&accountType=PHONE
|
||||||
|
&password=123456
|
||||||
|
|
||||||
|
# 方式3:邮箱密码(新增)
|
||||||
|
grant_type=password
|
||||||
|
&account=user@example.com
|
||||||
|
&accountType=EMAIL
|
||||||
|
&password=123456
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
PasswordAuthenticationConverter
|
||||||
|
├─ 提取 grant_type=password
|
||||||
|
├─ 如果有 username → 走兼容模式
|
||||||
|
└─ 如果有 account + accountType → 走扩展模式
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
PasswordAuthenticationProvider
|
||||||
|
├─ 校验客户端支持 password 授权
|
||||||
|
├─ 构建 UsernamePasswordAuthenticationToken
|
||||||
|
│ ├─ 兼容模式: username 作为 principal
|
||||||
|
│ └─ 扩展模式: account 作为 principal
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
AuthenticationManager
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
DaoAuthenticationProvider
|
||||||
|
├─ 调用 RemoteUserDetailsService.loadUserByUsername(username)
|
||||||
|
│ 或 RemoteUserDetailsService.loadUserByAccount(account, accountType)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
RemoteUserDetailsService
|
||||||
|
├─ USERNAME → userAuthFeign.loadUser(account)
|
||||||
|
├─ PHONE → userAuthFeign.loadUser({account, PHONE})
|
||||||
|
└─ EMAIL → userAuthFeign.loadUser({account, EMAIL})
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
UserInnerController.loadUser(LoginAccountDTO)
|
||||||
|
├─ 根据 accountType 查询用户
|
||||||
|
├─ PHONE → lambdaQuery().eq(User::getPhone, account)
|
||||||
|
├─ EMAIL → lambdaQuery().eq(User::getEmail, account)
|
||||||
|
└─ USERNAME → lambdaQuery().eq(User::getUsername, account)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
返回 UserDetails → 生成 Token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 短信登录流程
|
||||||
|
|
||||||
|
```
|
||||||
|
前端请求
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
POST /oauth2/token
|
||||||
|
grant_type=sms
|
||||||
|
&phone=13800138000
|
||||||
|
&code=123456
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
SmsAuthenticationConverter
|
||||||
|
├─ 校验 grant_type=sms
|
||||||
|
├─ 校验 phone 必填
|
||||||
|
└─ 校验 code 必填
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
SmsAuthenticationProvider
|
||||||
|
├─ 校验客户端支持 sms 授权
|
||||||
|
├─ 从 Redis 获取验证码(key: sms:code:{phone})
|
||||||
|
├─ 比对验证码
|
||||||
|
├─ 验证码错误 → 抛出异常
|
||||||
|
└─ 验证码正确 → 继续
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
根据 phone 查询用户
|
||||||
|
├─ 找到 → 生成 Token
|
||||||
|
└─ 未找到 → 创建新用户
|
||||||
|
├─ username = phone
|
||||||
|
├─ phone = phone
|
||||||
|
├─ password = 随机生成(BCrypt加密)
|
||||||
|
└─ user_no = 自动生成
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
生成 OAuth2 Token
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:短信验证码发送接口(`POST /sms/send`)本次不实现,只预留框架结构。Redis 中的验证码需要前端开发时手动设置或通过其他方式注入。
|
||||||
|
|
||||||
|
### 5.3 微信登录流程
|
||||||
|
|
||||||
|
```
|
||||||
|
前端请求
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
POST /oauth2/token
|
||||||
|
grant_type=wechat
|
||||||
|
&code=wx_auth_code
|
||||||
|
&phone=13800138000 ← 可选
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
WechatAuthenticationConverter
|
||||||
|
├─ 校验 grant_type=wechat
|
||||||
|
├─ 校验 code 必填
|
||||||
|
└─ 提取 phone(可选)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
WechatAuthenticationProvider
|
||||||
|
├─ 校验客户端支持 wechat 授权
|
||||||
|
├─ 调用微信 API 换取 access_token
|
||||||
|
│ GET https://api.weixin.qq.com/sns/oauth2/access_token
|
||||||
|
│ ?appid={appid}&secret={secret}&code={code}&grant_type=authorization_code
|
||||||
|
│
|
||||||
|
├─ 获取 openId, unionId, access_token
|
||||||
|
│
|
||||||
|
├─ 根据 unionId 查询 rui_uc_user_social
|
||||||
|
│ ├─ 找到 → 获取 user_id → 查询用户 → 生成 Token
|
||||||
|
│ └─ 未找到 → 根据 openId 查询
|
||||||
|
│ ├─ 找到 → 获取 user_id → 查询用户 → 生成 Token
|
||||||
|
│ └─ 未找到 → 创建新用户
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
创建新用户流程
|
||||||
|
├─ 有 phone 参数
|
||||||
|
│ ├─ 查询 phone 是否已存在
|
||||||
|
│ ├─ 存在 → 使用该用户,记录绑定关系
|
||||||
|
│ └─ 不存在 → 创建新用户
|
||||||
|
│ ├─ username = phone
|
||||||
|
│ ├─ phone = phone
|
||||||
|
│ └─ password = 随机生成
|
||||||
|
│
|
||||||
|
└─ 无 phone 参数
|
||||||
|
├─ username = 随机生成(如 WX_ + 时间戳)
|
||||||
|
├─ phone = null
|
||||||
|
└─ password = 随机生成
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
记录绑定关系
|
||||||
|
INSERT INTO rui_uc_user_social
|
||||||
|
(user_id, provider, union_id, open_id, extra)
|
||||||
|
VALUES (?, 'wechat', ?, ?, ?)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
生成 OAuth2 Token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 支付宝登录流程
|
||||||
|
|
||||||
|
与微信登录类似,区别:
|
||||||
|
1. 调用支付宝 API:`alipay.system.oauth.token` 换取 access_token
|
||||||
|
2. 调用 `alipay.user.info.share` 获取用户信息
|
||||||
|
3. 支付宝没有 unionId,使用 userId 作为唯一标识
|
||||||
|
4. 存储到 `rui_uc_user_social` 时,`union_id` 为 null
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 代码结构
|
||||||
|
|
||||||
|
### 6.1 新增/修改文件清单
|
||||||
|
|
||||||
|
#### rui-common-oauth2 模块
|
||||||
|
|
||||||
|
```
|
||||||
|
rui-common-oauth2/src/main/java/com/rui/common/oauth2/
|
||||||
|
├── authentication/
|
||||||
|
│ ├── BaseAuthenticationConverter.java # 已有,无需修改
|
||||||
|
│ ├── BaseAuthenticationProvider.java # 已有,无需修改
|
||||||
|
│ ├── password/
|
||||||
|
│ │ ├── PasswordAuthenticationConverter.java # 修改:支持 accountType
|
||||||
|
│ │ └── PasswordAuthenticationProvider.java # 已有,无需修改
|
||||||
|
│ ├── sms/
|
||||||
|
│ │ ├── SmsAuthenticationConverter.java # 重写:实现短信参数提取
|
||||||
|
│ │ ├── SmsAuthenticationProvider.java # 重写:实现短信认证逻辑
|
||||||
|
│ │ └── SmsAuthenticationToken.java # 新增:短信认证令牌
|
||||||
|
│ ├── weixin/
|
||||||
|
│ │ ├── WeixinAuthenticationConverter.java # 重写:实现微信参数提取
|
||||||
|
│ │ ├── WeixinAuthenticationProvider.java # 重写:实现微信认证逻辑
|
||||||
|
│ │ └── WeixinAuthenticationToken.java # 新增:微信认证令牌
|
||||||
|
│ └── alipay/
|
||||||
|
│ ├── AlipayAuthenticationConverter.java # 重写:实现支付宝参数提取
|
||||||
|
│ ├── AlipayAuthenticationProvider.java # 重写:实现支付宝认证逻辑
|
||||||
|
│ └── AlipayAuthenticationToken.java # 新增:支付宝认证令牌
|
||||||
|
├── config/
|
||||||
|
│ └── OAuth2ServerConfig.java # 修改:注册新的 Converter 和 Provider
|
||||||
|
└── service/
|
||||||
|
└── RemoteUserDetailsService.java # 修改:支持 EMAIL 类型
|
||||||
|
```
|
||||||
|
|
||||||
|
#### rui-service-user 模块
|
||||||
|
|
||||||
|
```
|
||||||
|
rui-service-user/src/main/java/com/rui/service/user/
|
||||||
|
├── entity/
|
||||||
|
│ ├── User.java # 修改:新增 email 字段
|
||||||
|
│ ├── UserDetail.java # 修改:删除 email 字段
|
||||||
|
│ └── UserSocial.java # 新增:第三方账号关联实体
|
||||||
|
├── mapper/
|
||||||
|
│ └── UserSocialMapper.java # 新增
|
||||||
|
├── service/
|
||||||
|
│ ├── IUserSocialService.java # 新增
|
||||||
|
│ └── impl/
|
||||||
|
│ └── UserSocialServiceImpl.java # 新增
|
||||||
|
├── controller/
|
||||||
|
│ └── inner/
|
||||||
|
│ └── UserInnerController.java # 修改:支持 EMAIL 查询
|
||||||
|
└── dto/
|
||||||
|
└── LoginAccountDTO.java # 已有,无需修改
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 关键类设计
|
||||||
|
|
||||||
|
#### 6.2.1 PasswordAuthenticationConverter(修改)
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class PasswordAuthenticationConverter extends BaseAuthenticationConverter<PasswordAuthenticationToken> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkParams(HttpServletRequest request) {
|
||||||
|
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||||
|
|
||||||
|
// 兼容模式:使用 username
|
||||||
|
String username = parameters.getFirst("username");
|
||||||
|
if (StringUtils.hasText(username)) {
|
||||||
|
// 校验 password
|
||||||
|
String password = parameters.getFirst("password");
|
||||||
|
if (!StringUtils.hasText(password)) {
|
||||||
|
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "password", ...);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扩展模式:使用 account + accountType
|
||||||
|
String account = parameters.getFirst("account");
|
||||||
|
if (!StringUtils.hasText(account)) {
|
||||||
|
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "account", ...);
|
||||||
|
}
|
||||||
|
|
||||||
|
String accountType = parameters.getFirst("accountType");
|
||||||
|
if (!StringUtils.hasText(accountType)) {
|
||||||
|
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "accountType", ...);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验 password
|
||||||
|
String password = parameters.getFirst("password");
|
||||||
|
if (!StringUtils.hasText(password)) {
|
||||||
|
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "password", ...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PasswordAuthenticationToken buildToken(...) {
|
||||||
|
// 将 accountType 放入 additionalParameters
|
||||||
|
// 供 Provider 使用
|
||||||
|
return new PasswordAuthenticationToken(...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.2.2 WechatAuthenticationProvider(重写)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Slf4j
|
||||||
|
public class WechatAuthenticationProvider extends BaseAuthenticationProvider<WechatAuthenticationToken> {
|
||||||
|
|
||||||
|
private final WechatApiClient wechatApiClient;
|
||||||
|
private final UserSocialService userSocialService;
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
|
||||||
|
String code = (String) reqParameters.get("code");
|
||||||
|
String phone = (String) reqParameters.get("phone");
|
||||||
|
|
||||||
|
// 调用微信 API 获取 openId, unionId
|
||||||
|
WechatTokenResponse wxResponse = wechatApiClient.getAccessToken(code);
|
||||||
|
String openId = wxResponse.getOpenid();
|
||||||
|
String unionId = wxResponse.getUnionid();
|
||||||
|
|
||||||
|
// 查找或创建用户
|
||||||
|
User user = findOrCreateUser(openId, unionId, phone);
|
||||||
|
|
||||||
|
// 构建认证令牌
|
||||||
|
return new UsernamePasswordAuthenticationToken(user.getUsername(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private User findOrCreateUser(String openId, String unionId, String phone) {
|
||||||
|
// 1. 根据 unionId 查找
|
||||||
|
if (StringUtils.hasText(unionId)) {
|
||||||
|
UserSocial social = userSocialService.findByUnionId(unionId);
|
||||||
|
if (social != null) {
|
||||||
|
return userService.getById(social.getUserId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 根据 openId 查找
|
||||||
|
UserSocial social = userSocialService.findByOpenId("wechat", openId);
|
||||||
|
if (social != null) {
|
||||||
|
return userService.getById(social.getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 创建新用户
|
||||||
|
User user = new User();
|
||||||
|
if (StringUtils.hasText(phone)) {
|
||||||
|
// 检查手机号是否已存在
|
||||||
|
User existUser = userService.findByPhone(phone);
|
||||||
|
if (existUser != null) {
|
||||||
|
user = existUser;
|
||||||
|
} else {
|
||||||
|
user.setUsername(phone);
|
||||||
|
user.setPhone(phone);
|
||||||
|
user.setPassword(generateRandomPassword());
|
||||||
|
userService.save(user);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 无手机号,生成随机用户名
|
||||||
|
user.setUsername(generateRandomUsername());
|
||||||
|
user.setPassword(generateRandomPassword());
|
||||||
|
userService.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 记录绑定关系
|
||||||
|
UserSocial newSocial = new UserSocial();
|
||||||
|
newSocial.setUserId(user.getId());
|
||||||
|
newSocial.setProvider("wechat");
|
||||||
|
newSocial.setUnionId(unionId);
|
||||||
|
newSocial.setOpenId(openId);
|
||||||
|
userSocialService.save(newSocial);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.2.3 UserSocial 实体
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Data
|
||||||
|
@TableName(value = "uc_user_social", keepGlobalPrefix = true)
|
||||||
|
public class UserSocial extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "平台 wechat/alipay")
|
||||||
|
private String provider;
|
||||||
|
|
||||||
|
@Schema(description = "unionId")
|
||||||
|
private String unionId;
|
||||||
|
|
||||||
|
@Schema(description = "openId")
|
||||||
|
private String openId;
|
||||||
|
|
||||||
|
@Schema(description = "扩展信息")
|
||||||
|
private String extra;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. API 接口设计
|
||||||
|
|
||||||
|
### 7.1 密码登录
|
||||||
|
|
||||||
|
```http
|
||||||
|
### 用户名密码登录(兼容现有)
|
||||||
|
POST /oauth2/token
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
Authorization: Basic c3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Q6
|
||||||
|
|
||||||
|
grant_type=password
|
||||||
|
&username=admin
|
||||||
|
&password=123456
|
||||||
|
&scope=server
|
||||||
|
|
||||||
|
### 手机号密码登录(新增)
|
||||||
|
POST /oauth2/token
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
Authorization: Basic c3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Q6
|
||||||
|
|
||||||
|
grant_type=password
|
||||||
|
&account=13800138000
|
||||||
|
&accountType=PHONE
|
||||||
|
&password=123456
|
||||||
|
&scope=server
|
||||||
|
|
||||||
|
### 邮箱密码登录(新增)
|
||||||
|
POST /oauth2/token
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
Authorization: Basic c3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Q6
|
||||||
|
|
||||||
|
grant_type=password
|
||||||
|
&account=user@example.com
|
||||||
|
&accountType=EMAIL
|
||||||
|
&password=123456
|
||||||
|
&scope=server
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 短信登录
|
||||||
|
|
||||||
|
```http
|
||||||
|
### 短信验证码登录
|
||||||
|
POST /oauth2/token
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
Authorization: Basic c3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Q6
|
||||||
|
|
||||||
|
grant_type=sms
|
||||||
|
&phone=13800138000
|
||||||
|
&code=123456
|
||||||
|
&scope=server
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 微信登录
|
||||||
|
|
||||||
|
```http
|
||||||
|
### 微信登录
|
||||||
|
POST /oauth2/token
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
Authorization: Basic c3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Q6
|
||||||
|
|
||||||
|
grant_type=wechat
|
||||||
|
&code=wx_auth_code_xxx
|
||||||
|
&phone=13800138000
|
||||||
|
&scope=server
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.4 支付宝登录
|
||||||
|
|
||||||
|
```http
|
||||||
|
### 支付宝登录
|
||||||
|
POST /oauth2/token
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
Authorization: Basic c3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Q6
|
||||||
|
|
||||||
|
grant_type=alipay
|
||||||
|
&code=alipay_auth_code_xxx
|
||||||
|
&phone=13800138000
|
||||||
|
&scope=server
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.5 响应格式
|
||||||
|
|
||||||
|
所有登录方式返回统一的 OAuth2 Token 响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"access_token": "abc123...",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 7200,
|
||||||
|
"refresh_token": "def456...",
|
||||||
|
"scope": "server"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 配置设计
|
||||||
|
|
||||||
|
### 8.1 微信配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Nacos 配置:rui-service-auth.yaml 或 rui-common.yaml
|
||||||
|
social:
|
||||||
|
wechat:
|
||||||
|
app-id: wx1234567890abcdef
|
||||||
|
app-secret: your-app-secret
|
||||||
|
# 可选:token 刷新地址
|
||||||
|
token-url: https://api.weixin.qq.com/sns/oauth2/access_token
|
||||||
|
# 可选:用户信息地址
|
||||||
|
user-info-url: https://api.weixin.qq.com/sns/userinfo
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 支付宝配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
social:
|
||||||
|
alipay:
|
||||||
|
app-id: 2024XXXXXXXXXXXX
|
||||||
|
private-key: your-private-key
|
||||||
|
public-key: alipay-public-key
|
||||||
|
# 可选:网关地址
|
||||||
|
gateway-url: https://openapi.alipay.com/gateway.do
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 客户端授权类型配置
|
||||||
|
|
||||||
|
修改 `sys_oauth_client` 表,为客户端添加新的授权类型:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 更新默认客户端,支持所有登录方式
|
||||||
|
UPDATE sys_oauth_client
|
||||||
|
SET grant_types = 'password,refresh_token,client_credentials,sms,wechat,alipay'
|
||||||
|
WHERE client_id = 'rui-client';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 安全设计
|
||||||
|
|
||||||
|
### 9.1 验证码安全
|
||||||
|
|
||||||
|
- 短信验证码有效期:5 分钟
|
||||||
|
- 验证码错误次数限制:5 次/小时
|
||||||
|
- 验证码存储:Redis,key = `sms:code:{phone}`
|
||||||
|
|
||||||
|
### 9.2 第三方登录安全
|
||||||
|
|
||||||
|
- 微信/支付宝授权码只能使用一次
|
||||||
|
- 授权码有效期:5 分钟(由微信/支付宝平台控制)
|
||||||
|
- 后端必须校验授权码的真实性(调平台 API)
|
||||||
|
|
||||||
|
### 9.3 密码安全
|
||||||
|
|
||||||
|
- 第三方登录自动创建的用户,生成随机密码(32 位随机字符串)
|
||||||
|
- 用户首次设置密码时,要求提供原密码或通过手机验证码验证
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 错误码设计
|
||||||
|
|
||||||
|
| 错误码 | 描述 | 场景 |
|
||||||
|
|-------|------|------|
|
||||||
|
| `invalid_request` | 请求参数错误 | 缺少必填参数、参数格式错误 |
|
||||||
|
| `invalid_grant` | 授权失败 | 验证码错误、授权码无效 |
|
||||||
|
| `invalid_client` | 客户端认证失败 | 客户端不存在、授权类型不支持 |
|
||||||
|
| `unauthorized_client` | 客户端未授权 | 客户端不支持该授权类型 |
|
||||||
|
| `server_error` | 服务器内部错误 | 调用第三方 API 失败 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 测试策略
|
||||||
|
|
||||||
|
### 11.1 单元测试
|
||||||
|
|
||||||
|
- `PasswordAuthenticationConverterTest`: 测试参数提取和校验
|
||||||
|
- `SmsAuthenticationProviderTest`: 测试验证码校验逻辑
|
||||||
|
- `WechatAuthenticationProviderTest`: Mock 微信 API,测试用户创建流程
|
||||||
|
|
||||||
|
### 11.2 集成测试
|
||||||
|
|
||||||
|
- 使用 H2 内存数据库测试完整登录流程
|
||||||
|
- 使用 WireMock 模拟微信/支付宝 API
|
||||||
|
|
||||||
|
### 11.3 手动测试清单
|
||||||
|
|
||||||
|
- [ ] 用户名密码登录(兼容测试)
|
||||||
|
- [ ] 手机号密码登录
|
||||||
|
- [ ] 邮箱密码登录
|
||||||
|
- [ ] 短信验证码登录(使用 Redis 手动设置验证码)
|
||||||
|
- [ ] 微信登录(使用测试授权码)
|
||||||
|
- [ ] 支付宝登录(使用测试授权码)
|
||||||
|
- [ ] 第三方登录后绑定手机号
|
||||||
|
- [ ] 同一微信不同手机号创建不同用户
|
||||||
|
- [ ] unionId 优先查询验证
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 风险与回滚
|
||||||
|
|
||||||
|
### 12.1 风险
|
||||||
|
|
||||||
|
| 风险 | 影响 | 缓解措施 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 微信/支付宝 API 变更 | 登录失败 | 封装 API 调用,便于快速适配 |
|
||||||
|
| 手机号重复 | 数据不一致 | 数据库唯一索引 + 代码校验 |
|
||||||
|
| 性能问题 | 登录慢 | Redis 缓存 + 异步记录日志 |
|
||||||
|
|
||||||
|
### 12.2 回滚方案
|
||||||
|
|
||||||
|
- 数据库变更:保留原字段,新增字段不影响现有数据
|
||||||
|
- 代码回滚:新授权模式独立实现,不影响现有 `password` 模式
|
||||||
|
- 配置回滚:移除新 grant_type 即可禁用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 后续优化
|
||||||
|
|
||||||
|
1. **短信服务商接入**:实现真实的短信发送功能
|
||||||
|
2. **社交账号解绑**:提供 API 解除第三方绑定
|
||||||
|
3. **多账号合并**:支持将多个第三方账号合并到同一用户
|
||||||
|
4. **登录设备管理**:记录登录设备,支持远程登出
|
||||||
|
5. **扫码登录**:支持微信扫码登录 PC 端
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. 附录
|
||||||
|
|
||||||
|
### 14.1 登录类型枚举
|
||||||
|
|
||||||
|
```java
|
||||||
|
public enum LoginType {
|
||||||
|
PASSWORD(1, "密码登录"),
|
||||||
|
SMS(2, "短信登录"),
|
||||||
|
WECHAT(3, "微信登录"),
|
||||||
|
ALIPAY(4, "支付宝登录");
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
LoginType(int code, String description) {
|
||||||
|
this.code = code;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 14.2 账号类型枚举
|
||||||
|
|
||||||
|
```java
|
||||||
|
public enum AccountType {
|
||||||
|
USERNAME("用户名"),
|
||||||
|
PHONE("手机号"),
|
||||||
|
EMAIL("邮箱");
|
||||||
|
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
AccountType(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 14.3 第三方平台枚举
|
||||||
|
|
||||||
|
```java
|
||||||
|
public enum SocialProvider {
|
||||||
|
WECHAT("微信"),
|
||||||
|
ALIPAY("支付宝");
|
||||||
|
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
SocialProvider(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档结束**
|
||||||
Reference in New Issue
Block a user