- UserSocialMapper/Service/Impl 创建完成 (commit c147e56) - 标注 5 处实际执行的偏差(#prefix#、@EnableRedisCache、@Transactional、baseMapper、tenantId)
58 KiB
多方式登录与第三方登录实施计划
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- 扩展支持 accountTypeauthentication/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 和 Providerservice/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 -
Step 1: 在
rui_uc_user表添加 email 字段
在 rui_uc_user 表的 phone 字段后添加 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='用户';
- Step 2: 从
rui_uc_user_detail表删除 email 字段
修改 rui_uc_user_detail 表定义,删除 email 字段:
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='用户详情';
- Step 3: 新增
rui_uc_user_social表
在 rui_sys_login_log 表之前添加:
-- ----------------------------------------------------------------------------
-- 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='用户第三方账号关联';
- Step 4: Commit
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 -
Step 1: 在
User实体添加 email 字段
@Schema(description = "邮箱")
@SearchField(alias = "email")
private String email;
在 phone 字段后添加,确保字段顺序:username → phone → email → userNo → password...
- Step 2: 从
UserDetail实体删除 email 字段
删除以下代码:
@Schema(description = "邮箱")
@SearchField(alias = "email")
private String email;
- Step 3: 创建
UserSocial实体
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 处理)。
- Step 4: Commit
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 -
Step 1: 创建
UserSocialMapper
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<UserSocial> {
/**
* 根据 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 启用)。
- Step 2: 创建
IUserSocialService
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<UserSocial> {
/**
* 根据 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);
}
- Step 3: 创建
UserSocialServiceImpl
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<UserSocialMapper, UserSocial> 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 一致)。
- Step 4: Commit
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 -
Step 1: 扩展
loadUser方法支持 EMAIL 类型
修改 loadUser 方法的 switch 语句,添加 EMAIL case:
@PostMapping("/auth/load")
public Result<JSONObject> 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 的 "邮箱登录暂未支持" 返回。
- Step 2: Commit
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 -
Step 1: 修改
PasswordAuthenticationConverter支持 accountType
重写 checkParams 方法:
@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) || 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);
}
- Step 2: 修改
PasswordAuthenticationProvider.buildToken
@Override
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> 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);
}
- Step 3: 修改
RemoteUserDetailsService支持 EMAIL
修改 loadUserByAccount 方法,添加 EMAIL 支持:
public UserDetails loadUserByAccount(String account, String accountType) throws UsernameNotFoundException {
String cacheKey = String.format(CACHE_KEY, account);
JSONObject info = getCache(cacheKey);
if (info == null) {
try {
Result<JSONObject> result;
if ("USERNAME".equals(accountType)) {
result = userAuthFeign.loadUser(account);
} else {
Map<String, Object> loginAccount = Map.of(
"account", account,
"accountType", accountType
);
result = userAuthFeign.loadUser(loginAccount);
}
// ... 原有逻辑
} catch (Exception e) {
// ... 原有逻辑
}
}
return buildUserDetails(info, account);
}
- Step 4: Commit
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 -
Step 1: 创建
SmsAuthenticationToken
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<String> scopes,
Map<String, Object> additionalParameters) {
super(SMS_GRANT_TYPE, clientPrincipal, scopes, additionalParameters);
}
}
- Step 2: 重写
SmsAuthenticationConverter
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<SmsAuthenticationToken> {
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<String> requestedScopes,
Map<String, Object> additionalParameters) {
return new SmsAuthenticationToken(clientPrincipal, requestedScopes, additionalParameters);
}
@Override
public void checkParams(HttpServletRequest request) {
MultiValueMap<String, String> 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);
}
}
}
- Step 3: 重写
SmsAuthenticationProvider
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<SmsAuthenticationToken> {
public SmsAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
super(authenticationManager, authorizationService, tokenGenerator);
}
@Override
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
String phone = (String) reqParameters.get("phone");
// 短信登录不需要密码,使用 phone 作为 principal
// 验证码校验在 authenticationManager 中进行
return new UsernamePasswordAuthenticationToken(phone, null);
}
@Override
public boolean supports(Class<? extends Authentication> 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);
}
}
}
- Step 4: Commit
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 登录模式"
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 -
Step 1: 创建
WeixinAuthenticationToken
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<String> scopes,
Map<String, Object> additionalParameters) {
super(WECHAT_GRANT_TYPE, clientPrincipal, scopes, additionalParameters);
}
}
- Step 2: 创建
WechatApiClient
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; }
}
}
- Step 3: 重写
WeixinAuthenticationConverter
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<WeixinAuthenticationToken> {
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<String> requestedScopes,
Map<String, Object> additionalParameters) {
return new WeixinAuthenticationToken(clientPrincipal, requestedScopes, additionalParameters);
}
@Override
public void checkParams(HttpServletRequest request) {
MultiValueMap<String, String> 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);
}
}
}
- Step 4: 重写
WeixinAuthenticationProvider
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<WeixinAuthenticationToken> {
private final WechatApiClient wechatApiClient;
private final UserAuthFeign userAuthFeign;
public WeixinAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
WechatApiClient wechatApiClient,
UserAuthFeign userAuthFeign) {
super(authenticationManager, authorizationService, tokenGenerator);
this.wechatApiClient = wechatApiClient;
this.userAuthFeign = userAuthFeign;
}
@Override
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> 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<? extends Authentication> 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);
}
}
}
- Step 5: Commit
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 换取用户信息"
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 -
Step 1: 创建
AlipayAuthenticationToken
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<String> scopes,
Map<String, Object> additionalParameters) {
super(ALIPAY_GRANT_TYPE, clientPrincipal, scopes, additionalParameters);
}
}
- Step 2: 创建
AlipayApiClient
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; }
}
}
- Step 3: 重写
AlipayAuthenticationConverter
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<AlipayAuthenticationToken> {
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<String> requestedScopes,
Map<String, Object> additionalParameters) {
return new AlipayAuthenticationToken(clientPrincipal, requestedScopes, additionalParameters);
}
@Override
public void checkParams(HttpServletRequest request) {
MultiValueMap<String, String> 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);
}
}
}
- Step 4: 重写
AlipayAuthenticationProvider
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<AlipayAuthenticationToken> {
private final AlipayApiClient alipayApiClient;
public AlipayAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
AlipayApiClient alipayApiClient) {
super(authenticationManager, authorizationService, tokenGenerator);
this.alipayApiClient = alipayApiClient;
}
@Override
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> 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<? extends Authentication> 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);
}
}
}
- Step 5: Commit
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 换取用户信息"
Task 9: rui-common-oauth2 配置注册
Files:
-
Modify:
rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/config/OAuth2ServerConfig.java -
Step 1: 修改
OAuth2ServerConfig注册新的 Converter 和 Provider
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
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: 添加社交登录配置
在配置文件中添加:
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
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:
-- 更新默认客户端,支持所有登录方式
UPDATE sys_oauth_client
SET authorized_grant_types = 'password,refresh_token,client_credentials,sms,wechat,alipay'
WHERE client_id = 'rui-client';
或者在 INSERT 语句中修改:
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
git add sql/init-database.sql
git commit -m "feat(db): 更新默认客户端支持新的授权类型
- 添加 sms, wechat, alipay 授权类型"
Task 12: 编译验证
Files:
-
All modified files
-
Step 1: 编译项目
mvn clean compile -DskipTests
- Step 2: 检查编译错误
如果有编译错误,根据错误信息修复:
-
缺少依赖
-
类不存在
-
方法签名不匹配
-
导入错误
-
Step 3: Commit(如需要修复)
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 | ✅ |
无占位符检查
- 无 "TBD"、"TODO"、"implement later"
- 无 "Add appropriate error handling" 等模糊描述
- 每个步骤都有具体代码
- 文件路径准确
- 类型一致性检查通过
计划完成!
保存路径:docs/superpowers/plans/2026-06-07-multi-login-social-login-plan.md
执行选项:
- Subagent-Driven(推荐) - 我为每个任务分派独立的子代理,任务间审查,快速迭代
- Inline Execution - 在本会话中执行任务,批量执行并设置检查点
请选择执行方式?