refactor: 社交登录架构重构与端点配置功能

- capability-social 业务层(config/endpoint/cache/bind/login)迁移至 service-iam
- SPI 接口(SocialBindStore/SocialLoginHandler)合并到实现类, capability-social 精简为纯协议层
- service-system 配置模块拆分为 Oss/Security/Url 三个独立子模块
- 社交登录回调地址改由端点配置自动生成, 移除 redirectUri 手动配置
- 修复 exchange 阶段 redirect_uri 与 authorize 阶段不一致问题
This commit is contained in:
DaxPay Dev
2026-06-21 21:44:05 +08:00
parent 8ffbdd2865
commit c4c115052d
54 changed files with 640 additions and 310 deletions

View File

@@ -18,3 +18,7 @@ ALTER TABLE iam_social_config DROP COLUMN IF EXISTS frontend_callback_url;
-- 字段长度 VARCHAR(256) 可容纳密文(appSecret 多为 32~64 字符, 密文 < 200 字符), 无需调整
-- 开发阶段无历史明文数据, 跳过迁移; 生产环境启用前需写一次性逻辑加密历史明文
COMMENT ON COLUMN iam_social_config.client_secret IS '客户端密钥(加密存储)';
-- 回调地址不再由社交配置维护, 改由端点配置(PlatformUrlConfig)的 baseUrl 自动生成
-- 实际回调地址为 {adminBaseUrl|merchantBaseUrl}/auth/oauth-callback/{source}
ALTER TABLE iam_social_config DROP COLUMN IF EXISTS redirect_uri;

View File

@@ -10,7 +10,7 @@
</parent>
<artifactId>capability-social</artifactId>
<description>第三方社交登录能力模块(参考JustAuth重新实现)</description>
<description>第三方社交登录通用 OAuth 协议能力模块(参考JustAuth重新实现)</description>
<dependencies>
<!-- web -->
@@ -62,12 +62,6 @@
<artifactId>common-config</artifactId>
<version>${project.version}</version>
</dependency>
<!-- mybatis-plus (社交平台配置表) -->
<dependency>
<groupId>cn.daxpay.open</groupId>
<artifactId>common-mybatis-plus</artifactId>
<version>${project.version}</version>
</dependency>
<!-- hutool http (OAuth2 接口调用) -->
<dependency>
<groupId>cn.hutool</groupId>

View File

@@ -1,18 +1,16 @@
package cn.daxpay.open.platform.capability.social;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.ComponentScan;
/// # 第三方社交登录能力模块自动配置
///
/// 仅负责通用 OAuth 协议层的组件扫描(justauth 包下的 Request/Factory).
/// 业务部分(配置管理、SocialEndpoint、绑定关系等)已迁移至 service-iam.
///
@Slf4j
@AutoConfiguration
@ComponentScan
@ConfigurationPropertiesScan
@MapperScan(annotationClass = Mapper.class)
public class SocialAutoConfiguration {
}

View File

