Commit 05599dc3 by jianan

feat(核对环境): local-prod 本地核对链路——模拟转介人身份 + 全量本地配置

本地搭建生产数据核对环境:csf-api 以 local-prod profile 连生产库
只读账号,同事经办公网访问本机逐人核对门户数据口径。

- DevMockIdentitySupplier/HeaderMockIdentitySupplier:dev 模拟身份,
  经 X-Mock-Broker-Biz-Id 请求头切换转介人,汇入 ReferrerIdentityFilter
  同一 applyIdentity 路径;@Profile("local-prod") Bean 级硬隔离,
  生产无 Bean 即物理禁用
- DevReferrerController + selectDevBrokerOptions:选人列表
  (salary 已推送 UNION policy_broker 归属,覆盖无保单归属单)
- bootstrap.yml local-prod 段:nacos.config/discovery 关闭。Nacos
  远程 property source 优先级高于本地 yml 且 override-none 对 Nacos
  无效,不关闭则测试库连接串静默覆盖生产只读配置
- application-local-prod.yml:全量自包含本地配置,生产库连接串经
  PROD_DB_* 环境变量注入(占位 fail-safe),敏感口令一律不入库
- XxlJobConfig:xxl.job.enabled=false 可关执行器,防本地抢定时任务
parent 7c7da2b1
package com.yd.csf.api.controller;
import com.yd.common.result.Result;
import com.yd.csf.service.dao.PolicyBrokerMapper;
import com.yd.csf.service.dto.DevBrokerOptionDto;
import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 转介人门户 - 本地核对专用 dev 接口。
*
* <p>仅 local-prod profile 注册;其他 profile 下 Bean 不存在,路径返回 404。
* 在安全链中 {@code /c/dev/**} 放行,无需模拟身份即可拉取选人列表。</p>
*/
@Profile("local-prod")
@RestController
@RequestMapping("/c/dev")
public class DevReferrerController {
private final PolicyBrokerMapper policyBrokerMapper;
public DevReferrerController(PolicyBrokerMapper policyBrokerMapper) {
this.policyBrokerMapper = policyBrokerMapper;
}
/**
* 核对选人列表:有已推送薪资单者(hasStatement=true,置顶)与有保单归属者之并集。
*/
@GetMapping("/brokers")
public Result<List<DevBrokerOptionDto>> brokers() {
return Result.success(policyBrokerMapper.selectDevBrokerOptions());
}
}
package com.yd.csf.api.security;
import javax.servlet.http.HttpServletRequest;
/**
* 开发/核对环境的模拟转介人身份供给。
*
* <p>仅在特定 profile(local-prod)下有实现 Bean;生产环境无此 Bean,
* 由 {@link org.springframework.beans.factory.ObjectProvider} 空提供保证物理不可用。</p>
*/
public interface DevMockIdentitySupplier {
/**
* 解析当前请求要模拟的 {@code broker_biz_id};无模拟头或身份不成立时返回 {@code null}。
*/
String resolveMockBrokerBizId(HttpServletRequest request);
}
package com.yd.csf.api.security;
import com.yd.csf.service.dao.PolicyBrokerMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* 基于请求头 {@code X-Mock-Broker-Biz-Id} 的模拟身份供给,仅 local-prod profile 注册。
*
* <p>解析出的 bizId 必须在生产库 {@code policy_broker} 中存在,否则视为无身份(落 401),
* 避免任意伪造值返回空数据 200 造成误读。</p>
*/
@Slf4j
@Component
@Profile("local-prod")
public class HeaderMockIdentitySupplier implements DevMockIdentitySupplier {
public static final String MOCK_HEADER = "X-Mock-Broker-Biz-Id";
private final PolicyBrokerMapper policyBrokerMapper;
public HeaderMockIdentitySupplier(PolicyBrokerMapper policyBrokerMapper) {
this.policyBrokerMapper = policyBrokerMapper;
}
@Override
public String resolveMockBrokerBizId(HttpServletRequest request) {
String brokerBizId = request.getHeader(MOCK_HEADER);
if (!StringUtils.hasText(brokerBizId)) {
return null;
}
brokerBizId = brokerBizId.trim();
if (policyBrokerMapper.countBrokerExistence(brokerBizId) == 0) {
log.warn("模拟身份 broker_biz_id 在 policy_broker/salary 中均不存在:{}", brokerBizId);
return null;
}
return brokerBizId;
}
}
......@@ -5,6 +5,7 @@ import com.yd.common.result.Result;
import com.yd.user.feign.client.clientuser.ApiClientUserFeignClient;
import com.yd.user.feign.response.clientuser.ApiClientUserIdpLoginResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
......@@ -29,7 +30,9 @@ import java.util.Objects;
* <li>请求携带 {@code Authorization: Bearer} 且配置了 JWKS:用 {@link JwtDecoder} 校验
* RS256 + iss + aud,取 {@code sub} 调 yd-user 解析客户端用户,
* 并强制 {@code status=1 && approval_status=CG} 门禁;</li>
* <li>未携带 token 但配置了 {@code referrer.oidc.mock-client-user-biz-id}:注入固定身份,
* <li>Bearer 未建立身份但存在 {@link DevMockIdentitySupplier}(仅 local-prod profile):
* 按请求头模拟转介人身份;</li>
* <li>以上均无、但配置了 {@code referrer.oidc.mock-client-user-biz-id}:注入固定身份,
* 仅供开发期联调(生产不得配置)。</li>
* </ol>
*
......@@ -43,13 +46,16 @@ public class ReferrerIdentityFilter extends OncePerRequestFilter {
private final ReferrerOidcProperties properties;
private final ApiClientUserFeignClient clientUserFeignClient;
private final JwtDecoder jwtDecoder;
private final ObjectProvider<DevMockIdentitySupplier> mockSupplierProvider;
public ReferrerIdentityFilter(ReferrerOidcProperties properties,
ApiClientUserFeignClient clientUserFeignClient,
JwtDecoder jwtDecoder) {
JwtDecoder jwtDecoder,
ObjectProvider<DevMockIdentitySupplier> mockSupplierProvider) {
this.properties = properties;
this.clientUserFeignClient = clientUserFeignClient;
this.jwtDecoder = jwtDecoder;
this.mockSupplierProvider = mockSupplierProvider;
}
@Override
......@@ -63,11 +69,27 @@ public class ReferrerIdentityFilter extends OncePerRequestFilter {
authenticated = authenticateWithJwt(token);
} else if (StringUtils.hasText(token)) {
log.warn("收到 Bearer token 但 JwtDecoder 未初始化:referrer.oidc.jwk-set-uri 是否配置?");
} else if (StringUtils.hasText(properties.getMockClientUserBizId())) {
}
// Bearer 未建立身份时,dev profile 下尝试请求头模拟身份(真实凭证优先)
if (!authenticated) {
DevMockIdentitySupplier mockSupplier = mockSupplierProvider.getIfAvailable();
if (mockSupplier != null) {
String mockBrokerBizId = mockSupplier.resolveMockBrokerBizId(request);
if (StringUtils.hasText(mockBrokerBizId)) {
applyIdentity(mockBrokerBizId);
authenticated = true;
}
}
}
if (!authenticated && StringUtils.hasText(properties.getMockClientUserBizId())) {
applyIdentity(properties.getMockClientUserBizId());
authenticated = true;
} else {
log.debug("请求未携带 Bearer token,且未配置 mock 身份");
}
if (!authenticated) {
log.debug("请求未建立任何转介人身份");
}
if (!authenticated) {
......
package com.yd.csf.api.security;
import com.yd.user.feign.client.clientuser.ApiClientUserFeignClient;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
......@@ -50,11 +51,14 @@ public class ReferrerPortalSecurityConfig extends WebSecurityConfigurerAdapter {
private final ReferrerOidcProperties properties;
private final ApiClientUserFeignClient clientUserFeignClient;
private final ObjectProvider<DevMockIdentitySupplier> mockSupplierProvider;
public ReferrerPortalSecurityConfig(ReferrerOidcProperties properties,
ApiClientUserFeignClient clientUserFeignClient) {
ApiClientUserFeignClient clientUserFeignClient,
ObjectProvider<DevMockIdentitySupplier> mockSupplierProvider) {
this.properties = properties;
this.clientUserFeignClient = clientUserFeignClient;
this.mockSupplierProvider = mockSupplierProvider;
}
@Override
......@@ -68,10 +72,13 @@ public class ReferrerPortalSecurityConfig extends WebSecurityConfigurerAdapter {
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
.authorizeRequests()
// 核对用 dev 选人接口无需身份;dev Controller 仅 local-prod 存在,其他 profile 下为 404
.antMatchers("/c/dev/**").permitAll()
.anyRequest().authenticated()
.and()
// 链内手动 new:不加 @Component,避免被 Boot 注册成容器级 Filter 污染老后台 /** 链
.addFilterBefore(new ReferrerIdentityFilter(properties, clientUserFeignClient, buildJwtDecoder()),
.addFilterBefore(new ReferrerIdentityFilter(properties, clientUserFeignClient,
buildJwtDecoder(), mockSupplierProvider),
UsernamePasswordAuthenticationFilter.class);
}
......
# 本地生产数据核对 profile —— 全量本地配置(不拉 Nacos 远程,原因见 bootstrap.yml local-prod 段)。
# 内容复制自 test Nacos 的 yd-common.yml + yd-csf-api.yml,数据源替换为生产只读账号。
server:
port: 9202
servlet:
context-path: /csf/api
tomcat:
uri-encoding: UTF-8
spring:
jackson:
time-zone: Asia/Shanghai
mvc:
pathmatch:
matching-strategy: ant_path_matcher
main:
allow-bean-definition-overriding: true
allow-circular-references: true
datasource:
# 生产库【只读账号】。真实值经环境变量或 IDE 运行配置注入,禁止把生产连接串提交仓库。
# 默认值为不可用占位——未注入时启动即连接失败(fail-safe,防止静默退回到其他库)。
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${PROD_DB_URL:jdbc:mysql://localhost:1/REPLACE_ME_PROD_READONLY?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true}
username: ${PROD_DB_USER:REPLACE_ME_READONLY_USER}
password: ${PROD_DB_PASSWORD:REPLACE_ME_READONLY_PASSWORD}
druid:
initialSize: 2
minIdle: 2
maxActive: 10
maxWait: 60000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
redis:
# 继承测试环境 Redis(门户只读链路不使用 Redis;即使边角代码碰到也是测试环境,不影响生产)
host: ${TEST_REDIS_HOST:139.224.139.2}
port: ${TEST_REDIS_PORT:6300}
database: 5
password: ${TEST_REDIS_PASSWORD:}
timeout: 10s
lettuce:
pool:
min-idle: 0
max-idle: 8
max-active: 8
max-wait: -1ms
mail:
host: smtp.exmail.qq.com
port: 465
default-encoding: UTF-8
username: system@csf.com.hk
password: ${MAIL_PASSWORD:}
properties:
mail.smtp.auth: true
mail.smtp.ssl.enable: true
mybatis-plus:
mapper-locations: classpath:mappers/*.xml
configuration:
map-underscore-to-camel-case: true
global-config:
db-config:
id-type: auto
logic-delete-field: isDeleted
logic-delete-value: 1
logic-not-delete-value: 0
feign:
okhttp:
enabled: false
httpclient:
enabled: true
client:
refresh-enabled: true
jwt:
secret: "v92cuMDOZEd0WIbmX9LHQpZ0HgMIfRPXvDXGmP+5ThbADd2YdD0mCbGujIvLSBZR2ZWby8PKYRT2ReBQ7wIDPQ=="
expiration: 86400000
refresh-expiration: 2592000000
login:
redisTimeout: 2
referrer:
oidc:
# 不外联 Casdoor:jwk-set-uri 留空时 JwtDecoder=null
jwk-set-uri:
issuer:
audience:
xxl:
job:
# 关闭执行器注册,本地实例不抢生产/测试定时任务
enabled: false
# 以下为沿用测试值的边角配置,防止 @Value/@ConfigurationProperties 启动缺项报错
renewal:
reminder:
mail-from:
recipients:
policy:
to:
cc:
default-grace-days: 31
send-interval-ms: 2000
logging:
level:
com.alibaba.druid.filter.stat.StatFilter: DEBUG
spring:
profiles:
active: test
active: local-prod
# active: '@spring.profiles.active@'
---
spring:
......@@ -67,6 +67,24 @@ spring:
server-addr: ${spring.cloud.nacos.config.server-addr}
group: YD_GROUP
---
# 本地生产数据核对 profile:不拉 Nacos 远程配置、不注册服务发现。
# 原因:Nacos 远程 property source 优先级高于本地 application-local-prod.yml,
# spring.cloud.config.override-none 仅对 Config Server 生效,Nacos 不消费——
# 若拉远程,测试库连接串会覆盖本地生产只读配置。故配置全量本地自备。
spring:
application:
name: yd-csf-api
profiles: local-prod
main:
allow-bean-definition-overriding: true
allow-circular-references: true
cloud:
nacos:
config:
enabled: false
discovery:
enabled: false
---
spring:
profiles: prod
main:
......
......@@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
......@@ -45,6 +46,7 @@ public class XxlJobConfig {
private int logRetentionDays;
@Bean
@ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true", matchIfMissing = true)
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> xxl-job config init. adminAddresses={}, appname={}, ip={}, port={}",
adminAddresses, appname, StringUtils.defaultIfBlank(ip, "auto"), port);
......
package com.yd.csf.service.dao;
import com.yd.csf.service.dto.DevBrokerOptionDto;
import com.yd.csf.service.model.PolicyBroker;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
......@@ -15,6 +16,18 @@ import java.util.List;
public interface PolicyBrokerMapper extends BaseMapper<PolicyBroker> {
List<String> selectRelatedBrokerBizIds(@Param("clientUserId") String clientUserId);
/**
* 本地核对选人列表:已推送薪资单(salary status=5)的转介人与有保单归属者之并集,
* hasStatement 标记前者,供前端置顶分组。
*/
List<DevBrokerOptionDto> selectDevBrokerOptions();
/**
* 模拟身份存在性校验:该 bizId 在 policy_broker 或 salary 任一表存在即合法
* (存在「有薪资单但无保单归属」的真实数据,如顾艳艳,不能只锚 policy_broker)。
*/
int countBrokerExistence(@Param("brokerBizId") String brokerBizId);
}
......
package com.yd.csf.service.dto;
import lombok.Data;
/**
* 本地核对选人器选项(dev-only)。
*/
@Data
public class DevBrokerOptionDto {
private String brokerBizId;
private String brokerName;
/**
* 是否存在已推送薪资单(status=5);用于前端将有月结单者置顶分组。
*/
private Boolean hasStatement;
}
......@@ -69,4 +69,32 @@
) AS t
WHERE related_user_id IS NOT NULL
</select>
<!-- 本地核对:选人列表 = 已推送薪资单者 ∪ 有保单归属者 -->
<select id="selectDevBrokerOptions" resultType="com.yd.csf.service.dto.DevBrokerOptionDto">
SELECT broker_biz_id AS brokerBizId,
MAX(broker_name) AS brokerName,
MAX(has_statement) AS hasStatement
FROM (
SELECT broker_biz_id, broker_name, 1 AS has_statement
FROM salary
WHERE is_deleted = 0 AND status = '5' AND broker_biz_id IS NOT NULL
UNION ALL
SELECT broker_biz_id, broker_name, 0 AS has_statement
FROM policy_broker
WHERE is_deleted = 0 AND broker_biz_id IS NOT NULL
) t
GROUP BY broker_biz_id
ORDER BY hasStatement DESC, brokerName
</select>
<!-- 本地核对:模拟身份存在性(两表任一存在即可) -->
<select id="countBrokerExistence" resultType="int">
SELECT
(SELECT COUNT(1) FROM policy_broker
WHERE broker_biz_id = #{brokerBizId} AND is_deleted = 0)
+
(SELECT COUNT(1) FROM salary
WHERE broker_biz_id = #{brokerBizId} AND is_deleted = 0)
</select>
</mapper>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment