4271f333b6
- 2026-06-07-sys-app-management: 已批准,待实现 → 已实现 - 2026-06-07-multi-login-social-login: 已批准,待实现 → 已实现 - 2026-06-06-user-aggregate-query: 已批准 → 已实现 各 spec 对应的 plan 已结清/实施 commit 已落地,更新状态描述并补充实施情况指引。
854 lines
28 KiB
Markdown
854 lines
28 KiB
Markdown
# 多方式登录与第三方登录设计文档
|
||
|
||
> **日期**: 2026-06-07
|
||
> **状态**: 已实现(2026-06-07)
|
||
> **作者**: AI Assistant
|
||
> **实施情况**: 数据库变更、实体调整、UserSocial 增删、密码登录扩展、短信/微信/支付宝框架、OAuth2ServerConfig 注册、配置更新、编译验证共 12 任务全部完成,详见 `docs/superpowers/plans/2026-06-07-multi-login-social-login-plan.md` 与 git log `6fd82fb` 起各 commit。
|
||
|
||
---
|
||
|
||
## 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;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
**文档结束**
|