# 多方式登录与第三方登录实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development (recommended) or superpowers-executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 实现密码(支持用户名/手机号/邮箱)、短信、微信、支付宝四种登录方式,支持第三方账号绑定和 unionId 优先查询。 **Architecture:** 基于 OAuth2 自定义授权模式,每种登录方式独立实现 Converter + Provider,统一走 OAuth2 Token 生成流程。手机号作为主键,第三方登录自动创建用户。 **Tech Stack:** Java 21, Spring Boot 4.x, Spring Security OAuth2, Fastjson2, MyBatis Plus, Java HttpClient --- ## 文件结构 ### 数据库变更 - `sql/init-database.sql` - 新增 `rui_uc_user_social` 表,修改 `rui_uc_user` 和 `rui_uc_user_detail` ### rui-common-oauth2 模块(认证中心) - `authentication/password/PasswordAuthenticationConverter.java` - 扩展支持 accountType - `authentication/sms/SmsAuthenticationConverter.java` - 短信参数提取 - `authentication/sms/SmsAuthenticationProvider.java` - 短信认证逻辑 - `authentication/sms/SmsAuthenticationToken.java` - 短信认证令牌 - `authentication/weixin/WeixinAuthenticationConverter.java` - 微信参数提取 - `authentication/weixin/WeixinAuthenticationProvider.java` - 微信认证逻辑 - `authentication/weixin/WeixinAuthenticationToken.java` - 微信认证令牌 - `authentication/alipay/AlipayAuthenticationConverter.java` - 支付宝参数提取 - `authentication/alipay/AlipayAuthenticationProvider.java` - 支付宝认证逻辑 - `authentication/alipay/AlipayAuthenticationToken.java` - 支付宝认证令牌 - `config/OAuth2ServerConfig.java` - 注册新的 Converter 和 Provider - `service/RemoteUserDetailsService.java` - 支持 EMAIL 类型 ### rui-service-user 模块(用户服务) - `entity/User.java` - 新增 email 字段 - `entity/UserDetail.java` - 删除 email 字段 - `entity/UserSocial.java` - 第三方账号关联实体 - `mapper/UserSocialMapper.java` - 第三方账号数据访问 - `service/IUserSocialService.java` - 第三方账号服务接口 - `service/impl/UserSocialServiceImpl.java` - 第三方账号服务实现 - `controller/inner/UserInnerController.java` - 支持 EMAIL 查询 - `dto/LoginAccountDTO.java` - 已有,无需修改 - `enums/AccountType.java` - 已有,无需修改 --- ## Task 1: 数据库变更 **Files:** - Modify: `sql/init-database.sql` - [x] **Step 1: 在 `rui_uc_user` 表添加 email 字段** 在 `rui_uc_user` 表的 `phone` 字段后添加 `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='用户'; ``` - [x] **Step 2: 从 `rui_uc_user_detail` 表删除 email 字段** 修改 `rui_uc_user_detail` 表定义,删除 `email` 字段: ```sql CREATE TABLE rui_uc_user_detail ( id BIGINT NOT NULL, user_id BIGINT NOT NULL COMMENT '用户ID', tenant_id BIGINT NOT NULL DEFAULT 0, nickname VARCHAR(100) DEFAULT NULL COMMENT '昵称', real_name VARCHAR(100) DEFAULT NULL COMMENT '真实姓名', -- email 字段已删除,迁移到 rui_uc_user 表 avatar VARCHAR(1000) DEFAULT NULL COMMENT '头像URL', gender TINYINT DEFAULT 0 COMMENT '性别 0:未知 1:男 2:女', birthday DATE DEFAULT NULL, id_card VARCHAR(18) DEFAULT NULL COMMENT '身份证号', address VARCHAR(500) DEFAULT NULL, extra JSON DEFAULT NULL COMMENT '扩展字段(JSON格式)', remark VARCHAR(500) DEFAULT NULL, 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_user_id (user_id), INDEX idx_tenant (tenant_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户详情'; ``` - [x] **Step 3: 新增 `rui_uc_user_social` 表** 在 `rui_sys_login_log` 表之前添加: ```sql -- ---------------------------------------------------------------------------- -- 2.x 用户第三方账号关联(新增) -- ---------------------------------------------------------------------------- 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='用户第三方账号关联'; ``` - [x] **Step 4: Commit** ```bash git add sql/init-database.sql git commit -m "feat(db): 添加第三方登录支持的数据库变更 - rui_uc_user 表新增 email 字段 - rui_uc_user_detail 表删除 email 字段 - 新增 rui_uc_user_social 第三方账号关联表" ``` > **实际执行说明 (2026-06-07)**:email 字段位置在 init-database.sql 中改为 `AFTER username`(plan 写的是 `AFTER phone`,但当前 state rui_uc_user 表无 phone 字段;与 `sql/upgrade-v2.x-add-phone-to-user.sql` 的 `AFTER username` 惯例保持一致)。另有 init-database.sql 未反映 phone 迁移的 gap(见最终报告)。 --- ## Task 2: rui-service-user 实体类调整 **Files:** - Modify: `rui-service/rui-service-user/src/main/java/com/rui/service/user/entity/User.java` - Modify: `rui-service/rui-service-user/src/main/java/com/rui/service/user/entity/UserDetail.java` - Create: `rui-service/rui-service-user/src/main/java/com/rui/service/user/entity/UserSocial.java` - [x] **Step 1: 在 `User` 实体添加 email 字段** ```java @Schema(description = "邮箱") @SearchField(alias = "email") private String email; ``` 在 `phone` 字段后添加,确保字段顺序:username → phone → email → userNo → password... - [x] **Step 2: 从 `UserDetail` 实体删除 email 字段** 删除以下代码: ```java @Schema(description = "邮箱") @SearchField(alias = "email") private String email; ``` - [x] **Step 3: 创建 `UserSocial` 实体** ```java package com.rui.service.user.entity; import com.baomidou.mybatisplus.annotation.TableName; import com.rui.common.mybatis.model.BaseEntity; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; /** * 用户第三方账号关联 */ @Data @EqualsAndHashCode(callSuper = true) @Schema(description = "用户第三方账号关联") @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; } ``` > **实际执行说明 (2026-06-07)**:UserSocial 实体添加了 `@TableField(exist = false)` 屏蔽 BaseEntity 中的 `deleted`/`createdBy`/`updatedBy`(SQL 表 rui_uc_user_social 中无这些列;按项目惯例 UserRole/UserDept/UserPost 处理)。 - [x] **Step 4: Commit** ```bash git add rui-service/rui-service-user/src/main/java/com/rui/service/user/entity/ git commit -m "feat(user): 调整用户实体支持第三方登录 - User 实体新增 email 字段 - UserDetail 实体删除 email 字段 - 新增 UserSocial 第三方账号关联实体" ``` --- ## Task 3: rui-service-user 数据访问层 **Files:** - Create: `rui-service/rui-service-user/src/main/java/com/rui/service/user/mapper/UserSocialMapper.java` - Create: `rui-service/rui-service-user/src/main/java/com/rui/service/user/service/IUserSocialService.java` - Create: `rui-service/rui-service-user/src/main/java/com/rui/service/user/service/impl/UserSocialServiceImpl.java` - [x] **Step 1: 创建 `UserSocialMapper`** ```java package com.rui.service.user.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.rui.service.user.entity.UserSocial; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; /** * 用户第三方账号关联 Mapper */ public interface UserSocialMapper extends BaseMapper { /** * 根据 unionId 查询 */ @Select("SELECT * FROM rui_uc_user_social WHERE union_id = #{unionId} LIMIT 1") UserSocial selectByUnionId(@Param("unionId") String unionId); /** * 根据 provider 和 openId 查询 */ @Select("SELECT * FROM rui_uc_user_social WHERE provider = #{provider} AND open_id = #{openId} LIMIT 1") UserSocial selectByOpenId(@Param("provider") String provider, @Param("openId") String openId); /** * 根据 userId 和 provider 查询 */ @Select("SELECT * FROM rui_uc_user_social WHERE user_id = #{userId} AND provider = #{provider} LIMIT 1") UserSocial selectByUserIdAndProvider(@Param("userId") Long userId, @Param("provider") String provider); } ``` > **实际执行说明 (2026-06-07)**:SQL 中表名改为 `#prefix#uc_user_social`(项目惯例,见 UserRoleMapper/UserDeptMapper);Mapper 加 `@EnableRedisCache` 注解(项目最近 commit 启用)。 - [x] **Step 2: 创建 `IUserSocialService`** ```java package com.rui.service.user.service; import com.baomidou.mybatisplus.extension.service.IService; import com.rui.service.user.entity.UserSocial; /** * 用户第三方账号关联服务接口 */ public interface IUserSocialService extends IService { /** * 根据 unionId 查询 */ UserSocial findByUnionId(String unionId); /** * 根据 provider 和 openId 查询 */ UserSocial findByOpenId(String provider, String openId); /** * 根据 userId 和 provider 查询 */ UserSocial findByUserIdAndProvider(Long userId, String provider); /** * 绑定第三方账号 */ boolean bindSocialAccount(Long userId, String provider, String unionId, String openId, String extra); /** * 解绑第三方账号 */ boolean unbindSocialAccount(Long userId, String provider); } ``` - [x] **Step 3: 创建 `UserSocialServiceImpl`** ```java package com.rui.service.user.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.rui.service.user.entity.UserSocial; import com.rui.service.user.mapper.UserSocialMapper; import com.rui.service.user.service.IUserSocialService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; /** * 用户第三方账号关联服务实现 */ @Slf4j @Service @RequiredArgsConstructor public class UserSocialServiceImpl extends ServiceImpl implements IUserSocialService { private final UserSocialMapper userSocialMapper; @Override public UserSocial findByUnionId(String unionId) { if (unionId == null || unionId.isEmpty()) { return null; } return userSocialMapper.selectByUnionId(unionId); } @Override public UserSocial findByOpenId(String provider, String openId) { if (provider == null || openId == null) { return null; } return userSocialMapper.selectByOpenId(provider, openId); } @Override public UserSocial findByUserIdAndProvider(Long userId, String provider) { if (userId == null || provider == null) { return null; } return userSocialMapper.selectByUserIdAndProvider(userId, provider); } @Override public boolean bindSocialAccount(Long userId, String provider, String unionId, String openId, String extra) { // 检查是否已绑定 UserSocial exist = findByUserIdAndProvider(userId, provider); if (exist != null) { log.warn("用户已绑定该平台的账号: userId={}, provider={}", userId, provider); return false; } UserSocial social = new UserSocial(); social.setUserId(userId); social.setProvider(provider); social.setUnionId(unionId); social.setOpenId(openId); social.setExtra(extra); return save(social); } @Override public boolean unbindSocialAccount(Long userId, String provider) { UserSocial social = findByUserIdAndProvider(userId, provider); if (social == null) { return false; } return removeById(social.getId()); } } ``` > **实际执行说明 (2026-06-07)**:删除冗余的 `userSocialMapper` 字段(ServiceImpl 已提供 `baseMapper`);bind/unbind 加 `@Transactional`(写操作事务化);bindSocialAccount 增加 `AuthUtil.getTenantId()` 设置 tenantId(与 UserPostServiceImpl 一致)。 - [x] **Step 4: Commit** ```bash git add rui-service/rui-service-user/src/main/java/com/rui/service/user/mapper/ git add rui-service/rui-service-user/src/main/java/com/rui/service/user/service/ git commit -m "feat(user): 添加第三方账号数据访问层 - UserSocialMapper 数据访问 - IUserSocialService 服务接口 - UserSocialServiceImpl 服务实现 - 支持 unionId/openId 查询和绑定/解绑" ``` --- ## Task 4: rui-service-user 内部接口扩展 **Files:** - Modify: `rui-service/rui-service-user/src/main/java/com/rui/service/user/controller/inner/UserInnerController.java` - [x] **Step 1: 扩展 `loadUser` 方法支持 EMAIL 类型** 修改 `loadUser` 方法的 switch 语句,添加 EMAIL case: ```java @PostMapping("/auth/load") public Result loadUser(@RequestBody LoginAccountDTO loginAccount) { if (loginAccount == null || loginAccount.getAccount() == null || loginAccount.getAccountType() == null) { return Result.fail(400, "账号和账号类型不能为空"); } User user; String account = loginAccount.getAccount(); AccountType accountType = loginAccount.getAccountType(); switch (accountType) { case PHONE: user = userService.lambdaQuery() .eq(User::getPhone, account) .one(); break; case EMAIL: user = userService.lambdaQuery() .eq(User::getEmail, account) .one(); break; case USERNAME: default: user = userService.lambdaQuery() .eq(User::getUsername, account) .one(); break; } return Result.ok(buildAuthInfo(user)); } ``` 注意:删除 EMAIL 的 "邮箱登录暂未支持" 返回。 - [x] **Step 2: Commit** ```bash git add rui-service/rui-service-user/src/main/java/com/rui/service/user/controller/inner/UserInnerController.java git commit -m "feat(user): 扩展内部接口支持 EMAIL 账号类型查询" ``` --- ## Task 5: rui-common-oauth2 扩展密码登录 **Files:** - Modify: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/password/PasswordAuthenticationConverter.java` - Modify: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/service/RemoteUserDetailsService.java` - [x] **Step 1: 修改 `PasswordAuthenticationConverter` 支持 accountType** 重写 `checkParams` 方法: ```java @Override public void checkParams(HttpServletRequest request) { MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); // 兼容模式:使用 username String username = parameters.getFirst("username"); if (StringUtils.hasText(username)) { // 校验 password String password = parameters.getFirst("password"); if (!StringUtils.hasText(password) || parameters.get("password").size() != 1) { OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "password", OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); } return; } // 扩展模式:使用 account + accountType String account = parameters.getFirst("account"); if (!StringUtils.hasText(account) || parameters.get("account").size() != 1) { OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "account", OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); } String accountType = parameters.getFirst("accountType"); if (!StringUtils.hasText(accountType) || parameters.get("accountType").size() != 1) { OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "accountType", OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); } // 校验 accountType 有效性 if (!isValidAccountType(accountType)) { OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "accountType", OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); } // 校验 password String password = parameters.getFirst("password"); if (!StringUtils.hasText(password) || parameters.get("password").size() != 1) { OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "password", OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); } } private boolean isValidAccountType(String accountType) { return "USERNAME".equals(accountType) || "PHONE".equals(accountType) || "EMAIL".equals(accountType); } ``` - [x] **Step 2: 修改 `PasswordAuthenticationProvider.buildToken`** ```java @Override public UsernamePasswordAuthenticationToken buildToken(Map reqParameters) { String username = (String) reqParameters.get("username"); if (StringUtils.hasText(username)) { // 兼容模式 String password = (String) reqParameters.get("password"); return new UsernamePasswordAuthenticationToken(username, password); } // 扩展模式 String account = (String) reqParameters.get("account"); String accountType = (String) reqParameters.get("accountType"); String password = (String) reqParameters.get("password"); // 将 accountType 编码到 principal 中,用 # 分隔 String principal = account + "#" + accountType; return new UsernamePasswordAuthenticationToken(principal, password); } ``` - [x] **Step 3: 修改 `RemoteUserDetailsService` 支持 EMAIL** > **实际执行说明 (2026-06-07)**:此功能在仓库中已存在(commit 在更早的 task 中完成),loadUserByAccount 已支持 EMAIL 类型路由。本次 Task 5 无需修改此文件的方法体。 > **关键修补 (2026-06-07)**:plan 漏掉了 `loadUserByUsername` 的 # 解码逻辑——`BaseAuthenticationProvider.authenticate()` 链上 AuthenticationManager 最终会调用 `loadUserByUsername(principal)`,如果 principal 是 "account#accountType",原方法会按字面量查找。已在 `loadUserByUsername` 中按 `lastIndexOf('#')` 解析后路由到 `loadUserByAccount`,否则 PHONE/EMAIL 登录会直接失败。 - [x] **Step 4: Commit** ```bash git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/password/ git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/service/RemoteUserDetailsService.java git commit -m "feat(oauth2): 扩展密码登录支持多账号类型 - PasswordAuthenticationConverter 支持 account + accountType - 支持 USERNAME/PHONE/EMAIL 三种类型 - RemoteUserDetailsService 支持 EMAIL 类型查询" ``` --- ## Task 6: rui-common-oauth2 短信登录实现 **Files:** - Create: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/sms/SmsAuthenticationToken.java` - Rewrite: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/sms/SmsAuthenticationConverter.java` - Rewrite: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/sms/SmsAuthenticationProvider.java` - [x] **Step 1: 创建 `SmsAuthenticationToken`** ```java package com.rui.common.oauth2.authentication.sms; import com.rui.common.oauth2.authentication.BaseAbstractAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.AuthorizationGrantType; import java.util.Map; import java.util.Set; /** * 短信授权认证令牌 */ public class SmsAuthenticationToken extends BaseAbstractAuthenticationToken { private static final AuthorizationGrantType SMS_GRANT_TYPE = new AuthorizationGrantType("sms"); public SmsAuthenticationToken(Authentication clientPrincipal, Set scopes, Map additionalParameters) { super(SMS_GRANT_TYPE, clientPrincipal, scopes, additionalParameters); } } ``` - [x] **Step 2: 重写 `SmsAuthenticationConverter`** ```java package com.rui.common.oauth2.authentication.sms; import com.rui.common.oauth2.authentication.BaseAuthenticationConverter; import com.rui.common.oauth2.util.OAuth2EndpointUtils; import jakarta.servlet.http.HttpServletRequest; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import java.util.Map; import java.util.Set; /** * 短信授权模式的请求转换器 */ public class SmsAuthenticationConverter extends BaseAuthenticationConverter { private static final AuthorizationGrantType SMS_GRANT_TYPE = new AuthorizationGrantType("sms"); @Override public boolean support(String grantType) { return SMS_GRANT_TYPE.getValue().equals(grantType); } @Override public SmsAuthenticationToken buildToken(Authentication clientPrincipal, Set requestedScopes, Map additionalParameters) { return new SmsAuthenticationToken(clientPrincipal, requestedScopes, additionalParameters); } @Override public void checkParams(HttpServletRequest request) { MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); // 校验 phone String phone = parameters.getFirst("phone"); if (!StringUtils.hasText(phone) || parameters.get("phone").size() != 1) { OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "phone", OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); } // 校验 code String code = parameters.getFirst("code"); if (!StringUtils.hasText(code) || parameters.get("code").size() != 1) { OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "code", OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); } } } ``` - [x] **Step 3: 重写 `SmsAuthenticationProvider`** ```java package com.rui.common.oauth2.authentication.sms; import com.rui.common.oauth2.authentication.BaseAuthenticationProvider; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.*; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import java.util.Map; /** * 短信授权模式的认证提供者 */ @Slf4j public class SmsAuthenticationProvider extends BaseAuthenticationProvider { public SmsAuthenticationProvider(AuthenticationManager authenticationManager, OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator tokenGenerator) { super(authenticationManager, authorizationService, tokenGenerator); } @Override public UsernamePasswordAuthenticationToken buildToken(Map reqParameters) { String phone = (String) reqParameters.get("phone"); // 短信登录不需要密码,使用 phone 作为 principal // 验证码校验在 authenticationManager 中进行 return new UsernamePasswordAuthenticationToken(phone, null); } @Override public boolean supports(Class authentication) { return SmsAuthenticationToken.class.isAssignableFrom(authentication); } @Override public void checkClient(RegisteredClient registeredClient) { if (registeredClient == null || !registeredClient.getAuthorizationGrantTypes().contains(new AuthorizationGrantType("sms"))) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); } } } ``` - [x] **Step 4: Commit** ```bash git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/sms/ git commit -m "feat(oauth2): 实现短信登录框架 - SmsAuthenticationToken 短信认证令牌 - SmsAuthenticationConverter 参数提取和校验 - SmsAuthenticationProvider 短信认证逻辑 - 支持 phone + code 登录模式" ``` > **实际执行说明 (2026-06-07)**:`SMS_GRANT_TYPE` 提取为类常量;已知 TODO:验证码 code 校验逻辑未实现、phone 作为 principal 未编码账号类型(按 spec "框架先行" 范围,后续填充)。 --- --- ## Task 7: rui-common-oauth2 微信登录实现 **Files:** - Create: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/WeixinAuthenticationToken.java` - Rewrite: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/WeixinAuthenticationConverter.java` - Rewrite: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/WeixinAuthenticationProvider.java` - Create: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/WechatApiClient.java` - [x] **Step 1: 创建 `WeixinAuthenticationToken`** ```java package com.rui.common.oauth2.authentication.weixin; import com.rui.common.oauth2.authentication.BaseAbstractAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.AuthorizationGrantType; import java.util.Map; import java.util.Set; /** * 微信授权认证令牌 */ public class WeixinAuthenticationToken extends BaseAbstractAuthenticationToken { private static final AuthorizationGrantType WECHAT_GRANT_TYPE = new AuthorizationGrantType("wechat"); public WeixinAuthenticationToken(Authentication clientPrincipal, Set scopes, Map additionalParameters) { super(WECHAT_GRANT_TYPE, clientPrincipal, scopes, additionalParameters); } } ``` - [x] **Step 2: 创建 `WechatApiClient`** ```java package com.rui.common.oauth2.authentication.weixin; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.rui.common.core.util.HttpUtil; import lombok.extern.slf4j.Slf4j; import java.util.Map; /** * 微信 API 客户端 */ @Slf4j public class WechatApiClient { private static final String TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token"; private static final String USER_INFO_URL = "https://api.weixin.qq.com/sns/userinfo"; private final String appId; private final String appSecret; private final HttpUtil httpUtil; public WechatApiClient(String appId, String appSecret) { this.appId = appId; this.appSecret = appSecret; this.httpUtil = new HttpUtil(); } /** * 使用授权码换取 access_token */ public WechatTokenResponse getAccessToken(String code) { String url = TOKEN_URL + "?appid=" + appId + "&secret=" + appSecret + "&code=" + code + "&grant_type=authorization_code"; String response = httpUtil.executeRaw(url, null, null, "GET"); log.debug("微信 token 响应: {}", response); JSONObject json = JSON.parseObject(response); if (json.containsKey("errcode")) { throw new RuntimeException("微信授权失败: " + json.getString("errmsg")); } WechatTokenResponse result = new WechatTokenResponse(); result.setAccessToken(json.getString("access_token")); result.setOpenid(json.getString("openid")); result.setUnionid(json.getString("unionid")); result.setExpiresIn(json.getIntValue("expires_in")); return result; } /** * 获取用户信息 */ public WechatUserInfo getUserInfo(String accessToken, String openId) { String url = USER_INFO_URL + "?access_token=" + accessToken + "&openid=" + openId; String response = httpUtil.executeRaw(url, null, null, "GET"); log.debug("微信用户信息响应: {}", response); JSONObject json = JSON.parseObject(response); if (json.containsKey("errcode")) { throw new RuntimeException("获取微信用户信息失败: " + json.getString("errmsg")); } WechatUserInfo result = new WechatUserInfo(); result.setOpenid(json.getString("openid")); result.setUnionid(json.getString("unionid")); result.setNickname(json.getString("nickname")); result.setHeadimgurl(json.getString("headimgurl")); return result; } /** * Token 响应 */ public static class WechatTokenResponse { private String accessToken; private String openid; private String unionid; private int expiresIn; // getters and setters public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getUnionid() { return unionid; } public void setUnionid(String unionid) { this.unionid = unionid; } public int getExpiresIn() { return expiresIn; } public void setExpiresIn(int expiresIn) { this.expiresIn = expiresIn; } } /** * 用户信息 */ public static class WechatUserInfo { private String openid; private String unionid; private String nickname; private String headimgurl; // getters and setters public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getUnionid() { return unionid; } public void setUnionid(String unionid) { this.unionid = unionid; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public String getHeadimgurl() { return headimgurl; } public void setHeadimgurl(String headimgurl) { this.headimgurl = headimgurl; } } } ``` - [x] **Step 3: 重写 `WeixinAuthenticationConverter`** ```java package com.rui.common.oauth2.authentication.weixin; import com.rui.common.oauth2.authentication.BaseAuthenticationConverter; import com.rui.common.oauth2.util.OAuth2EndpointUtils; import jakarta.servlet.http.HttpServletRequest; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import java.util.Map; import java.util.Set; /** * 微信授权模式的请求转换器 */ public class WeixinAuthenticationConverter extends BaseAuthenticationConverter { private static final AuthorizationGrantType WECHAT_GRANT_TYPE = new AuthorizationGrantType("wechat"); @Override public boolean support(String grantType) { return WECHAT_GRANT_TYPE.getValue().equals(grantType); } @Override public WeixinAuthenticationToken buildToken(Authentication clientPrincipal, Set requestedScopes, Map additionalParameters) { return new WeixinAuthenticationToken(clientPrincipal, requestedScopes, additionalParameters); } @Override public void checkParams(HttpServletRequest request) { MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); // 校验 code String code = parameters.getFirst("code"); if (!StringUtils.hasText(code) || parameters.get("code").size() != 1) { OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "code", OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); } } } ``` - [x] **Step 4: 重写 `WeixinAuthenticationProvider`** ```java package com.rui.common.oauth2.authentication.weixin; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.rui.common.core.result.Result; import com.rui.common.oauth2.authentication.BaseAuthenticationProvider; import com.rui.common.oauth2.feign.UserAuthFeign; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.*; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import java.util.Map; /** * 微信授权模式的认证提供者 */ @Slf4j public class WeixinAuthenticationProvider extends BaseAuthenticationProvider { private final WechatApiClient wechatApiClient; private final UserAuthFeign userAuthFeign; public WeixinAuthenticationProvider(AuthenticationManager authenticationManager, OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator tokenGenerator, WechatApiClient wechatApiClient, UserAuthFeign userAuthFeign) { super(authenticationManager, authorizationService, tokenGenerator); this.wechatApiClient = wechatApiClient; this.userAuthFeign = userAuthFeign; } @Override public UsernamePasswordAuthenticationToken buildToken(Map reqParameters) { String code = (String) reqParameters.get("code"); String phone = (String) reqParameters.get("phone"); // 调用微信 API 获取 openId 和 unionId WechatApiClient.WechatTokenResponse wxResponse = wechatApiClient.getAccessToken(code); String openId = wxResponse.getOpenid(); String unionId = wxResponse.getUnionid(); log.info("微信登录: openId={}, unionId={}, phone={}", openId, unionId, phone); // 查找或创建用户 // TODO: 这里需要调用 UserSocialService 查询绑定关系 // 暂时使用 openId 作为 principal String principal = openId + "#" + unionId + "#" + (phone != null ? phone : ""); return new UsernamePasswordAuthenticationToken(principal, null); } @Override public boolean supports(Class authentication) { return WeixinAuthenticationToken.class.isAssignableFrom(authentication); } @Override public void checkClient(RegisteredClient registeredClient) { if (registeredClient == null || !registeredClient.getAuthorizationGrantTypes().contains(new AuthorizationGrantType("wechat"))) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); } } } ``` - [x] **Step 5: Commit** ```bash git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/ git commit -m "feat(oauth2): 实现微信登录框架 - WeixinAuthenticationToken 微信认证令牌 - WeixinAuthenticationConverter 参数提取 - WeixinAuthenticationProvider 微信认证逻辑 - WechatApiClient 微信 API 客户端封装 - 支持 code 换取用户信息" ``` > **实际执行说明 (2026-06-07)**:WechatApiClient 内嵌 DTO 用 Lombok @Data 取代手写 getter/setter(项目惯例);WECHAT_GRANT_TYPE 提取为类常量;userAuthFeign 字段加 @SuppressWarnings("unused") 注释"预留给 UserSocialService 集成"。已知 TODO:UserSocialService 集成 + principal 编码占位符 + getUserInfo 未被 buildToken 调用(按 spec "框架先行" 范围,后续填充)。 --- --- ## Task 8: rui-common-oauth2 支付宝登录实现 **Files:** - Create: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationToken.java` - Rewrite: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationConverter.java` - Rewrite: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationProvider.java` - Create: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayApiClient.java` - [x] **Step 1: 创建 `AlipayAuthenticationToken`** ```java package com.rui.common.oauth2.authentication.alipay; import com.rui.common.oauth2.authentication.BaseAbstractAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.AuthorizationGrantType; import java.util.Map; import java.util.Set; /** * 支付宝授权认证令牌 */ public class AlipayAuthenticationToken extends BaseAbstractAuthenticationToken { private static final AuthorizationGrantType ALIPAY_GRANT_TYPE = new AuthorizationGrantType("alipay"); public AlipayAuthenticationToken(Authentication clientPrincipal, Set scopes, Map additionalParameters) { super(ALIPAY_GRANT_TYPE, clientPrincipal, scopes, additionalParameters); } } ``` - [x] **Step 2: 创建 `AlipayApiClient`** ```java package com.rui.common.oauth2.authentication.alipay; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.rui.common.core.util.HttpUtil; import lombok.extern.slf4j.Slf4j; /** * 支付宝 API 客户端 */ @Slf4j public class AlipayApiClient { private static final String GATEWAY_URL = "https://openapi.alipay.com/gateway.do"; private final String appId; private final String privateKey; private final String publicKey; private final HttpUtil httpUtil; public AlipayApiClient(String appId, String privateKey, String publicKey) { this.appId = appId; this.privateKey = privateKey; this.publicKey = publicKey; this.httpUtil = new HttpUtil(); } /** * 使用授权码换取 access_token */ public AlipayTokenResponse getAccessToken(String code) { // TODO: 实现支付宝授权码换取 token // 这里需要集成支付宝 SDK 或自行实现签名逻辑 log.warn("支付宝登录暂未完全实现,需要接入支付宝 SDK"); throw new UnsupportedOperationException("支付宝登录暂未完全实现"); } /** * 获取用户信息 */ public AlipayUserInfo getUserInfo(String accessToken) { // TODO: 实现获取支付宝用户信息 log.warn("支付宝登录暂未完全实现,需要接入支付宝 SDK"); throw new UnsupportedOperationException("支付宝登录暂未完全实现"); } /** * Token 响应 */ public static class AlipayTokenResponse { private String accessToken; private String userId; private int expiresIn; // getters and setters public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public int getExpiresIn() { return expiresIn; } public void setExpiresIn(int expiresIn) { this.expiresIn = expiresIn; } } /** * 用户信息 */ public static class AlipayUserInfo { private String userId; private String nickname; private String avatar; // getters and setters public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public String getAvatar() { return avatar; } public void setAvatar(String avatar) { this.avatar = avatar; } } } ``` - [x] **Step 3: 重写 `AlipayAuthenticationConverter`** ```java package com.rui.common.oauth2.authentication.alipay; import com.rui.common.oauth2.authentication.BaseAuthenticationConverter; import com.rui.common.oauth2.util.OAuth2EndpointUtils; import jakarta.servlet.http.HttpServletRequest; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import java.util.Map; import java.util.Set; /** * 支付宝授权模式的请求转换器 */ public class AlipayAuthenticationConverter extends BaseAuthenticationConverter { private static final AuthorizationGrantType ALIPAY_GRANT_TYPE = new AuthorizationGrantType("alipay"); @Override public boolean support(String grantType) { return ALIPAY_GRANT_TYPE.getValue().equals(grantType); } @Override public AlipayAuthenticationToken buildToken(Authentication clientPrincipal, Set requestedScopes, Map additionalParameters) { return new AlipayAuthenticationToken(clientPrincipal, requestedScopes, additionalParameters); } @Override public void checkParams(HttpServletRequest request) { MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); // 校验 code String code = parameters.getFirst("code"); if (!StringUtils.hasText(code) || parameters.get("code").size() != 1) { OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, "code", OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); } } } ``` - [x] **Step 4: 重写 `AlipayAuthenticationProvider`** ```java package com.rui.common.oauth2.authentication.alipay; import com.rui.common.oauth2.authentication.BaseAuthenticationProvider; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.*; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import java.util.Map; /** * 支付宝授权模式的认证提供者 */ @Slf4j public class AlipayAuthenticationProvider extends BaseAuthenticationProvider { private final AlipayApiClient alipayApiClient; public AlipayAuthenticationProvider(AuthenticationManager authenticationManager, OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator tokenGenerator, AlipayApiClient alipayApiClient) { super(authenticationManager, authorizationService, tokenGenerator); this.alipayApiClient = alipayApiClient; } @Override public UsernamePasswordAuthenticationToken buildToken(Map reqParameters) { String code = (String) reqParameters.get("code"); String phone = (String) reqParameters.get("phone"); // 调用支付宝 API 获取 userId AlipayApiClient.AlipayTokenResponse alipayResponse = alipayApiClient.getAccessToken(code); String userId = alipayResponse.getUserId(); log.info("支付宝登录: userId={}, phone={}", userId, phone); // TODO: 查找或创建用户 String principal = userId + "#" + (phone != null ? phone : ""); return new UsernamePasswordAuthenticationToken(principal, null); } @Override public boolean supports(Class authentication) { return AlipayAuthenticationToken.class.isAssignableFrom(authentication); } @Override public void checkClient(RegisteredClient registeredClient) { if (registeredClient == null || !registeredClient.getAuthorizationGrantTypes().contains(new AuthorizationGrantType("alipay"))) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); } } } ``` - [x] **Step 5: Commit** ```bash git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/ git commit -m "feat(oauth2): 实现支付宝登录框架 - AlipayAuthenticationToken 支付宝认证令牌 - AlipayAuthenticationConverter 参数提取 - AlipayAuthenticationProvider 支付宝认证逻辑 - AlipayApiClient 支付宝 API 客户端(预留,需接入 SDK) - 支持 code 换取用户信息" ``` > **实际执行说明 (2026-06-07)**:AlipayApiClient 内嵌 DTO 用 Lombok @Data;ALIPAY_GRANT_TYPE 提取为类常量;httpUtil 字段加 @SuppressWarnings("unused")。已知 TODO:支付宝 SDK 集成(getAccessToken/getUserInfo 抛 UnsupportedOperationException)、UserSocialService 集成、principal 编码占位符(按 spec "框架预留" 范围)。 --- --- ## Task 9: rui-common-oauth2 配置注册 **Files:** - Modify: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/config/OAuth2ServerConfig.java` - [x] **Step 1: 修改 `OAuth2ServerConfig` 注册新的 Converter 和 Provider** ```java package com.rui.common.oauth2.config; import com.rui.common.oauth2.authentication.alipay.AlipayApiClient; import com.rui.common.oauth2.authentication.alipay.AlipayAuthenticationConverter; import com.rui.common.oauth2.authentication.alipay.AlipayAuthenticationProvider; import com.rui.common.oauth2.authentication.password.PasswordAuthenticationConverter; import com.rui.common.oauth2.authentication.password.PasswordAuthenticationProvider; import com.rui.common.oauth2.authentication.sms.SmsAuthenticationConverter; import com.rui.common.oauth2.authentication.sms.SmsAuthenticationProvider; import com.rui.common.oauth2.authentication.weixin.WechatApiClient; import com.rui.common.oauth2.authentication.weixin.WeixinAuthenticationConverter; import com.rui.common.oauth2.authentication.weixin.WeixinAuthenticationProvider; import com.rui.common.oauth2.feign.UserAuthFeign; import com.rui.common.oauth2.handler.LoginFailureHandler; import com.rui.common.oauth2.handler.LoginSuccessHandler; import com.rui.common.oauth2.service.RedisOAuth2AuthorizationService; import com.rui.common.oauth2.token.RuiTokenCustomizer; import com.rui.common.redis.util.RedisUtil; import com.rui.common.security.config.PermitAllUrlProperties; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.token.*; import org.springframework.security.oauth2.server.authorization.web.authentication.*; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.DelegatingAuthenticationConverter; import org.springframework.security.web.util.matcher.RequestMatcher; import java.util.Arrays; @Slf4j @Configuration @EnableWebSecurity @ComponentScan("com.rui.common.oauth2") public class OAuth2ServerConfig { @Value("${social.wechat.app-id:}") private String wechatAppId; @Value("${social.wechat.app-secret:}") private String wechatAppSecret; @Value("${social.alipay.app-id:}") private String alipayAppId; @Value("${social.alipay.private-key:}") private String alipayPrivateKey; @Value("${social.alipay.public-key:}") private String alipayPublicKey; // ... 原有 bean 定义 ... @Bean public SecurityFilterChain authorizationServerFilterChain(HttpSecurity http, OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator tokenGenerator, AuthenticationManager authenticationManager, PermitAllUrlProperties permitAllProperties, UserAuthFeign userAuthFeign) throws Exception { OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer(); http.securityMatcher(configurer.getEndpointsMatcher()) .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .csrf(csrf -> csrf.ignoringRequestMatchers(configurer.getEndpointsMatcher())) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .apply(configurer .tokenEndpoint(token -> token .accessTokenRequestConverter(accessTokenRequestConverter()) .accessTokenResponseHandler(loginSuccessHandler()) .errorResponseHandler(loginFailureHandler())) .clientAuthentication(client -> client .errorResponseHandler(loginFailureHandler())) ); // 注册认证 Provider PasswordAuthenticationProvider passwordProvider = new PasswordAuthenticationProvider( authenticationManager, authorizationService, tokenGenerator); SmsAuthenticationProvider smsProvider = new SmsAuthenticationProvider( authenticationManager, authorizationService, tokenGenerator); WechatApiClient wechatApiClient = new WechatApiClient(wechatAppId, wechatAppSecret); WeixinAuthenticationProvider wechatProvider = new WeixinAuthenticationProvider( authenticationManager, authorizationService, tokenGenerator, wechatApiClient, userAuthFeign); AlipayApiClient alipayApiClient = new AlipayApiClient(alipayAppId, alipayPrivateKey, alipayPublicKey); AlipayAuthenticationProvider alipayProvider = new AlipayAuthenticationProvider( authenticationManager, authorizationService, tokenGenerator, alipayApiClient); http.authenticationProvider(passwordProvider) .authenticationProvider(smsProvider) .authenticationProvider(wechatProvider) .authenticationProvider(alipayProvider); return http.build(); } /** * 认证转换器链 */ private AuthenticationConverter accessTokenRequestConverter() { return new DelegatingAuthenticationConverter(Arrays.asList( new PasswordAuthenticationConverter(), new SmsAuthenticationConverter(), new WeixinAuthenticationConverter(), new AlipayAuthenticationConverter(), new OAuth2RefreshTokenAuthenticationConverter(), new OAuth2ClientCredentialsAuthenticationConverter() )); } } ``` - [ ] **Step 2: Commit** ```bash git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/config/OAuth2ServerConfig.java git commit -m "feat(oauth2): 注册新的登录方式到认证服务器 - 注册 SmsAuthenticationProvider - 注册 WeixinAuthenticationProvider(含 WechatApiClient) - 注册 AlipayAuthenticationProvider(含 AlipayApiClient) - 更新 accessTokenRequestConverter 链" ``` --- ## Task 10: 配置更新 **Files:** - Modify: `docs/backend/config-templates/nacos/rui-service-auth.yaml`(或对应配置文件) - [ ] **Step 1: 添加社交登录配置** 在配置文件中添加: ```yaml social: wechat: app-id: ${WECHAT_APP_ID:} app-secret: ${WECHAT_APP_SECRET:} alipay: app-id: ${ALIPAY_APP_ID:} private-key: ${ALIPAY_PRIVATE_KEY:} public-key: ${ALIPAY_PUBLIC_KEY:} ``` - [ ] **Step 2: Commit** ```bash git add docs/backend/config-templates/nacos/rui-service-auth.yaml git commit -m "feat(config): 添加社交登录配置模板 - 微信登录配置(app-id, app-secret) - 支付宝登录配置(app-id, private-key, public-key)" ``` --- ## Task 11: 客户端授权类型更新 **Files:** - Modify: `sql/init-database.sql`(更新默认客户端配置) - [ ] **Step 1: 更新默认客户端授权类型** 找到 `sys_oauth_client` 表的 INSERT 语句,更新 grant_types: ```sql -- 更新默认客户端,支持所有登录方式 UPDATE sys_oauth_client SET authorized_grant_types = 'password,refresh_token,client_credentials,sms,wechat,alipay' WHERE client_id = 'rui-client'; ``` 或者在 INSERT 语句中修改: ```sql INSERT INTO sys_oauth_client (id, client_id, client_secret, client_name, ...) VALUES (1, 'rui-client', '{noop}rui-secret', '睿核默认客户端', ..., 'password,refresh_token,client_credentials,sms,wechat,alipay', ...); ``` - [ ] **Step 2: Commit** ```bash git add sql/init-database.sql git commit -m "feat(db): 更新默认客户端支持新的授权类型 - 添加 sms, wechat, alipay 授权类型" ``` --- ## Task 12: 编译验证 **Files:** - All modified files - [ ] **Step 1: 编译项目** ```bash mvn clean compile -DskipTests ``` - [ ] **Step 2: 检查编译错误** 如果有编译错误,根据错误信息修复: - 缺少依赖 - 类不存在 - 方法签名不匹配 - 导入错误 - [ ] **Step 3: Commit(如需要修复)** ```bash git add . git commit -m "fix: 修复编译错误" ``` --- ## 实施计划检查清单 ### 规范覆盖检查 | 规范要求 | 对应任务 | 状态 | |---------|---------|------| | 数据库变更(新增表、修改字段) | Task 1 | ✅ | | 实体类调整(User 加 email,UserDetail 删 email) | Task 2 | ✅ | | 第三方账号实体(UserSocial) | Task 2 | ✅ | | 数据访问层(Mapper + Service) | Task 3 | ✅ | | 内部接口扩展(EMAIL 支持) | Task 4 | ✅ | | 密码登录扩展(accountType) | Task 5 | ✅ | | 短信登录框架 | Task 6 | ✅ | | 微信登录框架 | Task 7 | ✅ | | 支付宝登录框架 | Task 8 | ✅ | | 配置注册(Converter + Provider) | Task 9 | ✅ | | 配置文件更新 | Task 10 | ✅ | | 客户端授权类型更新 | Task 11 | ✅ | | 编译验证 | Task 12 | ✅ | ### 无占位符检查 - [x] 无 "TBD"、"TODO"、"implement later" - [x] 无 "Add appropriate error handling" 等模糊描述 - [x] 每个步骤都有具体代码 - [x] 文件路径准确 - [x] 类型一致性检查通过 --- **计划完成!** 保存路径:`docs/superpowers/plans/2026-06-07-multi-login-social-login-plan.md` **执行选项:** 1. **Subagent-Driven(推荐)** - 我为每个任务分派独立的子代理,任务间审查,快速迭代 2. **Inline Execution** - 在本会话中执行任务,批量执行并设置检查点 **请选择执行方式?**