Files
rui-docs/superpowers/specs/2026-06-07-multi-login-social-login-design.md
T
vifo 4271f333b6 docs(specs): 更新已完成 spec 文档状态
- 2026-06-07-sys-app-management: 已批准,待实现 → 已实现
- 2026-06-07-multi-login-social-login: 已批准,待实现 → 已实现
- 2026-06-06-user-aggregate-query: 已批准 → 已实现

各 spec 对应的 plan 已结清/实施 commit 已落地,更新状态描述并补充实施情况指引。
2026-06-07 18:46:30 +08:00

28 KiB
Raw Blame History

多方式登录与第三方登录设计文档

日期: 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 参数,无法支持手机号、邮箱登录。微信、支付宝、短信登录的 ConverterProvider 均为空实现。

1.2 目标

  1. 扩展密码登录:支持用户名、手机号、邮箱三种账号类型登录
  2. 实现短信登录:框架结构先行,验证码逻辑后续填充
  3. 实现微信登录:支持微信授权码换取用户信息并自动创建账号
  4. 实现支付宝登录:支持支付宝授权码换取用户信息并自动创建账号
  5. 第三方账号管理:存储 openId/unionId,支持 unionId 优先查询
  6. 手机号为主键:系统以手机号作为用户唯一标识,第三方登录自动创建新用户
  7. 字段迁移:将 emailuc_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

存储用户与第三方平台的绑定关系。

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: 平台标识,wechatalipay
  • union_id: 微信开放平台统一标识,同一主体下的不同应用 unionId 相同
  • open_id: 各应用内的唯一标识
  • extra: JSON 格式,存储第三方平台的额外信息(昵称、头像、性别等)

索引设计

  • uk_user_provider: 一个用户在同一平台只能绑定一个账号
  • uk_provider_openid: 同一平台的 openId 唯一
  • idx_union_id: 支持 unionId 查询

4.2 修改表:rui_uc_user

新增 email 字段:

-- 在 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);

修改后的表结构:

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 字段:

-- 从 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. 调用支付宝 APIalipay.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(修改)

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(重写)

@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 实体

@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 密码登录

### 用户名密码登录(兼容现有)
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 短信登录

### 短信验证码登录
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic c3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Qtc3ByaW5nLWJvb3Q6

grant_type=sms
&phone=13800138000
&code=123456
&scope=server

7.3 微信登录

### 微信登录
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 支付宝登录

### 支付宝登录
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 响应:

{
    "access_token": "abc123...",
    "token_type": "Bearer",
    "expires_in": 7200,
    "refresh_token": "def456...",
    "scope": "server"
}

8. 配置设计

8.1 微信配置

# 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 支付宝配置

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 表,为客户端添加新的授权类型:

-- 更新默认客户端,支持所有登录方式
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 次/小时
  • 验证码存储:Rediskey = 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 登录类型枚举

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 账号类型枚举

public enum AccountType {
    USERNAME("用户名"),
    PHONE("手机号"),
    EMAIL("邮箱");
    
    private final String description;
    
    AccountType(String description) {
        this.description = description;
    }
}

14.3 第三方平台枚举

public enum SocialProvider {
    WECHAT("微信"),
    ALIPAY("支付宝");
    
    private final String description;
    
    SocialProvider(String description) {
        this.description = description;
    }
}

文档结束