mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-10 14:56:06 +08:00
refactor: SocialEndpoint 瘦控制器化, 编排逻辑下沉至 SocialLoginService
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
package cn.daxpay.open.platform.iam.endpoint;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import cn.daxpay.open.platform.capability.auth.util.SecurityUtil;
|
||||
import cn.daxpay.open.platform.core.annotation.IgnoreAuth;
|
||||
import cn.daxpay.open.platform.core.rest.Res;
|
||||
import cn.daxpay.open.platform.core.rest.result.Result;
|
||||
import cn.daxpay.open.platform.iam.result.social.SocialBindResult;
|
||||
import cn.daxpay.open.platform.iam.result.social.SocialEnabledPlatformResult;
|
||||
import cn.daxpay.open.platform.iam.result.social.SocialExchangeResult;
|
||||
import cn.daxpay.open.platform.iam.service.social.SocialLoginService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/// # 第三方社交登录端点
|
||||
///
|
||||
/// 瘦控制器, 仅负责 HTTP 参数解包与结果包装, 业务编排全部委托 [SocialLoginService].
|
||||
/// 采用前端回调模式: 第三方平台直接重定向到前端回调页, 前端拿到 code+state 后
|
||||
/// 调用 exchange 接口完成换 token, 后端不做 302 跳转.
|
||||
///
|
||||
@IgnoreAuth
|
||||
@Tag(name = "第三方社交登录")
|
||||
@RestController
|
||||
@RequestMapping("/social")
|
||||
@RequiredArgsConstructor
|
||||
public class SocialEndpoint {
|
||||
|
||||
private final SocialLoginService socialLoginService;
|
||||
|
||||
/// 查询已启用的第三方登录平台(登录页公开接口)
|
||||
/// 仅返回平台编码列表, 不含任何敏感字段, 供登录页动态渲染第三方登录按钮.
|
||||
@Operation(summary = "查询已启用的第三方登录平台")
|
||||
@GetMapping("/enabled-list")
|
||||
public Result<List<SocialEnabledPlatformResult>> enabledList() {
|
||||
return Res.ok(socialLoginService.enabledList());
|
||||
}
|
||||
|
||||
/// 生成授权地址
|
||||
/// @param source 平台来源
|
||||
/// @param client 终端编码(admin/merchant), 用于解析端点配置中的 baseUrl
|
||||
/// @param mode 授权场景(不传则按登录态判断: 已登录=绑定, 未登录=登录)
|
||||
/// @param redirect 成功后前端跳转路径(可选)
|
||||
@Operation(summary = "生成授权地址")
|
||||
@GetMapping("/render/{source}")
|
||||
public Result<String> render(@PathVariable String source,
|
||||
@RequestParam String client,
|
||||
@RequestParam(required = false) String mode,
|
||||
@RequestParam(required = false) String redirect) {
|
||||
return Res.ok(socialLoginService.generateAuthorizeUrl(source, client, mode, redirect));
|
||||
}
|
||||
|
||||
/// OAuth 授权码兑换(前端回调模式)
|
||||
/// 前端回调页收到第三方平台的 code+state 后调用此接口,
|
||||
/// 后端完成 code 换 token 并返回结果 JSON.
|
||||
@Operation(summary = "授权码兑换")
|
||||
@PostMapping("/exchange")
|
||||
public Result<SocialExchangeResult> exchange(@RequestParam("code") String code,
|
||||
@RequestParam("state") String state,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
return Res.ok(socialLoginService.exchangeCode(code, state, request, response));
|
||||
}
|
||||
|
||||
/// 查询当前登录用户已绑定的第三方账号
|
||||
@IgnoreAuth(login = true)
|
||||
@Operation(summary = "已绑定的第三方账号列表")
|
||||
@GetMapping("/bind/list")
|
||||
public Result<List<SocialBindResult>> bindList() {
|
||||
return Res.ok(socialLoginService.bindList(SecurityUtil.getUserId()));
|
||||
}
|
||||
|
||||
/// 解除当前登录用户的指定平台绑定
|
||||
@IgnoreAuth(login = true)
|
||||
@Operation(summary = "解除第三方账号绑定")
|
||||
@PostMapping("/unbind")
|
||||
public Result<Void> unbind(@RequestParam String source) {
|
||||
socialLoginService.unbind(SecurityUtil.getUserId(), source);
|
||||
return Res.ok();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
package cn.daxpay.open.platform.iam.endpoint.social;
|
||||
package cn.daxpay.open.platform.iam.service.social;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import cn.daxpay.open.platform.capability.auth.util.SecurityUtil;
|
||||
import cn.daxpay.open.platform.capability.social.auth.SocialAuthRequestFactory;
|
||||
@@ -7,53 +9,39 @@ import cn.daxpay.open.platform.capability.social.justauth.SocialSourceEnum;
|
||||
import cn.daxpay.open.platform.capability.social.justauth.model.AuthCallback;
|
||||
import cn.daxpay.open.platform.capability.social.justauth.model.AuthUser;
|
||||
import cn.daxpay.open.platform.capability.social.justauth.request.SocialAuthRequest;
|
||||
import cn.daxpay.open.platform.core.annotation.IgnoreAuth;
|
||||
import cn.daxpay.open.platform.core.exception.operation.OperationFailException;
|
||||
import cn.daxpay.open.platform.core.rest.Res;
|
||||
import cn.daxpay.open.platform.core.rest.result.Result;
|
||||
import cn.daxpay.open.platform.iam.entity.social.SocialConfig;
|
||||
import cn.daxpay.open.platform.iam.enums.SocialClientEnum;
|
||||
import cn.daxpay.open.platform.iam.result.social.SocialBindResult;
|
||||
import cn.daxpay.open.platform.iam.result.social.SocialEnabledPlatformResult;
|
||||
import cn.daxpay.open.platform.iam.result.social.SocialExchangeResult;
|
||||
import cn.daxpay.open.platform.iam.service.social.IamSocialLoginHandler;
|
||||
import cn.daxpay.open.platform.iam.service.social.IamUserSocialBindStore;
|
||||
import cn.daxpay.open.platform.iam.service.social.SocialConfigService;
|
||||
import cn.daxpay.open.platform.iam.service.social.cache.RedisSocialStateCache;
|
||||
import cn.daxpay.open.platform.iam.service.social.cache.SocialAuthContext;
|
||||
import cn.daxpay.open.platform.iam.service.social.cache.SocialAuthMode;
|
||||
import cn.daxpay.open.platform.system.entity.config.platform.PlatformUrlConfig;
|
||||
import cn.daxpay.open.platform.iam.enums.SocialClientEnum;
|
||||
import cn.daxpay.open.platform.system.service.config.PlatformUrlConfigService;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// # 第三方社交登录端点
|
||||
/// # 第三方社交登录编排服务
|
||||
///
|
||||
/// 提供 OAuth2 授权(render)、授权码兑换(exchange)、绑定管理(bind list/unbind)等接口.
|
||||
/// 编排 OAuth2 授权流程: 授权地址生成(render)、授权码兑换(exchange)、
|
||||
/// 绑定关系查询与解绑. 由 [SocialEndpoint] 作为瘦控制器入口调用,
|
||||
/// 数据访问与配置管理分别委托 [IamUserSocialBindStore] / [SocialConfigService],
|
||||
/// Sa-Token 签发委托 [IamSocialLoginHandler].
|
||||
/// 采用前端回调模式: 第三方平台直接重定向到前端回调页, 前端拿到 code+state 后
|
||||
/// 调用 exchange API 完成换 token, 后端不做 302 跳转.
|
||||
/// 调用 [exchangeCode] 完成换 token, 后端不做 302 跳转.
|
||||
/// state 超时使用系统默认常量.
|
||||
///
|
||||
@Slf4j
|
||||
@IgnoreAuth
|
||||
@Tag(name = "第三方社交登录")
|
||||
@RestController
|
||||
@RequestMapping("/social")
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SocialEndpoint {
|
||||
public class SocialLoginService {
|
||||
|
||||
/// state 缓存超时时间(秒), 用户完成第三方授权的合理等待时长
|
||||
private static final long STATE_TIMEOUT_SECONDS = 300L;
|
||||
@@ -72,10 +60,8 @@ public class SocialEndpoint {
|
||||
|
||||
/// 查询已启用的第三方登录平台(登录页公开接口)
|
||||
/// 仅返回平台编码列表, 不含任何敏感字段, 供登录页动态渲染第三方登录按钮.
|
||||
@Operation(summary = "查询已启用的第三方登录平台")
|
||||
@GetMapping("/enabled-list")
|
||||
public Result<List<SocialEnabledPlatformResult>> enabledList() {
|
||||
return Res.ok(socialConfigService.findEnabledList());
|
||||
public List<SocialEnabledPlatformResult> enabledList() {
|
||||
return socialConfigService.findEnabledList();
|
||||
}
|
||||
|
||||
/// 生成授权地址并缓存上下文(前端拿到后跳转)
|
||||
@@ -83,27 +69,16 @@ public class SocialEndpoint {
|
||||
/// @param client 终端编码(admin/merchant), 用于解析端点配置中的 baseUrl
|
||||
/// @param mode 授权场景(不传则按登录态判断: 已登录=绑定, 未登录=登录)
|
||||
/// @param redirect 成功后前端跳转路径(可选)
|
||||
@Operation(summary = "生成授权地址")
|
||||
@GetMapping("/render/{source}")
|
||||
public Result<String> render(
|
||||
@PathVariable String source,
|
||||
@RequestParam String client,
|
||||
@RequestParam(required = false) String mode,
|
||||
@RequestParam(required = false) String redirect) {
|
||||
public String generateAuthorizeUrl(String source, String client, String mode, String redirect) {
|
||||
// 加载平台配置(全局唯一)
|
||||
var config = this.socialConfigService.findEnabledBySource(source);
|
||||
if (config == null) {
|
||||
// 社交登录: 平台未配置或未启用
|
||||
throw new OperationFailException("error.social.configNotExist");
|
||||
}
|
||||
SocialConfig config = this.loadEnabledConfig(source);
|
||||
SocialSourceEnum socialSource = SocialSourceEnum.of(source);
|
||||
if (socialSource == null) {
|
||||
// 社交登录: 不支持的平台
|
||||
throw new OperationFailException("error.social.unsupportedSource");
|
||||
}
|
||||
// 按 client 解析前端 baseUrl(用于 redirectUri 自动生成)
|
||||
PlatformUrlConfig urlConfig = platformUrlConfigService.getUrlConfig();
|
||||
String baseUrl = SocialClientEnum.of(client).resolveBaseUrl(urlConfig);
|
||||
String baseUrl = this.resolveBaseUrl(client);
|
||||
// 回调地址由端点配置的 baseUrl 自动生成, 必须配置 baseUrl
|
||||
if (StrUtil.isBlank(baseUrl)) {
|
||||
// 社交登录: 端点配置缺失
|
||||
@@ -125,37 +100,30 @@ public class SocialEndpoint {
|
||||
// 构建授权请求并生成授权地址
|
||||
SocialAuthConfig authConfig = socialConfigService.buildAuthConfig(config, baseUrl);
|
||||
SocialAuthRequest request = socialAuthRequestFactory.create(socialSource, authConfig);
|
||||
String authorizeUrl = request.authorize(state);
|
||||
return Res.ok(authorizeUrl);
|
||||
return request.authorize(state);
|
||||
}
|
||||
|
||||
/// OAuth 授权码兑换(前端回调模式)
|
||||
/// 前端回调页收到第三方平台的 code+state 后调用此接口,
|
||||
/// 前端回调页收到第三方平台的 code+state 后调用此方法,
|
||||
/// 后端完成 code 换 token 并返回结果 JSON.
|
||||
@Operation(summary = "授权码兑换")
|
||||
@PostMapping("/exchange")
|
||||
public Result<SocialExchangeResult> exchange(
|
||||
@RequestParam("code") String code,
|
||||
@RequestParam("state") String state,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
public SocialExchangeResult exchangeCode(String code, String state,
|
||||
HttpServletRequest request, HttpServletResponse response) {
|
||||
// 校验 state 并恢复上下文
|
||||
SocialAuthContext context = redisSocialStateCache.getAndRemove(state);
|
||||
if (context == null) {
|
||||
// state 已过期或非法
|
||||
return Res.ok(new SocialExchangeResult().setError("state_invalid"));
|
||||
return new SocialExchangeResult().setError("state_invalid");
|
||||
}
|
||||
String source = context.getSource();
|
||||
String clientCode = context.getClientCode();
|
||||
try {
|
||||
// 加载平台配置 + 创建对应 Request
|
||||
cn.daxpay.open.platform.iam.entity.social.SocialConfig config = socialConfigService.findEnabledBySource(source);
|
||||
SocialConfig config = socialConfigService.findEnabledBySource(source);
|
||||
if (config == null) {
|
||||
return Res.ok(new SocialExchangeResult().setError("oauth_failed"));
|
||||
return new SocialExchangeResult().setError("oauth_failed");
|
||||
}
|
||||
// exchange 阶段的 redirect_uri 必须与 authorize 阶段一致, 从端点配置按 client 解析 baseUrl
|
||||
PlatformUrlConfig urlConfig = platformUrlConfigService.getUrlConfig();
|
||||
String baseUrl = SocialClientEnum.of(clientCode).resolveBaseUrl(urlConfig);
|
||||
String baseUrl = this.resolveBaseUrl(clientCode);
|
||||
SocialAuthConfig authConfig = socialConfigService.buildAuthConfig(config, baseUrl);
|
||||
SocialSourceEnum socialSource = SocialSourceEnum.of(source);
|
||||
SocialAuthRequest authRequest = socialAuthRequestFactory.create(socialSource, authConfig);
|
||||
@@ -163,44 +131,31 @@ public class SocialEndpoint {
|
||||
// 按场景处理
|
||||
if (context.getMode() == SocialAuthMode.BIND) {
|
||||
socialBindStore.saveBind(context.getUserId(), clientCode, authUser);
|
||||
return Res.ok(new SocialExchangeResult().setResult("bind_success"));
|
||||
return new SocialExchangeResult().setResult("bind_success");
|
||||
} else {
|
||||
// LOGIN 场景: 仅已绑定的账号可直接登录
|
||||
Long userId = socialBindStore.findUserIdBySourceAndOpenId(source, authUser.getUuid()).orElse(null);
|
||||
if (userId == null) {
|
||||
// 未绑定
|
||||
return Res.ok(new SocialExchangeResult().setError("unbind"));
|
||||
return new SocialExchangeResult().setError("unbind");
|
||||
}
|
||||
String token = socialLoginHandler.login(userId, clientCode, request, response);
|
||||
return Res.ok(new SocialExchangeResult().setToken(token));
|
||||
return new SocialExchangeResult().setToken(token);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("社交登录兑换失败: source={}, msg={}", source, e.getMessage(), e);
|
||||
return Res.ok(new SocialExchangeResult().setError("oauth_failed"));
|
||||
return new SocialExchangeResult().setError("oauth_failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// 查询当前登录用户已绑定的第三方账号
|
||||
@IgnoreAuth(login = true)
|
||||
@Operation(summary = "已绑定的第三方账号列表")
|
||||
@GetMapping("/bind/list")
|
||||
public Result<List<SocialBindResult>> bindList() {
|
||||
Long userId = SecurityUtil.getUserId();
|
||||
return Res.ok(socialBindStore.findBindsByUserId(userId));
|
||||
/// 查询指定用户已绑定的所有第三方账号
|
||||
public List<SocialBindResult> bindList(Long userId) {
|
||||
return socialBindStore.findBindsByUserId(userId);
|
||||
}
|
||||
|
||||
/// 解除当前登录用户的指定平台绑定
|
||||
@IgnoreAuth(login = true)
|
||||
@Operation(summary = "解除第三方账号绑定")
|
||||
@PostMapping("/unbind")
|
||||
public Result<Void> unbind(@RequestParam String source) {
|
||||
Long userId = SecurityUtil.getUserId();
|
||||
boolean success = socialBindStore.removeBind(userId, source);
|
||||
if (!success) {
|
||||
// 社交登录: 未绑定该平台, 无需解绑
|
||||
return Res.ok();
|
||||
}
|
||||
return Res.ok();
|
||||
/// 解除指定用户的某个平台绑定(幂等: 未绑定则无操作)
|
||||
public void unbind(Long userId, String source) {
|
||||
socialBindStore.removeBind(userId, source);
|
||||
}
|
||||
|
||||
/// 解析授权场景(未传 mode 时按登录态判断)
|
||||
@@ -215,4 +170,20 @@ public class SocialEndpoint {
|
||||
boolean login = SecurityUtil.isLogin();
|
||||
return login ? SocialAuthMode.BIND : SocialAuthMode.LOGIN;
|
||||
}
|
||||
|
||||
/// 按 client 解析前端 baseUrl(用于 redirectUri 自动生成)
|
||||
private String resolveBaseUrl(String clientCode) {
|
||||
PlatformUrlConfig urlConfig = platformUrlConfigService.getUrlConfig();
|
||||
return SocialClientEnum.of(clientCode).resolveBaseUrl(urlConfig);
|
||||
}
|
||||
|
||||
/// 加载已启用的平台配置(不存在则抛业务异常)
|
||||
private SocialConfig loadEnabledConfig(String source) {
|
||||
SocialConfig config = socialConfigService.findEnabledBySource(source);
|
||||
if (config == null) {
|
||||
// 社交登录: 平台未配置或未启用
|
||||
throw new OperationFailException("error.social.configNotExist");
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user