@@ -9,40 +9,19 @@ import cn.daxpay.open.platform.capability.social.justauth.request.QqRequest;
import cn.daxpay.open.platform.capability.social.justauth.request.SocialAuthRequest;
import cn.daxpay.open.platform.capability.social.justauth.request.WeComRequest;
import cn.daxpay.open.platform.capability.social.justauth.request.WechatMpRequest;
import cn.daxpay.open.platform.capability.social.config.entity.SocialConfig;
import cn.daxpay.open.platform.capability.social.config.service.SocialConfigService;
import cn.daxpay.open.platform.capability.social.justauth.SocialAuthConfig;
import cn.daxpay.open.platform.capability.social.justauth.SocialSourceEnum;
import cn.daxpay.open.platform.core.exception.operation.OperationFailException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/// # 社交授权请求工厂
///
/// 根据平台来源构建对应的 SocialAuthRequest, 配置从 iam_social_config 表按终端加载
/// 仅负责按平台枚举创建对应的 [SocialAuthRequest] 实现.
/// 配置加载由调用方(SocialEndpoint)通过 SocialConfigService 完成, 工厂不再耦合业务配置.
///
@Component
@RequiredArgsConstructor
public class SocialAuthRequestFactory {
private final SocialConfigService socialConfigService;
/// 根据平台构建授权请求(自动加载已配置且启用的配置)
public SocialAuthRequest build(String sourceName) {
SocialSourceEnum source = SocialSourceEnum.of(sourceName);
if (source == null) {
// 社交登录: 不支持的平台
throw new OperationFailException("error.social.unsupportedSource");
}
SocialConfig config = socialConfigService.findEnabledBySource(sourceName);
if (config == null) {
// 社交登录: 平台未配置或未启用
throw new OperationFailException("error.social.configNotExist");
}
SocialAuthConfig authConfig = socialConfigService.buildAuthConfig(config);
return this.create(source, authConfig);
}
/// 根据平台来源创建对应的请求实现
public SocialAuthRequest create(SocialSourceEnum source, SocialAuthConfig config) {
return switch (source) {
@@ -56,4 +35,14 @@ public class SocialAuthRequestFactory {
case DOUYIN -> new DouyinRequest(config);
};
}
/// 根据平台来源创建对应的请求实现, 平台不支持时抛错
public SocialAuthRequest create(String sourceName, SocialAuthConfig config) {
SocialSourceEnum source = SocialSourceEnum.of(sourceName);
if (source == null) {
// 社交登录: 不支持的平台
throw new OperationFailException("error.social.unsupportedSource");
}
return this.create(source, config);
}
}

View File

@@ -1,34 +0,0 @@
package cn.daxpay.open.platform.capability.social.bind;
import cn.daxpay.open.platform.capability.social.bind.result.SocialBindResult;
import cn.daxpay.open.platform.capability.social.justauth.model.AuthUser;
import java.util.List;
import java.util.Optional;
/// # 社交账号绑定存储接口
///
/// capability-social 层只定义绑定关系的访问契约, 具体实现由 service-iam 提供(操作 iam_user_social 表),
/// 以保证依赖方向: 业务服务层依赖能力层, 而非相反
///
public interface SocialBindStore {
/// 根据平台来源和平台用户标识查询绑定的本地用户ID
Optional<Long> findUserIdBySourceAndOpenId(String source, String openId);
/// 判断指定平台账号是否已被绑定
boolean existsBind(String source, String openId);
/// 保存绑定关系
/// @param userId 本地用户ID
/// @param clientCode 终端编码
/// @param authUser 平台返回的用户信息
void saveBind(Long userId, String clientCode, AuthUser authUser);
/// 查询指定用户已绑定的所有第三方账号
List<SocialBindResult> findBindsByUserId(Long userId);
/// 解除指定用户的某个平台绑定
/// @return 是否解绑成功
boolean removeBind(Long userId, String source);
}

View File

@@ -16,7 +16,7 @@ import java.util.Arrays;
public enum SocialSourceEnum implements I18nSupport {
/// 微信公众号
/// 微信开放平台
WECHAT_MP(
"weChat",
"https://open.weixin.qq.com/connect/oauth2/authorize",

View File

@@ -40,7 +40,7 @@ public abstract class AbstractSocialAuthRequest implements SocialAuthRequest {
return SocialUrlBuilder.ofBaseUrl(source.authorize())
.queryParam("response_type", "code")
.queryParam("client_id", config.getClientId())
.queryParam("redirect_uri", config.getRedirectUri())
.queryParam("redirect_uri", this.buildRedirectUri())
.queryParam("state", state)
.build();
}
@@ -70,7 +70,7 @@ public abstract class AbstractSocialAuthRequest implements SocialAuthRequest {
.queryParam("client_id", config.getClientId())
.queryParam("client_secret", config.getClientSecret())
.queryParam("grant_type", "authorization_code")
.queryParam("redirect_uri", config.getRedirectUri())
.queryParam("redirect_uri", this.buildRedirectUri())
.build();
}
@@ -81,6 +81,24 @@ public abstract class AbstractSocialAuthRequest implements SocialAuthRequest {
.build();
}
/// 构建回调地址(配置基础路径 + 平台编码, 形如 .../oauth-callback/{source})
/// authorize 与 accessToken 两处必须一致, 集中在此方法保证.
/// 配置项 redirect_uri 约定为前端回调基础路径(不含 source), 如:
/// http://127.0.0.1:13333/auth/oauth-callback
/// 实际传给第三方的为:
/// http://127.0.0.1:13333/auth/oauth-callback/gitee
protected String buildRedirectUri() {
String base = config.getRedirectUri();
if (base == null) {
base = "";
}
// 去掉末尾斜杠, 避免出现 //gitee
if (base.endsWith("/")) {
base = base.substring(0, base.length() - 1);
}
return base + "/" + source.getCode();
}
/// GET 请求
protected String doGet(String url) {
return this.doGet(url, null);

View File

@@ -23,7 +23,7 @@ public class DingTalkRequest extends AbstractSocialAuthRequest {
return SocialUrlBuilder.ofBaseUrl(this.getSource().authorize())
.queryParam("response_type", "code")
.queryParam("client_id", this.getConfig().getClientId())
.queryParam("redirect_uri", this.getConfig().getRedirectUri())
.queryParam("redirect_uri", this.buildRedirectUri())
.queryParam("prompt", "consent")
.queryParam("state", state)
.build();

View File

@@ -26,7 +26,7 @@ public class DouyinRequest extends AbstractSocialAuthRequest {
.queryParam("client_key", this.getConfig().getClientId())
.queryParam("response_type", "code")
.queryParam("scope", "user_info")
.queryParam("redirect_uri", this.getConfig().getRedirectUri())
.queryParam("redirect_uri", this.buildRedirectUri())
.queryParam("state", state)
.build();
}

View File

@@ -25,7 +25,7 @@ public class FeishuRequest extends AbstractSocialAuthRequest {
public String authorize(String state) {
return SocialUrlBuilder.ofBaseUrl(this.getSource().authorize())
.queryParam("app_id", this.getConfig().getClientId())
.queryParam("redirect_uri", this.encode(this.getConfig().getRedirectUri()))
.queryParam("redirect_uri", this.encode(this.buildRedirectUri()))
.queryParam("state", state)
.build();
}

View File

@@ -23,7 +23,7 @@ public class WeComRequest extends AbstractSocialAuthRequest {
return SocialUrlBuilder.ofBaseUrl(this.getSource().authorize())
.queryParam("appid", this.getConfig().getClientId())
.queryParam("agentid", this.getConfig().getAgentId())
.queryParam("redirect_uri", this.getConfig().getRedirectUri())
.queryParam("redirect_uri", this.buildRedirectUri())
.queryParam("state", state)
.build();
}

View File

@@ -23,7 +23,7 @@ public class WechatMpRequest extends AbstractSocialAuthRequest {
public String authorize(String state) {
return SocialUrlBuilder.ofBaseUrl(this.getSource().authorize())
.queryParam("appid", this.getConfig().getClientId())
.queryParam("redirect_uri", this.encode(this.getConfig().getRedirectUri()))
.queryParam("redirect_uri", this.encode(this.buildRedirectUri()))
.queryParam("response_type", "code")
.queryParam("scope", "snsapi_userinfo")
.queryParam("state", state.concat("#wechat_redirect"))

View File

@@ -1,17 +0,0 @@
package cn.daxpay.open.platform.capability.social.login;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/// # 社交登录处理器接口
///
/// capability-social 层在 LOGIN 场景下通过绑定关系确认了用户身份后, 委托 service-iam 完成实际的登录签发,
/// 以保证 session 中的 UserDetail 完整(复用平台既有登录成功流程)
///
public interface SocialLoginHandler {
/// 使用已确认身份的 userId 完成登录(含 session 填充与登录成功回调), 返回 token
/// @param userId 本地用户ID
/// @param clientCode 终端编码
String login(Long userId, String clientCode, HttpServletRequest request, HttpServletResponse response);
}

View File

@@ -1,7 +1,7 @@
-- 第三方平台登录配置表
-- 配置页采用"枚举驱动 + 读时初始化"模式: 首次访问时为每个 SocialSource 枚举平台
-- 插入占位记录(configured=false, 业务字段留空), 用户保存配置后才置 configured=true.
-- 因此 client_id/client_secret/redirect_uri 等业务字段允许为空.
-- 因此 client_id/client_secret 等业务字段允许为空.
DROP TABLE IF EXISTS iam_social_config;
CREATE TABLE IF NOT EXISTS iam_social_config (
id BIGINT NOT NULL,
@@ -14,7 +14,6 @@ CREATE TABLE IF NOT EXISTS iam_social_config (
source VARCHAR(32) NOT NULL,
client_id VARCHAR(128),
client_secret VARCHAR(256),
redirect_uri VARCHAR(256),
extra JSONB DEFAULT '{}'::jsonb,
configured BOOLEAN DEFAULT FALSE,
enabled BOOLEAN DEFAULT FALSE,
@@ -33,7 +32,6 @@ COMMENT ON COLUMN iam_social_config.deleted IS '逻辑删除';
COMMENT ON COLUMN iam_social_config.source IS '平台编码';
COMMENT ON COLUMN iam_social_config.client_id IS '客户端ID';
COMMENT ON COLUMN iam_social_config.client_secret IS '客户端密钥';
COMMENT ON COLUMN iam_social_config.redirect_uri IS '回调地址';
COMMENT ON COLUMN iam_social_config.extra IS '平台特有配置';
COMMENT ON COLUMN iam_social_config.configured IS '是否已完成配置';
COMMENT ON COLUMN iam_social_config.enabled IS '是否启用';

View File

@@ -15,9 +15,6 @@
"clientSecret": {
"notBlank": "Client secret cannot be blank"
},
"redirectUri": {
"notBlank": "Redirect URI cannot be blank"
},
"clientCode": {
"notBlank": "Client code cannot be blank"
},

View File

@@ -1,5 +1,5 @@
{
"weChat": "微信公众号",
"weChat": "微信开放平台",
"weCom": "企业微信",
"qq": "QQ",
"github": "GitHub",

View File

@@ -15,9 +15,6 @@
"clientSecret": {
"notBlank": "客户端密钥不能为空"
},
"redirectUri": {
"notBlank": "回调地址不能为空"
},
"clientCode": {
"notBlank": "终端不能为空"
},

View File

@@ -3,7 +3,7 @@ package cn.daxpay.open.platform.iam.auth.service;
import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformLoginSecurityConfig;
import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformPasswordPolicyConfig;
import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformSessionManagementConfig;
import cn.daxpay.open.platform.system.service.config.PlatformConfigService;
import cn.daxpay.open.platform.system.service.config.PlatformSecurityConfigService;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -15,21 +15,21 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor
public class IamSecurityConfigService {
private final PlatformConfigService platformConfigService;
private final PlatformSecurityConfigService platformSecurityConfigService;
/// 获取密码策略配置
public PlatformPasswordPolicyConfig getPasswordPolicy() {
return platformConfigService.getPasswordPolicyConfig();
return platformSecurityConfigService.getPasswordPolicyConfig();
}
/// 获取登录安全配置
public PlatformLoginSecurityConfig getLoginSecurity() {
return platformConfigService.getLoginSecurityConfig();
return platformSecurityConfigService.getLoginSecurityConfig();
}
/// 获取会话管理配置
public PlatformSessionManagementConfig getSessionManagement() {
return platformConfigService.getSessionManagementConfig();
return platformSecurityConfigService.getSessionManagementConfig();
}
}

View File

@@ -1,8 +1,8 @@
package cn.daxpay.open.platform.capability.social.config.controller;
package cn.daxpay.open.platform.iam.controller.social;
import cn.daxpay.open.platform.capability.social.config.param.SocialConfigParam;
import cn.daxpay.open.platform.capability.social.config.result.SocialConfigResult;
import cn.daxpay.open.platform.capability.social.config.service.SocialConfigService;
import cn.daxpay.open.platform.iam.param.social.SocialConfigParam;
import cn.daxpay.open.platform.iam.result.social.SocialConfigResult;
import cn.daxpay.open.platform.iam.service.social.SocialConfigService;
import cn.daxpay.open.platform.core.annotation.PermCode;
import cn.daxpay.open.platform.core.rest.Res;
import cn.daxpay.open.platform.core.rest.result.Result;

View File

@@ -1,11 +1,11 @@
package cn.daxpay.open.platform.capability.social.config.convert;
package cn.daxpay.open.platform.iam.convert.social;
import java.util.Collections;
import java.util.Map;
import cn.daxpay.open.platform.capability.social.config.entity.SocialConfig;
import cn.daxpay.open.platform.capability.social.config.param.SocialConfigParam;
import cn.daxpay.open.platform.capability.social.config.result.SocialConfigResult;
import cn.daxpay.open.platform.iam.entity.social.SocialConfig;
import cn.daxpay.open.platform.iam.param.social.SocialConfigParam;
import cn.daxpay.open.platform.iam.result.social.SocialConfigResult;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import org.mapstruct.Mapper;

View File

@@ -1,9 +1,9 @@
package cn.daxpay.open.platform.capability.social.config.dao;
package cn.daxpay.open.platform.iam.dao.social;
import java.util.List;
import java.util.Optional;
import cn.daxpay.open.platform.capability.social.config.entity.SocialConfig;
import cn.daxpay.open.platform.iam.entity.social.SocialConfig;
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
import org.springframework.stereotype.Repository;

View File

@@ -1,6 +1,6 @@
package cn.daxpay.open.platform.capability.social.config.dao;
package cn.daxpay.open.platform.iam.dao.social;
import cn.daxpay.open.platform.capability.social.config.entity.SocialConfig;
import cn.daxpay.open.platform.iam.entity.social.SocialConfig;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;

View File

@@ -1,23 +1,28 @@
package cn.daxpay.open.platform.capability.social.endpoint;
package cn.daxpay.open.platform.iam.endpoint.social;
import cn.daxpay.open.platform.capability.auth.util.SecurityUtil;
import cn.daxpay.open.platform.capability.social.auth.SocialAuthRequestFactory;
import cn.daxpay.open.platform.capability.social.bind.SocialBindStore;
import cn.daxpay.open.platform.capability.social.bind.result.SocialBindResult;
import cn.daxpay.open.platform.capability.social.cache.RedisSocialStateCache;
import cn.daxpay.open.platform.capability.social.cache.SocialAuthContext;
import cn.daxpay.open.platform.capability.social.cache.SocialAuthMode;
import cn.daxpay.open.platform.capability.social.config.entity.SocialConfig;
import cn.daxpay.open.platform.capability.social.config.service.SocialConfigService;
import cn.daxpay.open.platform.capability.social.justauth.SocialAuthConfig;
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.capability.social.login.SocialLoginHandler;
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.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;
@@ -35,13 +40,13 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/// # 第三方社交登录端点
///
/// 提供 OAuth2 授权(render)授权码兑换(exchange)绑定管理(bind list/unbind)等接口.
/// 采用前端回调模式: 第三方平台直接重定向到前端回调页, 前端拿到 code+state
/// 调用 exchange API 完成换 token, 后端不做 302 跳转.
/// state 超时使用系统默认常量.
///
/// # 第三方社交登录端点
///
/// 提供 OAuth2 授权(render)授权码兑换(exchange)绑定管理(bind list/unbind)等接口.
/// 采用前端回调模式: 第三方平台直接重定向到前端回调页, 前端拿到 code+state
/// 调用 exchange API 完成换 token, 后端不做 302 跳转.
/// state 超时使用系统默认常量.
///
@Slf4j
@IgnoreAuth
@Tag(name = "第三方社交登录")
@@ -57,15 +62,25 @@ public class SocialEndpoint {
private final RedisSocialStateCache redisSocialStateCache;
private final SocialBindStore socialBindStore;
private final IamUserSocialBindStore socialBindStore;
private final SocialLoginHandler socialLoginHandler;
private final IamSocialLoginHandler socialLoginHandler;
private final SocialConfigService socialConfigService;
private final PlatformUrlConfigService platformUrlConfigService;
/// 查询已启用的第三方登录平台(登录页公开接口)
/// 仅返回平台编码列表, 不含任何敏感字段, 供登录页动态渲染第三方登录按钮.
@Operation(summary = "查询已启用的第三方登录平台")
@GetMapping("/enabled-list")
public Result<List<SocialEnabledPlatformResult>> enabledList() {
return Res.ok(socialConfigService.findEnabledList());
}
/// 生成授权地址并缓存上下文(前端拿到后跳转)
/// @param source 平台来源
/// @param client 终端编码
/// @param client 终端编码(admin/merchant), 用于解析端点配置中的 baseUrl
/// @param mode 授权场景(不传则按登录态判断: 已登录=绑定, 未登录=登录)
/// @param redirect 成功后前端跳转路径(可选)
@Operation(summary = "生成授权地址")
@@ -76,7 +91,7 @@ public class SocialEndpoint {
@RequestParam(required = false) String mode,
@RequestParam(required = false) String redirect) {
// 加载平台配置(全局唯一)
SocialConfig config = socialConfigService.findEnabledBySource(source);
var config = this.socialConfigService.findEnabledBySource(source);
if (config == null) {
// 社交登录: 平台未配置或未启用
throw new OperationFailException("error.social.configNotExist");
@@ -86,6 +101,14 @@ public class SocialEndpoint {
// 社交登录: 不支持的平台
throw new OperationFailException("error.social.unsupportedSource");
}
// client 解析前端 baseUrl(用于 redirectUri 自动生成)
PlatformUrlConfig urlConfig = platformUrlConfigService.getUrlConfig();
String baseUrl = SocialClientEnum.of(client).resolveBaseUrl(urlConfig);
// 回调地址由端点配置的 baseUrl 自动生成, 必须配置 baseUrl
if (StrUtil.isBlank(baseUrl)) {
// 社交登录: 端点配置缺失
throw new OperationFailException("error.social.endpointNotConfigured");
}
SocialAuthMode authMode = this.resolveMode(mode);
// 构建 state 并缓存上下文(含平台来源, exchange 阶段使用)
String state = IdUtil.fastSimpleUUID();
@@ -96,11 +119,11 @@ public class SocialEndpoint {
.setSource(source);
if (authMode == SocialAuthMode.BIND) {
// 绑定场景必须已登录, 用户ID从登录态获取
context.setUserId(cn.daxpay.open.platform.capability.auth.util.SecurityUtil.getUserId());
context.setUserId(SecurityUtil.getUserId());
}
redisSocialStateCache.cache(state, context, STATE_TIMEOUT_SECONDS);
// 构建授权请求并生成授权地址
SocialAuthConfig authConfig = socialConfigService.buildAuthConfig(config);
SocialAuthConfig authConfig = socialConfigService.buildAuthConfig(config, baseUrl);
SocialAuthRequest request = socialAuthRequestFactory.create(socialSource, authConfig);
String authorizeUrl = request.authorize(state);
return Res.ok(authorizeUrl);
@@ -125,8 +148,17 @@ public class SocialEndpoint {
String source = context.getSource();
String clientCode = context.getClientCode();
try {
// 用授权码换取用户信息
SocialAuthRequest authRequest = socialAuthRequestFactory.build(source);
// 加载平台配置 + 创建对应 Request
cn.daxpay.open.platform.iam.entity.social.SocialConfig config = socialConfigService.findEnabledBySource(source);
if (config == null) {
return Res.ok(new SocialExchangeResult().setError("oauth_failed"));
}
// exchange 阶段的 redirect_uri 必须与 authorize 阶段一致, 从端点配置按 client 解析 baseUrl
PlatformUrlConfig urlConfig = platformUrlConfigService.getUrlConfig();
String baseUrl = SocialClientEnum.of(clientCode).resolveBaseUrl(urlConfig);
SocialAuthConfig authConfig = socialConfigService.buildAuthConfig(config, baseUrl);
SocialSourceEnum socialSource = SocialSourceEnum.of(source);
SocialAuthRequest authRequest = socialAuthRequestFactory.create(socialSource, authConfig);
AuthUser authUser = authRequest.login(AuthCallback.of(code, state));
// 按场景处理
if (context.getMode() == SocialAuthMode.BIND) {
@@ -153,7 +185,7 @@ public class SocialEndpoint {
@Operation(summary = "已绑定的第三方账号列表")
@GetMapping("/bind/list")
public Result<List<SocialBindResult>> bindList() {
Long userId = cn.daxpay.open.platform.capability.auth.util.SecurityUtil.getUserId();
Long userId = SecurityUtil.getUserId();
return Res.ok(socialBindStore.findBindsByUserId(userId));
}
@@ -162,7 +194,7 @@ public class SocialEndpoint {
@Operation(summary = "解除第三方账号绑定")
@PostMapping("/unbind")
public Result<Void> unbind(@RequestParam String source) {
Long userId = cn.daxpay.open.platform.capability.auth.util.SecurityUtil.getUserId();
Long userId = SecurityUtil.getUserId();
boolean success = socialBindStore.removeBind(userId, source);
if (!success) {
// 社交登录: 未绑定该平台, 无需解绑
@@ -180,7 +212,7 @@ public class SocialEndpoint {
}
}
// 默认: 已登录走绑定, 未登录走登录
boolean login = cn.daxpay.open.platform.capability.auth.util.SecurityUtil.isLogin();
boolean login = SecurityUtil.isLogin();
return login ? SocialAuthMode.BIND : SocialAuthMode.LOGIN;
}
}

View File

@@ -1,7 +1,7 @@
package cn.daxpay.open.platform.capability.social.config.entity;
package cn.daxpay.open.platform.iam.entity.social;
import cn.daxpay.open.platform.capability.social.config.convert.SocialConfigConvert;
import cn.daxpay.open.platform.capability.social.config.result.SocialConfigResult;
import cn.daxpay.open.platform.iam.convert.social.SocialConfigConvert;
import cn.daxpay.open.platform.iam.result.social.SocialConfigResult;
import cn.daxpay.open.platform.capability.social.justauth.SocialSourceEnum;
import cn.daxpay.open.platform.common.mybatisplus.base.MpBaseEntity;
import cn.daxpay.open.platform.common.mybatisplus.function.ToResult;
@@ -15,7 +15,8 @@ import lombok.experimental.Accessors;
/// # 第三方平台登录配置
///
/// 记录各社交平台(appId/appSecret/回调地址)的配置, 全局唯一( source 区分), 管理端可动态维护.
/// 记录各社交平台(appId/appSecret )的配置, 全局唯一( source 区分), 管理端可动态维护.
/// 回调地址不再单独配置, 由端点配置(PlatformUrlConfig) baseUrl 自动生成: {baseUrl}/auth/oauth-callback/{source}.
/// 平台特有参数(如企业微信 agentId)统一存放在 extra jsonb 字段, 避免表结构随平台扩展频繁变更.
/// `configured` 标识是否已完成配置: 配置页内存合并时缺失项为 false, 用户保存配置后才为 true.
///
@@ -37,9 +38,6 @@ public class SocialConfig extends MpBaseEntity implements ToResult<SocialConfigR
@TableField(typeHandler = DataEncryptTypeHandler.class)
private String clientSecret;
/// 回调地址
private String redirectUri;
/// 平台特有配置(如企业微信 agentId), jsonb 存储, 此处为原始 JSON 文本
@TableField(typeHandler = JsonbStringTypeHandler.class)
private String extra;

View File

@@ -0,0 +1,47 @@
package cn.daxpay.open.platform.iam.enums;
import java.util.Arrays;
import cn.daxpay.open.platform.system.entity.config.platform.PlatformUrlConfig;
import lombok.AllArgsConstructor;
import lombok.Getter;
/// # 社交登录终端编码
///
/// 用于按 client(admin/merchant) 解析端点配置中对应的前端 baseUrl.
/// 默认值 ADMIN: client 参数无法识别时回退到管理端配置.
///
@Getter
@AllArgsConstructor
public enum SocialClientEnum {
/// 运营管理端
ADMIN("admin") {
@Override
public String resolveBaseUrl(PlatformUrlConfig config) {
return config.getAdminBaseUrl();
}
},
/// 商户管理端
MERCHANT("merchant") {
@Override
public String resolveBaseUrl(PlatformUrlConfig config) {
return config.getMerchantBaseUrl();
}
};
/// 终端编码
private final String code;
/// 从端点配置中解析当前终端对应的 baseUrl
public abstract String resolveBaseUrl(PlatformUrlConfig config);
/// 根据编码查找枚举, 无法识别时回退到 ADMIN(容错)
public static SocialClientEnum of(String code) {
return Arrays.stream(values())
.filter(e -> e.code.equalsIgnoreCase(code))
.findFirst()
.orElse(ADMIN);
}
}

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.platform.capability.social.config.param;
package cn.daxpay.open.platform.iam.param.social;
import java.util.Map;
@@ -28,10 +28,6 @@ public class SocialConfigParam {
@Schema(description = "客户端密钥(编辑未修改不传, 修改传新值)")
private String clientSecret;
@Schema(description = "回调地址")
@NotBlank(message = "{validation.field.redirectUri.notBlank}")
private String redirectUri;
/// 平台特有配置(如企业微信 agentId)
@Schema(description = "平台特有配置(如企业微信 agentId)")
private Map<String, String> extra;

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.platform.capability.social.bind.result;
package cn.daxpay.open.platform.iam.result.social;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.platform.capability.social.config.result;
package cn.daxpay.open.platform.iam.result.social;
import java.util.Map;
@@ -28,9 +28,6 @@ public class SocialConfigResult {
@Schema(description = "客户端密钥(脱敏)")
private String clientSecret;
@Schema(description = "回调地址")
private String redirectUri;
/// 平台特有配置(如企业微信 agentId)
@Schema(description = "平台特有配置(如企业微信 agentId)")
private Map<String, String> extra;

View File

@@ -0,0 +1,21 @@
package cn.daxpay.open.platform.iam.result.social;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 已启用的第三方登录平台
///
/// 登录页未登录场景下的最小公开返回, 仅暴露平台编码(source),
/// 不含 clientId/clientSecret/redirectUri/extra 等任何敏感字段.
/// 平台显示名/图标/品牌色由前端本地映射表(socialEnum.ts)决定.
///
@Data
@Accessors(chain = true)
@Schema(title = "已启用的第三方登录平台")
public class SocialEnabledPlatformResult {
/// 平台编码(weChat/weCom/qq/github/gitee/feishu/dingTalk/douyin)
@Schema(description = "平台编码")
private String source;
}

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.platform.capability.social.endpoint;
package cn.daxpay.open.platform.iam.result.social;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

View File

@@ -1,6 +1,5 @@
package cn.daxpay.open.platform.iam.service.social;
import cn.daxpay.open.platform.capability.social.login.SocialLoginHandler;
import cn.daxpay.open.platform.core.code.CommonCode;
import cn.daxpay.open.platform.core.entity.UserDetail;
import cn.daxpay.open.platform.iam.result.user.UserInfoResult;
@@ -14,18 +13,21 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/// # 社交登录处理器实现
/// # 社交登录处理器
///
/// 在 LOGIN 场景下, 通过绑定关系确认用户身份后, 委托本类完成 Sa-Token 登录签发与 session 填充
/// 在 LOGIN 场景下, 通过绑定关系确认用户身份后, 完成本地 Sa-Token 登录签发与 session 填充.
/// 被 SocialEndpoint 直接注入使用(同模块, 无需 SPI 抽象).
///
@Slf4j
@Service
@RequiredArgsConstructor
public class IamSocialLoginHandler implements SocialLoginHandler {
public class IamSocialLoginHandler {
private final UserQueryService userQueryService;
@Override
/// 使用已确认身份的 userId 完成登录(含 session 填充), 返回 token
/// @param userId 本地用户ID
/// @param clientCode 终端编码
public String login(Long userId, String clientCode, HttpServletRequest request, HttpServletResponse response) {
// 加载用户信息并构建会话对象
UserInfoResult userInfo = userQueryService.findById(userId);

View File

@@ -1,7 +1,6 @@
package cn.daxpay.open.platform.iam.service.social;
import cn.daxpay.open.platform.capability.social.bind.SocialBindStore;
import cn.daxpay.open.platform.capability.social.bind.result.SocialBindResult;
import cn.daxpay.open.platform.iam.result.social.SocialBindResult;
import cn.daxpay.open.platform.capability.social.justauth.model.AuthUser;
import cn.daxpay.open.platform.iam.dao.social.IamUserSocialManager;
import cn.daxpay.open.platform.iam.entity.social.IamUserSocial;
@@ -14,19 +13,19 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
/// # 用户第三方账号绑定存储实现
/// # 用户第三方账号绑定存储
///
/// 实现 capability-social 定义的 SocialBindStore 契约, 操作 iam_user_social 表,
/// 被 SocialEndpoint 通过接口注入使用(无需 capability-social 反向依赖 service-iam)
/// 操作 iam_user_social 表, 提供"按平台+openId 查用户/保存绑定/列绑定/解绑"等能力,
/// 被 SocialEndpoint 直接注入使用(同模块, 无需 SPI 抽象).
///
@Slf4j
@Service
@RequiredArgsConstructor
public class IamUserSocialBindStore implements SocialBindStore {
public class IamUserSocialBindStore {
private final IamUserSocialManager iamUserSocialManager;
@Override
/// 根据平台来源和平台用户标识查询绑定的本地用户ID
public Optional<Long> findUserIdBySourceAndOpenId(String source, String openId) {
return iamUserSocialManager.lambdaQuery()
.eq(IamUserSocial::getSource, source)
@@ -35,7 +34,7 @@ public class IamUserSocialBindStore implements SocialBindStore {
.map(IamUserSocial::getUserId);
}
@Override
/// 判断指定平台账号是否已被绑定
public boolean existsBind(String source, String openId) {
return iamUserSocialManager.lambdaQuery()
.eq(IamUserSocial::getSource, source)
@@ -43,7 +42,10 @@ public class IamUserSocialBindStore implements SocialBindStore {
.exists();
}
@Override
/// 保存绑定关系
/// @param userId 本地用户ID
/// @param clientCode 终端编码
/// @param authUser 平台返回的用户信息
@Transactional(rollbackFor = Exception.class)
public void saveBind(Long userId, String clientCode, AuthUser authUser) {
String source = authUser.getSource();
@@ -84,7 +86,7 @@ public class IamUserSocialBindStore implements SocialBindStore {
iamUserSocialManager.save(entity);
}
@Override
/// 查询指定用户已绑定的所有第三方账号
public List<SocialBindResult> findBindsByUserId(Long userId) {
return iamUserSocialManager.lambdaQuery()
.eq(IamUserSocial::getUserId, userId)
@@ -93,7 +95,8 @@ public class IamUserSocialBindStore implements SocialBindStore {
.toList();
}
@Override
/// 解除指定用户的某个平台绑定
/// @return 是否解绑成功
public boolean removeBind(Long userId, String source) {
return iamUserSocialManager.lambdaUpdate()
.eq(IamUserSocial::getUserId, userId)

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.platform.capability.social.config.service;
package cn.daxpay.open.platform.iam.service.social;
import java.util.ArrayList;
import java.util.Arrays;
@@ -6,11 +6,12 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import cn.daxpay.open.platform.capability.social.config.convert.SocialConfigConvert;
import cn.daxpay.open.platform.capability.social.config.dao.SocialConfigManager;
import cn.daxpay.open.platform.capability.social.config.entity.SocialConfig;
import cn.daxpay.open.platform.capability.social.config.param.SocialConfigParam;
import cn.daxpay.open.platform.capability.social.config.result.SocialConfigResult;
import cn.daxpay.open.platform.iam.convert.social.SocialConfigConvert;
import cn.daxpay.open.platform.iam.dao.social.SocialConfigManager;
import cn.daxpay.open.platform.iam.entity.social.SocialConfig;
import cn.daxpay.open.platform.iam.param.social.SocialConfigParam;
import cn.daxpay.open.platform.iam.result.social.SocialConfigResult;
import cn.daxpay.open.platform.iam.result.social.SocialEnabledPlatformResult;
import cn.daxpay.open.platform.capability.social.justauth.SocialAuthConfig;
import cn.daxpay.open.platform.capability.social.justauth.SocialSourceEnum;
import cn.daxpay.open.platform.core.exception.operation.OperationFailException;
@@ -104,8 +105,24 @@ public class SocialConfigService {
return socialConfigManager.findEnabledBySource(source).orElse(null);
}
/// 查询所有已配置且启用的平台(登录页公开接口使用)
/// 仅返回平台编码(source), 不暴露任何敏感字段(clientId/clientSecret/redirectUri/extra ).
public List<SocialEnabledPlatformResult> findEnabledList() {
return socialConfigManager.findAllEnabled().stream()
.map(c -> new SocialEnabledPlatformResult().setSource(c.getSource()))
.toList();
}
/// 将配置实体转换为授权配置
public SocialAuthConfig buildAuthConfig(SocialConfig entity) {
/// @param baseUrl 端点解析出的前端基础地址, 用于 redirectUri 自动生成(exchange 阶段传 null 表示不需要 redirectUri)
public SocialAuthConfig buildAuthConfig(SocialConfig entity, String baseUrl) {
// 回调地址由端点配置自动生成: {baseUrl}/auth/oauth-callback(exchange 阶段 baseUrl null 时跳过)
String redirectUri = null;
if (StrUtil.isNotBlank(baseUrl)) {
// 末尾斜杠归一化由 AbstractSocialAuthRequest.buildRedirectUri 处理
String base = StrUtil.removeSuffix(baseUrl, "/");
redirectUri = base + "/auth/oauth-callback";
}
// 企业微信 agentId 等平台特有参数从 extra(jsonb) 读取
String agentId = null;
if (StrUtil.isNotBlank(entity.getExtra())) {
@@ -114,7 +131,7 @@ public class SocialConfigService {
return new SocialAuthConfig()
.setClientId(entity.getClientId())
.setClientSecret(entity.getClientSecret())
.setRedirectUri(entity.getRedirectUri())
.setRedirectUri(redirectUri)
.setAgentId(agentId);
}

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.platform.capability.social.cache;
package cn.daxpay.open.platform.iam.service.social.cache;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.platform.capability.social.cache;
package cn.daxpay.open.platform.iam.service.social.cache;
import lombok.Data;
import lombok.experimental.Accessors;

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.platform.capability.social.cache;
package cn.daxpay.open.platform.iam.service.social.cache;
/// # 社交登录授权场景
///

View File

@@ -0,0 +1,41 @@
package cn.daxpay.open.platform.system.controller.config;
import cn.daxpay.open.platform.core.annotation.PermCode;
import cn.daxpay.open.platform.core.rest.Res;
import cn.daxpay.open.platform.core.rest.result.Result;
import cn.daxpay.open.platform.system.param.config.PlatformOssConfigParam;
import cn.daxpay.open.platform.system.result.config.platform.PlatformOssConfigResult;
import cn.daxpay.open.platform.system.service.config.PlatformOssConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/// # 平台OSS配置
///
/// 管理对象存储配置
@PermCode(menuCode = "system:platform:config")
@Validated
@Tag(name = "平台OSS配置")
@RestController
@RequestMapping("/platform/config/oss")
@RequiredArgsConstructor
public class PlatformOssConfigController {
private final PlatformOssConfigService platformOssConfigService;
@PermCode(code = "platformConfig:view", nameCn = "平台配置查看", nameEn = "Platform Config View")
@Operation(summary = "获取OSS配置")
@GetMapping("/get")
public Result<PlatformOssConfigResult> getOssConfig() {
return Res.ok(platformOssConfigService.findOssConfig());
}
@PermCode(code = "platformConfig:manage", nameCn = "平台配置管理", nameEn = "Platform Config Manage")
@Operation(summary = "更新OSS配置")
@PostMapping("/update")
public Result<Void> updateOssConfig(@RequestBody @Validated PlatformOssConfigParam param) {
platformOssConfigService.updateOssConfig(param);
return Res.ok();
}
}

View File

@@ -4,122 +4,106 @@ import cn.daxpay.open.platform.core.annotation.IgnoreAuth;
import cn.daxpay.open.platform.core.annotation.PermCode;
import cn.daxpay.open.platform.core.rest.Res;
import cn.daxpay.open.platform.core.rest.result.Result;
import cn.daxpay.open.platform.system.param.config.*;
import cn.daxpay.open.platform.system.param.config.security.*;
import cn.daxpay.open.platform.system.result.config.platform.*;
import cn.daxpay.open.platform.system.service.config.PlatformConfigService;
import cn.daxpay.open.platform.system.service.config.PlatformSecurityConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/// # 平台配置
/// # 平台安全配置
///
/// 管理密码策略登录安全会话管理异常登录检测双因素认证等安全类配置
@PermCode(menuCode = "system:security:config")
@Validated
@Tag(name = "平台配置")
@Tag(name = "平台安全配置")
@RestController
@RequestMapping("/platform/config")
@RequestMapping("/platform/config/security")
@RequiredArgsConstructor
public class PlatformConfigController {
private final PlatformConfigService platformConfigService;
public class PlatformSecurityConfigController {
private final PlatformSecurityConfigService platformSecurityConfigService;
@PermCode(code = "security:view", nameCn = "安全配置查看", nameEn = "Security View")
@Operation(summary = "获取密码策略配置")
@GetMapping("/security/password-policy/get")
@GetMapping("/password-policy/get")
public Result<PlatformPasswordPolicyConfigResult> getPasswordPolicyConfig() {
return Res.ok(platformConfigService.findPasswordPolicyConfig());
return Res.ok(platformSecurityConfigService.findPasswordPolicyConfig());
}
@IgnoreAuth
@Operation(summary = "获取密码策略校验配置(供前端校验使用)")
@GetMapping("/security/password-policy/validate-config")
@GetMapping("/password-policy/validate-config")
public Result<PlatformPasswordPolicyConfigResult> getPasswordPolicyValidateConfig() {
return Res.ok(platformConfigService.findPasswordPolicyConfig());
return Res.ok(platformSecurityConfigService.findPasswordPolicyConfig());
}
@PermCode(code = "security:manage", nameCn = "安全配置管理", nameEn = "Security Manage")
@Operation(summary = "更新密码策略配置")
@PostMapping("/security/password-policy/update")
@PostMapping("/password-policy/update")
public Result<Void> updatePasswordPolicyConfig(@RequestBody @Validated PlatformPasswordPolicyConfigParam param) {
platformConfigService.updatePasswordPolicyConfig(param);
platformSecurityConfigService.updatePasswordPolicyConfig(param);
return Res.ok();
}
@PermCode(code = "security:view", nameCn = "安全配置查看", nameEn = "Security View")
@Operation(summary = "获取登录安全配置")
@GetMapping("/security/login/get")
@GetMapping("/login/get")
public Result<PlatformLoginSecurityConfigResult> getLoginSecurityConfig() {
return Res.ok(platformConfigService.findLoginSecurityConfig());
return Res.ok(platformSecurityConfigService.findLoginSecurityConfig());
}
@PermCode(code = "security:manage", nameCn = "安全配置管理", nameEn = "Security Manage")
@Operation(summary = "更新登录安全配置")
@PostMapping("/security/login/update")
@PostMapping("/login/update")
public Result<Void> updateLoginSecurityConfig(@RequestBody @Validated PlatformLoginSecurityConfigParam param) {
platformConfigService.updateLoginSecurityConfig(param);
platformSecurityConfigService.updateLoginSecurityConfig(param);
return Res.ok();
}
@PermCode(code = "security:view", nameCn = "安全配置查看", nameEn = "Security View")
@Operation(summary = "获取会话管理配置")
@GetMapping("/security/session/get")
@GetMapping("/session/get")
public Result<PlatformSessionManagementConfigResult> getSessionManagementConfig() {
return Res.ok(platformConfigService.findSessionManagementConfig());
return Res.ok(platformSecurityConfigService.findSessionManagementConfig());
}
@PermCode(code = "security:manage", nameCn = "安全配置管理", nameEn = "Security Manage")
@Operation(summary = "更新会话管理配置")
@PostMapping("/security/session/update")
@PostMapping("/session/update")
public Result<Void> updateSessionManagementConfig(@RequestBody @Validated PlatformSessionManagementConfigParam param) {
platformConfigService.updateSessionManagementConfig(param);
platformSecurityConfigService.updateSessionManagementConfig(param);
return Res.ok();
}
@PermCode(code = "security:view", nameCn = "安全配置查看", nameEn = "Security View")
@Operation(summary = "获取异常登录检测配置")
@GetMapping("/security/anomaly-detection/get")
@GetMapping("/anomaly-detection/get")
public Result<PlatformAnomalyDetectionConfigResult> getAnomalyDetectionConfig() {
return Res.ok(platformConfigService.findAnomalyDetectionConfig());
return Res.ok(platformSecurityConfigService.findAnomalyDetectionConfig());
}
@PermCode(code = "security:manage", nameCn = "安全配置管理", nameEn = "Security Manage")
@Operation(summary = "更新异常登录检测配置")
@PostMapping("/security/anomaly-detection/update")
@PostMapping("/anomaly-detection/update")
public Result<Void> updateAnomalyDetectionConfig(@RequestBody @Validated PlatformAnomalyDetectionConfigParam param) {
platformConfigService.updateAnomalyDetectionConfig(param);
platformSecurityConfigService.updateAnomalyDetectionConfig(param);
return Res.ok();
}
@PermCode(code = "security:view", nameCn = "安全配置查看", nameEn = "Security View")
@Operation(summary = "获取双因素认证配置")
@GetMapping("/security/two-factor-auth/get")
@GetMapping("/two-factor-auth/get")
public Result<PlatformTwoFactorAuthConfigResult> getTwoFactorAuthConfig() {
return Res.ok(platformConfigService.findTwoFactorAuthConfig());
return Res.ok(platformSecurityConfigService.findTwoFactorAuthConfig());
}
@PermCode(code = "security:manage", nameCn = "安全配置管理", nameEn = "Security Manage")
@Operation(summary = "更新双因素认证配置")
@PostMapping("/security/two-factor-auth/update")
@PostMapping("/two-factor-auth/update")
public Result<Void> updateTwoFactorAuthConfig(@RequestBody @Validated PlatformTwoFactorAuthConfigParam param) {
platformConfigService.updateTwoFactorAuthConfig(param);
platformSecurityConfigService.updateTwoFactorAuthConfig(param);
return Res.ok();
}
@PermCode(code = "security:view", nameCn = "安全配置查看", nameEn = "Security View")
@Operation(summary = "获取OSS配置")
@GetMapping("/oss/get")
public Result<PlatformOssConfigResult> getOssConfig() {
return Res.ok(platformConfigService.findOssConfig());
}
@PermCode(code = "security:manage", nameCn = "安全配置管理", nameEn = "Security Manage")
@Operation(summary = "更新OSS配置")
@PostMapping("/oss/update")
public Result<Void> updateOssConfig(@RequestBody @Validated PlatformOssConfigParam param) {
platformConfigService.updateOssConfig(param);
return Res.ok();
}
}

View File

@@ -0,0 +1,41 @@
package cn.daxpay.open.platform.system.controller.config;
import cn.daxpay.open.platform.core.annotation.PermCode;
import cn.daxpay.open.platform.core.rest.Res;
import cn.daxpay.open.platform.core.rest.result.Result;
import cn.daxpay.open.platform.system.param.config.PlatformUrlConfigParam;
import cn.daxpay.open.platform.system.result.config.platform.PlatformUrlConfigResult;
import cn.daxpay.open.platform.system.service.config.PlatformUrlConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/// # 平台端点配置
///
/// 管理系统访问地址等端点配置
@PermCode(menuCode = "system:platform:config")
@Validated
@Tag(name = "平台端点配置")
@RestController
@RequestMapping("/platform/config/url")
@RequiredArgsConstructor
public class PlatformUrlConfigController {
private final PlatformUrlConfigService platformUrlConfigService;
@PermCode(code = "platformConfig:view", nameCn = "平台配置查看", nameEn = "Platform Config View")
@Operation(summary = "获取端点配置")
@GetMapping("/get")
public Result<PlatformUrlConfigResult> getUrlConfig() {
return Res.ok(platformUrlConfigService.findUrlConfig());
}
@PermCode(code = "platformConfig:manage", nameCn = "平台配置管理", nameEn = "Platform Config Manage")
@Operation(summary = "更新端点配置")
@PostMapping("/update")
public Result<Void> updateUrlConfig(@RequestBody @Validated PlatformUrlConfigParam param) {
platformUrlConfigService.updateUrlConfig(param);
return Res.ok();
}
}

View File

@@ -0,0 +1,24 @@
package cn.daxpay.open.platform.system.convert;
import cn.daxpay.open.platform.system.entity.config.platform.PlatformOssConfig;
import cn.daxpay.open.platform.system.param.config.PlatformOssConfigParam;
import cn.daxpay.open.platform.system.result.config.platform.PlatformOssConfigResult;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
/// # 平台OSS配置转换
///
@Mapper
public interface PlatformOssConfigConvert {
PlatformOssConfigConvert CONVERT = Mappers.getMapper(PlatformOssConfigConvert.class);
PlatformOssConfigResult toOssResult(PlatformOssConfig data);
PlatformOssConfig convert(PlatformOssConfigParam param);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void copy(PlatformOssConfigParam param, @MappingTarget PlatformOssConfig data);
}

View File

@@ -1,49 +1,39 @@
package cn.daxpay.open.platform.system.convert;
import cn.daxpay.open.platform.system.entity.config.platform.PlatformOssConfig;
import cn.daxpay.open.platform.system.entity.config.platform.security.*;
import cn.daxpay.open.platform.system.param.config.*;
import cn.daxpay.open.platform.system.param.config.security.*;
import cn.daxpay.open.platform.system.result.config.platform.*;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
/// # 平台配置转换
/// # 平台安全配置转换
///
/// 转换密码策略登录安全会话管理异常登录检测双因素认证等安全类配置
@Mapper
public interface PlatformConfigConvert {
PlatformConfigConvert CONVERT = Mappers.getMapper(PlatformConfigConvert.class);
// ========== OSS配置转换单配置模式 ==========
PlatformOssConfigResult toOssResult(PlatformOssConfig data);
PlatformOssConfig convert(PlatformOssConfigParam param);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void copy(PlatformOssConfigParam param, @MappingTarget PlatformOssConfig data);
// ========== 异常登录检测配置转换 ==========
PlatformAnomalyDetectionConfigResult toAnomalyDetectionResult(PlatformAnomalyDetectionConfig data);
void copy(PlatformAnomalyDetectionConfigParam param, @MappingTarget PlatformAnomalyDetectionConfig data);
// ========== 登录安全配置转换 ==========
PlatformLoginSecurityConfigResult toLoginSecurityResult(PlatformLoginSecurityConfig data);
void copy(PlatformLoginSecurityConfigParam param, @MappingTarget PlatformLoginSecurityConfig data);
public interface PlatformSecurityConfigConvert {
PlatformSecurityConfigConvert CONVERT = Mappers.getMapper(PlatformSecurityConfigConvert.class);
// ========== 密码策略配置转换 ==========
PlatformPasswordPolicyConfigResult toPasswordPolicyResult(PlatformPasswordPolicyConfig data);
void copy(PlatformPasswordPolicyConfigParam param, @MappingTarget PlatformPasswordPolicyConfig data);
// ========== 登录安全配置转换 ==========
PlatformLoginSecurityConfigResult toLoginSecurityResult(PlatformLoginSecurityConfig data);
void copy(PlatformLoginSecurityConfigParam param, @MappingTarget PlatformLoginSecurityConfig data);
// ========== 会话管理配置转换 ==========
PlatformSessionManagementConfigResult toSessionManagementResult(PlatformSessionManagementConfig data);
void copy(PlatformSessionManagementConfigParam param, @MappingTarget PlatformSessionManagementConfig data);
// ========== 异常登录检测配置转换 ==========
PlatformAnomalyDetectionConfigResult toAnomalyDetectionResult(PlatformAnomalyDetectionConfig data);
void copy(PlatformAnomalyDetectionConfigParam param, @MappingTarget PlatformAnomalyDetectionConfig data);
// ========== 双因素认证配置转换 ==========
PlatformTwoFactorAuthConfigResult toTwoFactorAuthResult(PlatformTwoFactorAuthConfig data);

View File

@@ -0,0 +1,22 @@
package cn.daxpay.open.platform.system.convert;
import cn.daxpay.open.platform.system.entity.config.platform.PlatformUrlConfig;
import cn.daxpay.open.platform.system.param.config.PlatformUrlConfigParam;
import cn.daxpay.open.platform.system.result.config.platform.PlatformUrlConfigResult;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
/// # 平台端点配置转换
///
@Mapper
public interface PlatformUrlConfigConvert {
PlatformUrlConfigConvert CONVERT = Mappers.getMapper(PlatformUrlConfigConvert.class);
PlatformUrlConfigResult toUrlResult(PlatformUrlConfig data);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void copy(PlatformUrlConfigParam param, @MappingTarget PlatformUrlConfig data);
}

View File

@@ -0,0 +1,53 @@
package cn.daxpay.open.platform.system.entity.config.platform;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 平台端点配置
///
/// 各端(管理端/商户端/支付网关/后端 API)的访问地址, 用于第三方登录回调 URL 自动生成、
/// 支付回调地址拼接等场景. 全局唯一, 通过 [PlatformConfigTypeEnum.URL] 存储于系统配置表.
///
/// getter 会自动去除 URL 尾部斜杠, 方便后续拼接路径.
///
@Data
@Accessors(chain = true)
public class PlatformUrlConfig {
/// 管理端访问地址(如 https://admin.daxpay.com)
/// 第三方登录回调等场景依赖此配置, client=admin 时使用
private String adminBaseUrl;
/// 商户端访问地址(如 https://merchant.daxpay.com)
/// client=merchant 时使用
private String merchantBaseUrl;
/// 支付网关前端地址(如 https://pay.daxpay.com)
/// 收银台/支付网关页面的访问地址
private String paymentGatewayBaseUrl;
/// 后端 API 地址(如 https://api.daxpay.com)
/// 用于支付回调通知等后端发起的场景
private String backendBaseUrl;
/// 去除尾部斜杠, 方便后续拼接路径
public String getAdminBaseUrl() {
return StrUtil.removeSuffix(adminBaseUrl, "/");
}
/// 去除尾部斜杠, 方便后续拼接路径
public String getMerchantBaseUrl() {
return StrUtil.removeSuffix(merchantBaseUrl, "/");
}
/// 去除尾部斜杠, 方便后续拼接路径
public String getPaymentGatewayBaseUrl() {
return StrUtil.removeSuffix(paymentGatewayBaseUrl, "/");
}
/// 去除尾部斜杠, 方便后续拼接路径
public String getBackendBaseUrl() {
return StrUtil.removeSuffix(backendBaseUrl, "/");
}
}

View File

@@ -0,0 +1,32 @@
package cn.daxpay.open.platform.system.param.config;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 平台端点配置参数
///
/// 全部选填: 未配置的端对应功能不可用, 但不强制要求一次性配置完整.
/// 如未启用支付网关的环境 paymentGatewayBaseUrl 可留空.
///
@Data
@Accessors(chain = true)
@Schema(title = "平台端点配置参数")
public class PlatformUrlConfigParam {
/// 管理端访问地址
@Schema(description = "管理端访问地址")
private String adminBaseUrl;
/// 商户端访问地址
@Schema(description = "商户端访问地址")
private String merchantBaseUrl;
/// 支付网关前端地址
@Schema(description = "支付网关前端地址")
private String paymentGatewayBaseUrl;
/// 后端 API 地址
@Schema(description = "后端 API 地址")
private String backendBaseUrl;
}

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.platform.system.param.config;
package cn.daxpay.open.platform.system.param.config.security;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.platform.system.param.config;
package cn.daxpay.open.platform.system.param.config.security;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.platform.system.param.config;
package cn.daxpay.open.platform.system.param.config.security;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.platform.system.param.config;
package cn.daxpay.open.platform.system.param.config.security;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.platform.system.param.config;
package cn.daxpay.open.platform.system.param.config.security;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

View File

@@ -3,7 +3,7 @@ package cn.daxpay.open.platform.system.provider;
import cn.daxpay.open.platform.capability.file.entity.FileStorageConfig;
import cn.daxpay.open.platform.capability.file.provider.OssConfigProvider;
import cn.daxpay.open.platform.system.entity.config.platform.PlatformOssConfig;
import cn.daxpay.open.platform.system.service.config.PlatformConfigService;
import cn.daxpay.open.platform.system.service.config.PlatformOssConfigService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -17,11 +17,11 @@ import java.util.Optional;
@RequiredArgsConstructor
public class OssConfigProviderImpl implements OssConfigProvider {
private final PlatformConfigService platformConfigService;
private final PlatformOssConfigService platformOssConfigService;
@Override
public Optional<FileStorageConfig> getDefaultConfig() {
PlatformOssConfig config = platformConfigService.getOssConfig();
PlatformOssConfig config = platformOssConfigService.getOssConfig();
if (config == null || config.getEndpoint() == null) {
log.warn("OSS配置不存在或未配置");
return Optional.empty();

View File

@@ -0,0 +1,29 @@
package cn.daxpay.open.platform.system.result.config.platform;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 平台端点配置返回结果
///
@Data
@Accessors(chain = true)
@Schema(title = "平台端点配置")
public class PlatformUrlConfigResult {
/// 管理端访问地址
@Schema(description = "管理端访问地址")
private String adminBaseUrl;
/// 商户端访问地址
@Schema(description = "商户端访问地址")
private String merchantBaseUrl;
/// 支付网关前端地址
@Schema(description = "支付网关前端地址")
private String paymentGatewayBaseUrl;
/// 后端 API 地址
@Schema(description = "后端 API 地址")
private String backendBaseUrl;
}

View File

@@ -0,0 +1,40 @@
package cn.daxpay.open.platform.system.service.config;
import cn.daxpay.open.platform.system.convert.PlatformOssConfigConvert;
import cn.daxpay.open.platform.system.entity.config.platform.PlatformOssConfig;
import cn.daxpay.open.platform.system.enums.EncryptPlatformConfigTypeEnum;
import cn.daxpay.open.platform.system.param.config.PlatformOssConfigParam;
import cn.daxpay.open.platform.system.result.config.platform.PlatformOssConfigResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/// # 平台OSS配置服务
///
/// 管理对象存储配置,数据通过加密配置服务进行加密存储
@Slf4j
@Service
@RequiredArgsConstructor
public class PlatformOssConfigService {
private final SystemPlatformEncryptConfigService encryptConfigService;
/// 获取OSS配置
public PlatformOssConfig getOssConfig() {
return encryptConfigService.getOrCreateConfig(EncryptPlatformConfigTypeEnum.OSS,
PlatformOssConfig.class,
new PlatformOssConfig());
}
/// 获取OSS配置
public PlatformOssConfigResult findOssConfig() {
return PlatformOssConfigConvert.CONVERT.toOssResult(this.getOssConfig());
}
/// 更新OSS配置
public void updateOssConfig(PlatformOssConfigParam param) {
PlatformOssConfig data = this.getOssConfig();
PlatformOssConfigConvert.CONVERT.copy(param, data);
encryptConfigService.updateConfig(EncryptPlatformConfigTypeEnum.OSS, data);
}
}

View File

@@ -1,27 +1,24 @@
package cn.daxpay.open.platform.system.service.config;
import cn.daxpay.open.platform.system.convert.PlatformConfigConvert;
import cn.daxpay.open.platform.system.entity.config.platform.PlatformOssConfig;
import cn.daxpay.open.platform.system.convert.PlatformSecurityConfigConvert;
import cn.daxpay.open.platform.system.entity.config.platform.security.*;
import cn.daxpay.open.platform.system.enums.EncryptPlatformConfigTypeEnum;
import cn.daxpay.open.platform.system.enums.PlatformConfigTypeEnum;
import cn.daxpay.open.platform.system.param.config.*;
import cn.daxpay.open.platform.system.param.config.security.*;
import cn.daxpay.open.platform.system.result.config.platform.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/// # 平台配置
/// # 平台安全配置服务
///
/// 统一管理密码策略登录安全会话管理异常登录检测双因素认证等安全类配置
@Slf4j
@Service
@RequiredArgsConstructor
public class PlatformConfigService {
public class PlatformSecurityConfigService {
private final SystemPlatformConfigService systemConfigService;
private final SystemPlatformEncryptConfigService encryptConfigService;
/// 获取密码策略配置
public PlatformPasswordPolicyConfig getPasswordPolicyConfig() {
return systemConfigService.getOrCreateConfig(PlatformConfigTypeEnum.SECURITY_PASSWORD_POLICY,
@@ -31,13 +28,13 @@ public class PlatformConfigService {
/// 获取密码策略配置
public PlatformPasswordPolicyConfigResult findPasswordPolicyConfig() {
return PlatformConfigConvert.CONVERT.toPasswordPolicyResult(this.getPasswordPolicyConfig());
return PlatformSecurityConfigConvert.CONVERT.toPasswordPolicyResult(this.getPasswordPolicyConfig());
}
/// 更新密码策略配置
public void updatePasswordPolicyConfig(PlatformPasswordPolicyConfigParam param) {
PlatformPasswordPolicyConfig data = this.getPasswordPolicyConfig();
PlatformConfigConvert.CONVERT.copy(param, data);
PlatformSecurityConfigConvert.CONVERT.copy(param, data);
systemConfigService.updateConfig(PlatformConfigTypeEnum.SECURITY_PASSWORD_POLICY, data);
}
@@ -50,13 +47,13 @@ public class PlatformConfigService {
/// 获取登录安全配置
public PlatformLoginSecurityConfigResult findLoginSecurityConfig() {
return PlatformConfigConvert.CONVERT.toLoginSecurityResult(this.getLoginSecurityConfig());
return PlatformSecurityConfigConvert.CONVERT.toLoginSecurityResult(this.getLoginSecurityConfig());
}
/// 更新登录安全配置
public void updateLoginSecurityConfig(PlatformLoginSecurityConfigParam param) {
PlatformLoginSecurityConfig data = this.getLoginSecurityConfig();
PlatformConfigConvert.CONVERT.copy(param, data);
PlatformSecurityConfigConvert.CONVERT.copy(param, data);
systemConfigService.updateConfig(PlatformConfigTypeEnum.SECURITY_LOGIN, data);
}
@@ -69,13 +66,13 @@ public class PlatformConfigService {
/// 获取会话管理配置
public PlatformSessionManagementConfigResult findSessionManagementConfig() {
return PlatformConfigConvert.CONVERT.toSessionManagementResult(this.getSessionManagementConfig());
return PlatformSecurityConfigConvert.CONVERT.toSessionManagementResult(this.getSessionManagementConfig());
}
/// 更新会话管理配置
public void updateSessionManagementConfig(PlatformSessionManagementConfigParam param) {
PlatformSessionManagementConfig data = this.getSessionManagementConfig();
PlatformConfigConvert.CONVERT.copy(param, data);
PlatformSecurityConfigConvert.CONVERT.copy(param, data);
systemConfigService.updateConfig(PlatformConfigTypeEnum.SECURITY_SESSION, data);
}
@@ -88,13 +85,13 @@ public class PlatformConfigService {
/// 获取异常登录检测配置
public PlatformAnomalyDetectionConfigResult findAnomalyDetectionConfig() {
return PlatformConfigConvert.CONVERT.toAnomalyDetectionResult(this.getAnomalyDetectionConfig());
return PlatformSecurityConfigConvert.CONVERT.toAnomalyDetectionResult(this.getAnomalyDetectionConfig());
}
/// 更新异常登录检测配置
public void updateAnomalyDetectionConfig(PlatformAnomalyDetectionConfigParam param) {
PlatformAnomalyDetectionConfig data = this.getAnomalyDetectionConfig();
PlatformConfigConvert.CONVERT.copy(param, data);
PlatformSecurityConfigConvert.CONVERT.copy(param, data);
systemConfigService.updateConfig(PlatformConfigTypeEnum.ANOMALY_DETECTION, data);
}
@@ -107,33 +104,13 @@ public class PlatformConfigService {
/// 获取双因素认证配置
public PlatformTwoFactorAuthConfigResult findTwoFactorAuthConfig() {
return PlatformConfigConvert.CONVERT.toTwoFactorAuthResult(this.getTwoFactorAuthConfig());
return PlatformSecurityConfigConvert.CONVERT.toTwoFactorAuthResult(this.getTwoFactorAuthConfig());
}
/// 更新双因素认证配置
public void updateTwoFactorAuthConfig(PlatformTwoFactorAuthConfigParam param) {
PlatformTwoFactorAuthConfig data = this.getTwoFactorAuthConfig();
PlatformConfigConvert.CONVERT.copy(param, data);
PlatformSecurityConfigConvert.CONVERT.copy(param, data);
systemConfigService.updateConfig(PlatformConfigTypeEnum.SECURITY_TWO_FACTOR_AUTH, data);
}
/// 获取OSS配置
public PlatformOssConfig getOssConfig() {
return encryptConfigService.getOrCreateConfig(EncryptPlatformConfigTypeEnum.OSS,
PlatformOssConfig.class,
new PlatformOssConfig());
}
/// 获取OSS配置
public PlatformOssConfigResult findOssConfig() {
return PlatformConfigConvert.CONVERT.toOssResult(this.getOssConfig());
}
/// 更新OSS配置
public void updateOssConfig(PlatformOssConfigParam param) {
PlatformOssConfig data = this.getOssConfig();
PlatformConfigConvert.CONVERT.copy(param, data);
encryptConfigService.updateConfig(EncryptPlatformConfigTypeEnum.OSS, data);
}
}

View File

@@ -0,0 +1,40 @@
package cn.daxpay.open.platform.system.service.config;
import cn.daxpay.open.platform.system.convert.PlatformUrlConfigConvert;
import cn.daxpay.open.platform.system.entity.config.platform.PlatformUrlConfig;
import cn.daxpay.open.platform.system.enums.PlatformConfigTypeEnum;
import cn.daxpay.open.platform.system.param.config.PlatformUrlConfigParam;
import cn.daxpay.open.platform.system.result.config.platform.PlatformUrlConfigResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/// # 平台端点配置服务
///
/// 管理系统访问地址等端点配置
@Slf4j
@Service
@RequiredArgsConstructor
public class PlatformUrlConfigService {
private final SystemPlatformConfigService systemConfigService;
/// 获取端点配置
public PlatformUrlConfig getUrlConfig() {
return systemConfigService.getOrCreateConfig(PlatformConfigTypeEnum.URL,
PlatformUrlConfig.class,
new PlatformUrlConfig());
}
/// 获取端点配置
public PlatformUrlConfigResult findUrlConfig() {
return PlatformUrlConfigConvert.CONVERT.toUrlResult(this.getUrlConfig());
}
/// 更新端点配置
public void updateUrlConfig(PlatformUrlConfigParam param) {
PlatformUrlConfig data = this.getUrlConfig();
PlatformUrlConfigConvert.CONVERT.copy(param, data);
systemConfigService.updateConfig(PlatformConfigTypeEnum.URL, data);
}
}