feat(security): 平台安全配置拆分并增强 nonce/防重放

将平台安全配置拆为 API/IAM/支付多份;nonce 校验可配置化;补齐 SQL、权限码与多语 validation/i18n。
This commit is contained in:
DaxPay Dev
2026-07-17 08:24:29 +08:00
parent 9d122f6941
commit 86b0fa0be2
54 changed files with 1188 additions and 68 deletions

View File

@@ -12,7 +12,7 @@
Target Server Version : 160014 (160014)
File Encoding : 65001
Date: 16/07/2026 08:06:16
Date: 16/07/2026 11:04:53
*/
@@ -91,7 +91,9 @@ INSERT INTO "public"."iam_perm_menu" VALUES (302, 3, NULL, 'admin', 'SystemLog',
INSERT INTO "public"."iam_perm_menu" VALUES (304, 3, 'system:config', 'admin', 'SystemConfig', 'menu.system.config', 'lucide:settings-2', 'f', 'f', NULL, '/system/config', NULL, 10, 'f', 't', 'f', 0, 1, 1, 'f', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-04-04 16:00:00+00', '2026-04-09 15:11:00.840153+00');
INSERT INTO "public"."iam_perm_menu" VALUES (401, 4, 'payment:platform', 'admin', 'PaymentPlatform', 'menu.payment.platform', 'lucide:building', 'f', 'f', NULL, '/payment/platform', NULL, 1, 'f', 't', 'f', 0, 1, 3, 'f', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-04-05 16:00:00+00', '2026-05-28 06:25:42.880461+00');
INSERT INTO "public"."iam_perm_menu" VALUES (203, 2, 'demos:region', 'admin', 'RegionCascaderDemo', 'menu.demos.region', 'lucide:map-pin', 'f', 'f', '/demos/region/RegionCascaderDemo', '/demos/region', NULL, 3, 'f', 't', 'f', 0, NULL, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-04-24 16:00:00+00', '2026-04-24 16:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (30401, 304, 'system:security-config', 'admin', 'SecurityConfig', 'menu.system.config.security', 'lucide:shield-check', 'f', 'f', '/system/config/security/SecurityConfig', '/system/config/security', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-04-05 02:00:00+00', '2026-07-13 13:17:21.931514+00');
INSERT INTO "public"."iam_perm_menu" VALUES (30401, 304, 'system:security-config', 'admin', 'SecurityManage', 'menu.system.security', 'lucide:shield-check', 'f', 'f', NULL, '/system/config/security', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-04-05 02:00:00+00', '2026-07-16 02:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (3040101, 30401, 'system:security-config:system', 'admin', 'SystemSecurityConfig', 'menu.system.security.system', 'lucide:lock', 'f', 'f', '/system/config/security/SystemSecurityConfig', '/system/config/security/system', NULL, 1, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-16 02:00:00+00', '2026-07-16 02:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (3040102, 30401, 'system:security-config:pay', 'admin', 'PaySecurityConfig', 'menu.system.security.pay', 'lucide:shield-alert', 'f', 'f', '/system/config/security/PaySecurityConfig', '/system/config/security/pay', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-16 02:00:00+00', '2026-07-16 02:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (3, NULL, NULL, 'admin', 'System', 'menu.system', 'lucide:sliders-horizontal', 'f', 'f', NULL, '/system', NULL, 0, 'f', 't', 'f', 0, 1, 2, 'f', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-03-20 03:11:13.134079+00', '2026-06-25 02:00:30.22348+00');
INSERT INTO "public"."iam_perm_menu" VALUES (30102, 301, 'iam:menu', 'admin', 'SystemMenu', 'menu.system.perm.menu', 'lucide:panel-top', 'f', 'f', '/iam/perm/menu/MenuList', '/system/basic/menu', NULL, 0, 'f', 't', 'f', 0, 1, 3, 'f', 'menu', NULL, NULL, NULL, NULL, '', '', '2026-03-20 03:11:13.134079+00', '2026-06-27 10:29:51.371435+00');
INSERT INTO "public"."iam_perm_menu" VALUES (301, 3, NULL, 'admin', 'SystemBasic', 'menu.system.basic', 'lucide:boxes', 'f', 'f', NULL, '/system/basic', NULL, 1, 'f', 't', 'f', 0, NULL, 0, 'f', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-03-20 03:11:13.134079+00', '2026-06-25 02:00:30.236321+00');
@@ -131,7 +133,7 @@ INSERT INTO "public"."iam_perm_menu" VALUES (8, NULL, 'develop', 'admin', 'Devel
INSERT INTO "public"."iam_perm_menu" VALUES (9, NULL, 'device', 'admin', 'DeviceManagement', 'menu.device', 'lucide:qr-code', 'f', 'f', NULL, '/device', NULL, 4.2, 'f', 't', 'f', 1, 1, 1, 'f', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-25 01:21:34.631155+00', '2026-07-11 02:17:16.019646+00');
INSERT INTO "public"."iam_perm_menu" VALUES (204, 2, 'demos:artemis', 'admin', 'ArtemisDemo', 'menu.demos.artemis', 'lucide:send', 'f', 'f', '/demos/artemis/ArtemisDemo', '/demos/artemis', NULL, 4, 'f', 't', 'f', 0, NULL, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-18 00:00:00+00', '2026-06-18 00:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (900001, 304, 'iam:social', 'admin', 'ThirdPlatform', 'menu.system.config.thirdPlatform', 'lucide:share-2', 'f', 'f', '/system/config/third-platform/ThirdPlatform', '/system/config/third-platform', NULL, 5, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-13 02:15:41.505142+00');
INSERT INTO "public"."iam_perm_menu" VALUES (30403, 304, 'system:protocol', 'admin', 'UserProtocol', 'menu.system.config.protocol', 'lucide:file-text', 'f', 'f', '/system/protocol/UserProtocolList', '/system/config/protocol', NULL, 10, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-24 10:32:13.183371+00', '2026-06-24 10:32:13.183371+00');
INSERT INTO "public"."iam_perm_menu" VALUES (30403, 301, 'system:protocol', 'admin', 'UserProtocol', 'menu.system.basic.protocol', 'lucide:file-text', 'f', 'f', '/system/basic/protocol/UserProtocolList', '/system/basic/protocol', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-24 10:32:13.183371+00', '2026-06-24 10:32:13.183371+00');
INSERT INTO "public"."iam_perm_menu" VALUES (4040103, 4040130, 'merchant:profile', 'admin', 'MchInfoManage', 'menu.payment.merchant.profile', NULL, 't', 'f', '/payment/merchant/manage/info/MchInfoManage', '/payment/merchant/manage/info', NULL, 1, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-11 03:37:15.424776+00');
INSERT INTO "public"."iam_perm_menu" VALUES (801, 8, 'develop:trade', 'admin', 'DevelopTrade', 'menu.develop.trade', 'lucide:credit-card', 'f', 'f', '/payment/develop/trade/DevelopTrade', '/develop/trade', NULL, 1, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-23 16:00:00+00', '2026-06-23 16:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (802, 8, 'develop:sign', 'admin', 'DevelopSign', 'menu.develop.sign', 'lucide:file-signature', 'f', 'f', '/payment/develop/sign/DevelopSign', '/develop/sign', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-23 16:00:00+00', '2026-06-23 16:00:00+00');
@@ -149,12 +151,13 @@ INSERT INTO "public"."iam_perm_menu" VALUES (40502, 40508, 'payment:isv', 'admin
INSERT INTO "public"."iam_perm_menu" VALUES (901, 9, 'device:qrcode', 'admin', 'DeviceQrCode', 'menu.device.qrcode', 'lucide:qr-code', 'f', 'f', '/payment/device/qrcode/DeviceQrCode', '/device/qrcode', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-25 01:21:34.634639+00', '2026-06-25 12:51:25.24304+00');
INSERT INTO "public"."iam_perm_menu" VALUES (4040119, 4040132, 'channel:app', 'admin', 'WechatIsvMchAppManage', 'menu.payment.merchant.channelMerchant.wechatIsvApp', NULL, 't', 'f', '/payment/channel/wechat/manage/mch/isv-app/WechatIsvMchAppManage', '/payment/merchant/channel-merchant/wechat-isv-mch-app-manage', NULL, 14, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-05 01:49:32.557374+00', '2026-07-11 03:37:15.430392+00');
INSERT INTO "public"."iam_perm_menu" VALUES (803, 8, 'develop:auth', 'admin', 'ChannelAuth', 'menu.develop.auth', 'lucide:key-round', 'f', 'f', '/payment/develop/auth/ChannelAuth', '/develop/auth', NULL, 3, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-09 16:00:00+00', '2026-07-09 16:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (804, 8, 'develop:gateway', 'admin', 'DevelopGateway', 'menu.develop.gateway', 'lucide:store', 'f', 'f', '/payment/develop/gateway/DevelopGateway', '/develop/gateway', NULL, 4, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-16 16:00:00+00', '2026-07-16 16:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (310, 308, 'system:notify:wechat-config', 'admin', 'WechatNotify', 'menu.system.notify.wechatConfig', 'lucide:message-circle', 'f', 'f', '/system/notify/wechat/index', '/system/notify/wechat', NULL, 30, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-09 00:00:00+00', '2026-07-09 00:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (4040121, 4040130, 'merchant:gateway-aggregate', 'admin', 'AggregateScanConfig', 'menu.payment.merchant.aggregateScan', NULL, 't', 'f', '/payment/merchant/aggregate/AggregateScanConfig', '/payment/merchant/aggregate', NULL, 3, 'f', 't', 'f', 1, 1, 1, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-11 03:00:13.374496+00', '2026-07-13 10:13:20.81126+00');
INSERT INTO "public"."iam_perm_menu" VALUES (40507, 304, 'payment:config:wx-verify', 'admin', 'PlatformWxDomainVerifyList', 'menu.payment.config.wxVerify', 'lucide:shield-check', 'f', 'f', '/payment/config/wx-verify/PlatformWxDomainVerifyList', '/payment/config/wx-verify', NULL, 7, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-09 16:00:00+00', '2026-07-13 13:02:20.706909+00');
INSERT INTO "public"."iam_perm_menu" VALUES (40507, 405, 'payment:config:wx-verify', 'admin', 'PlatformWxDomainVerifyList', 'menu.payment.config.wxVerify', 'lucide:shield-check', 'f', 'f', '/payment/config/wx-verify/PlatformWxDomainVerifyList', '/payment/config/wx-verify', NULL, 3, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-09 16:00:00+00', '2026-07-13 13:02:20.706909+00');
INSERT INTO "public"."iam_perm_menu" VALUES (4040130, 40401, NULL, 'admin', 'MchManageGroup', 'menu.payment.merchant.group.manage', 'lucide:settings-2', 't', 'f', NULL, NULL, NULL, 1, 'f', 'f', 'f', 1, 1, 0, 'f', 'subpage_group', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-11 03:37:15.421585+00', '2026-07-11 03:37:15.421585+00');
INSERT INTO "public"."iam_perm_menu" VALUES (4040131, 40401, NULL, 'admin', 'ChannelMerchantGroup', 'menu.payment.merchant.group.channelMerchant', 'lucide:repeat', 't', 'f', NULL, NULL, NULL, 2, 'f', 'f', 'f', 1, 1, 0, 'f', 'subpage_group', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-11 03:37:15.421585+00', '2026-07-11 03:37:15.421585+00');
INSERT INTO "public"."iam_perm_menu" VALUES (40402, 404, 'channel:merchant', 'admin', 'ChannelMerchantGlobal', 'menu.payment.merchant.channelMerchant.global', 'lucide:repeat', 'f', 'f', '/_core/fallback/coming-soon', '/payment/merchant/channel-merchants', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-13 13:12:31.180398+00', '2026-07-13 13:12:31.180398+00');
INSERT INTO "public"."iam_perm_menu" VALUES (40402, 404, 'channel:merchant', 'admin', 'ChannelMerchantGlobal', 'menu.payment.merchant.channelMerchant.global', 'lucide:repeat', 'f', 'f', '/payment/merchant/channel-merchant-global/ChannelMerchantGlobalList', '/payment/merchant/channel-merchants', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-13 13:12:31.180398+00', '2026-07-13 13:12:31.180398+00');
INSERT INTO "public"."iam_perm_menu" VALUES (4040125, 4040130, 'merchant:gateway-code', 'admin', 'CodePayConfig', 'menu.payment.merchant.codePayConfig', NULL, 't', 'f', '/payment/merchant/code-config/CodePayConfig', '/payment/merchant/code-config', NULL, 4, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-15 09:03:23.606222+00', '2026-07-15 09:03:23.606222+00');
INSERT INTO "public"."iam_perm_menu" VALUES (4040120, 4040130, 'merchant:wx-verify', 'admin', 'MchWxDomainVerifyList', 'menu.payment.merchant.wxVerify', NULL, 't', 'f', '/payment/merchant/manage/wx-verify/MchWxDomainVerifyList', '/payment/merchant/manage/wx-verify', NULL, 20, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-09 16:00:00+00', '2026-07-11 03:37:15.424776+00');
INSERT INTO "public"."iam_perm_menu" VALUES (40501, 40105, 'payment:config:product-detail', 'admin', 'ProductDetailDispatch', 'menu.payment.config.detail', NULL, 't', 'f', '/payment/config/product/detail/ProductDetailDispatch', '/payment/product-detail', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-14 00:00:00+00', '2026-06-27 13:01:24.321883+00');
@@ -169,9 +172,9 @@ INSERT INTO "public"."iam_perm_menu" VALUES (4040124, 4040130, 'merchant:easypay
INSERT INTO "public"."iam_perm_menu" VALUES (406, 4, 'payment:risk', 'admin', 'PaymentRisk', 'menu.payment.risk', 'lucide:shield-alert', 'f', 'f', NULL, '/payment/risk', NULL, 4, 'f', 't', 'f', 0, 1, 0, 'f', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-15 00:00:00+00', '2026-07-15 00:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (40601, 406, 'payment:risk:blacklist', 'admin', 'PayBlacklistList', 'menu.payment.risk.blacklist', 'lucide:ban', 'f', 'f', '/payment/risk/blacklist/PayBlacklistList', '/payment/risk/blacklist', NULL, 1, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-15 00:00:00+00', '2026-07-15 00:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (40602, 406, 'payment:risk:hit', 'admin', 'PayRiskHitList', 'menu.payment.risk.hit', 'lucide:triangle-alert', 'f', 'f', '/payment/risk/hit/PayRiskHitList', '/payment/risk/hit', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-15 00:00:00+00', '2026-07-15 00:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (311, 3, 'system:sensitive', 'admin', 'SensitiveWord', 'menu.system.sensitive', 'lucide:shield-ban', 'f', 'f', NULL, '/system/sensitive', NULL, 15, 'f', 't', 'f', 1, 1, 0, 'f', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-16 00:00:00+00', '2026-07-16 00:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (31101, 311, 'system:sensitive-word', 'admin', 'SensitiveWordList', 'menu.system.sensitive.word', 'lucide:book-x', 'f', 'f', '/system/sensitive-word/SensitiveWordList', '/system/sensitive/word', NULL, 1, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-16 00:00:00+00', '2026-07-16 00:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (31102, 311, 'system:sensitive-word-hit', 'admin', 'SensitiveWordHitList', 'menu.system.sensitive.hit', 'lucide:scan-search', 'f', 'f', '/system/sensitive-word/SensitiveWordHitList', '/system/sensitive/hit', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-16 00:00:00+00', '2026-07-16 00:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (311, 3, 'system:sensitive', 'admin', 'SensitiveWord', 'menu.system.sensitive', 'lucide:shield-ban', 'f', 'f', NULL, '/system/sensitive', NULL, 15, 'f', 't', 'f', 1, 1, 0, 'f', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-16 00:40:06.501938+00', '2026-07-16 00:40:06.501938+00');
INSERT INTO "public"."iam_perm_menu" VALUES (31101, 311, 'system:sensitive-word', 'admin', 'SensitiveWordList', 'menu.system.sensitive.word', 'lucide:book-x', 'f', 'f', '/system/sensitive-word/SensitiveWordList', '/system/sensitive/word', NULL, 1, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-16 00:40:06.511319+00', '2026-07-16 00:40:06.511319+00');
INSERT INTO "public"."iam_perm_menu" VALUES (31102, 311, 'system:sensitive-word-hit', 'admin', 'SensitiveWordHitList', 'menu.system.sensitive.hit', 'lucide:scan-search', 'f', 'f', '/system/sensitive-word/SensitiveWordHitList', '/system/sensitive/hit', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-16 00:40:06.51532+00', '2026-07-16 00:40:06.51532+00');
-- ----------------------------
-- Primary Key structure for table iam_perm_menu

View File

@@ -1,28 +1,105 @@
-- 敏感词菜单(系统管理 · 方案 Acatalog + 词库 + 命中
-- 策略开关在「平台配置」Tab不新增菜单
-- 不预置敏感词词条
-- 列序对齐 iam_perm_menu 全量导出
INSERT INTO "public"."iam_perm_menu" VALUES
(311, 3, 'system:sensitive', 'admin', 'SensitiveWord', 'menu.system.sensitive', 'lucide:shield-ban',
'f', 'f', NULL, '/system/sensitive', NULL, 15,
'f', 't', 'f', 1, 1, 0, 'f', 'catalog',
NULL, NULL, NULL, NULL, NULL, NULL,
NOW(), NOW())
ON CONFLICT ("id") DO NOTHING;
-- ----------------------------
-- 收银台模拟配置: 商户 M1781861282382 / 应用 A7846150576259390
-- H5(五环境 browser/wechat/alipay/union_pay/douyin) + WEB + MINI(四环境)
-- resolve_mode 全部 method; 固定 ID 段 9100000000000100001~026, 可重复执行
-- ----------------------------
DELETE FROM "public"."pay_gateway_cashier_item"
WHERE "mch_no" = 'M1781861282382'
AND "app_id" = 'A7846150576259390';
INSERT INTO "public"."iam_perm_menu" VALUES
(31101, 311, 'system:sensitive-word', 'admin', 'SensitiveWordList', 'menu.system.sensitive.word', 'lucide:book-x',
'f', 'f', '/system/sensitive-word/SensitiveWordList', '/system/sensitive/word', NULL, 1,
'f', 't', 'f', 1, 1, 0, 'f', 'menu',
NULL, NULL, NULL, NULL, NULL, NULL,
NOW(), NOW())
ON CONFLICT ("id") DO NOTHING;
INSERT INTO "public"."iam_perm_menu" VALUES
(31102, 311, 'system:sensitive-word-hit', 'admin', 'SensitiveWordHitList', 'menu.system.sensitive.hit', 'lucide:scan-search',
'f', 'f', '/system/sensitive-word/SensitiveWordHitList', '/system/sensitive/hit', NULL, 2,
'f', 't', 'f', 1, 1, 0, 'f', 'menu',
NULL, NULL, NULL, NULL, NULL, NULL,
NOW(), NOW())
ON CONFLICT ("id") DO NOTHING;
INSERT INTO "public"."pay_gateway_cashier_item" (
"id", "mch_no", "app_id", "cashier_type", "client_env",
"name", "icon", "recommend", "sort_no",
"resolve_mode", "method", "channel_mch_no", "capability",
"creator", "create_time", "last_modifier", "last_modified_time",
"version", "deleted"
) VALUES
-- H5 · browser
(9100000000000100001, 'M1781861282382', 'A7846150576259390', 'h5', 'browser',
'微信支付', 'wechat', 't', 1, 'method', 'wechat_h5', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100002, 'M1781861282382', 'A7846150576259390', 'h5', 'browser',
'支付宝', 'alipay', 'f', 2, 'method', 'alipay_h5', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100003, 'M1781861282382', 'A7846150576259390', 'h5', 'browser',
'云闪付', 'union_pay', 'f', 3, 'method', 'union_h5', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100004, 'M1781861282382', 'A7846150576259390', 'h5', 'browser',
'抖音支付', 'douyin', 'f', 4, 'method', 'douyin_h5', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
-- H5 · wechat
(9100000000000100005, 'M1781861282382', 'A7846150576259390', 'h5', 'wechat',
'微信支付', 'wechat', 't', 1, 'method', 'wechat_jsapi', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100006, 'M1781861282382', 'A7846150576259390', 'h5', 'wechat',
'微信扫码', 'wechat', 'f', 2, 'method', 'wechat_qr', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
-- H5 · alipay
(9100000000000100007, 'M1781861282382', 'A7846150576259390', 'h5', 'alipay',
'支付宝', 'alipay', 't', 1, 'method', 'alipay_jsapi', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100008, 'M1781861282382', 'A7846150576259390', 'h5', 'alipay',
'支付宝扫码', 'alipay', 'f', 2, 'method', 'alipay_qr', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
-- H5 · union_pay
(9100000000000100009, 'M1781861282382', 'A7846150576259390', 'h5', 'union_pay',
'云闪付', 'union_pay', 't', 1, 'method', 'union_jsapi', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100010, 'M1781861282382', 'A7846150576259390', 'h5', 'union_pay',
'银联扫码', 'union_pay', 'f', 2, 'method', 'union_qr', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
-- H5 · douyin
(9100000000000100011, 'M1781861282382', 'A7846150576259390', 'h5', 'douyin',
'抖音支付', 'douyin', 't', 1, 'method', 'douyin_jsapi', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100012, 'M1781861282382', 'A7846150576259390', 'h5', 'douyin',
'抖音扫码', 'douyin', 'f', 2, 'method', 'douyin_qr', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
-- MINI · wechat
(9100000000000100013, 'M1781861282382', 'A7846150576259390', 'mini', 'wechat',
'微信支付', 'wechat', 't', 1, 'method', 'wechat_mini', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100014, 'M1781861282382', 'A7846150576259390', 'mini', 'wechat',
'微信扫码', 'wechat', 'f', 2, 'method', 'wechat_qr', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
-- MINI · alipay
(9100000000000100015, 'M1781861282382', 'A7846150576259390', 'mini', 'alipay',
'支付宝', 'alipay', 't', 1, 'method', 'alipay_jsapi', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100016, 'M1781861282382', 'A7846150576259390', 'mini', 'alipay',
'支付宝扫码', 'alipay', 'f', 2, 'method', 'alipay_qr', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
-- MINI · union_pay
(9100000000000100017, 'M1781861282382', 'A7846150576259390', 'mini', 'union_pay',
'云闪付', 'union_pay', 't', 1, 'method', 'union_jsapi', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100018, 'M1781861282382', 'A7846150576259390', 'mini', 'union_pay',
'银联扫码', 'union_pay', 'f', 2, 'method', 'union_qr', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
-- MINI · douyin
(9100000000000100019, 'M1781861282382', 'A7846150576259390', 'mini', 'douyin',
'抖音支付', 'douyin', 't', 1, 'method', 'douyin_jsapi', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100020, 'M1781861282382', 'A7846150576259390', 'mini', 'douyin',
'抖音扫码', 'douyin', 'f', 2, 'method', 'douyin_qr', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
-- WEB · 扁平(client_env 为空)
(9100000000000100021, 'M1781861282382', 'A7846150576259390', 'web', NULL,
'微信扫码', 'wechat', 't', 1, 'method', 'wechat_qr', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100022, 'M1781861282382', 'A7846150576259390', 'web', NULL,
'支付宝扫码', 'alipay', 'f', 2, 'method', 'alipay_qr', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100023, 'M1781861282382', 'A7846150576259390', 'web', NULL,
'支付宝电脑', 'alipay', 'f', 3, 'method', 'alipay_pc', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100024, 'M1781861282382', 'A7846150576259390', 'web', NULL,
'银联扫码', 'union_pay', 'f', 4, 'method', 'union_qr', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100025, 'M1781861282382', 'A7846150576259390', 'web', NULL,
'抖音扫码', 'douyin', 'f', 5, 'method', 'douyin_qr', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'),
(9100000000000100026, 'M1781861282382', 'A7846150576259390', 'web', NULL,
'微信 H5', 'wechat', 'f', 6, 'method', 'wechat_h5', NULL, NULL,
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f');

View File

@@ -208,3 +208,58 @@ CREATE INDEX IF NOT EXISTS "idx_system_sensitive_word_hit_time" ON "public"."sys
CREATE INDEX IF NOT EXISTS "idx_system_sensitive_word_hit_word" ON "public"."system_sensitive_word_hit" ("hit_word");
CREATE INDEX IF NOT EXISTS "idx_system_sensitive_word_hit_mch" ON "public"."system_sensitive_word_hit" ("mch_no", "create_time");
CREATE INDEX IF NOT EXISTS "idx_system_sensitive_word_hit_scene" ON "public"."system_sensitive_word_hit" ("scene");
-- ----------------------------
-- Table structure for pay_gateway_cashier_item
-- 网关收银台支付项配置(应用级): H5 按 client_env 五档 / MINI 四档 / WEB 扁平(client_env 空)
-- ----------------------------
CREATE TABLE IF NOT EXISTS "public"."pay_gateway_cashier_item" (
"id" int8 NOT NULL,
"mch_no" varchar(32) COLLATE "pg_catalog"."default" NOT NULL,
"app_id" varchar(32) COLLATE "pg_catalog"."default" NOT NULL,
"cashier_type" varchar(16) COLLATE "pg_catalog"."default" NOT NULL,
"client_env" varchar(32) COLLATE "pg_catalog"."default",
"name" varchar(64) COLLATE "pg_catalog"."default" NOT NULL,
"icon" varchar(32) COLLATE "pg_catalog"."default",
"recommend" bool NOT NULL DEFAULT false,
"sort_no" int4 DEFAULT 0,
"resolve_mode" varchar(16) COLLATE "pg_catalog"."default" NOT NULL,
"method" varchar(32) COLLATE "pg_catalog"."default",
"channel_mch_no" varchar(64) COLLATE "pg_catalog"."default",
"capability" varchar(64) COLLATE "pg_catalog"."default",
"creator" int8,
"create_time" timestamptz(6),
"last_modifier" int8,
"last_modified_time" timestamptz(6),
"version" int4 DEFAULT 0,
"deleted" bool NOT NULL DEFAULT false
);
COMMENT ON TABLE "public"."pay_gateway_cashier_item" IS '网关收银台支付项配置(应用级)';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."id" IS '主键';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."mch_no" IS '商户号';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."app_id" IS '应用号';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."cashier_type" IS '收银台类型: h5/web/mini';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."client_env" IS '客户端环境(H5五档/MINI四档; WEB为空)';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."name" IS '前台展示名称';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."icon" IS '图标编码(与 PayProvider 对齐)';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."recommend" IS '是否推荐';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."sort_no" IS '排序号(越小越前)';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."resolve_mode" IS '解析模式: method/direct';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."method" IS '支付方式(METHOD 模式)';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."channel_mch_no" IS '通道商户号(DIRECT 模式)';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."capability" IS '支付能力(DIRECT 模式)';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."creator" IS '创建者ID';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."create_time" IS '创建时间';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."last_modifier" IS '最后修改者ID';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."last_modified_time" IS '最后修改时间';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."version" IS '版本号';
COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."deleted" IS '删除标志';
ALTER TABLE "public"."pay_gateway_cashier_item" DROP CONSTRAINT IF EXISTS "pay_gateway_cashier_item_pkey";
ALTER TABLE "public"."pay_gateway_cashier_item" ADD CONSTRAINT "pay_gateway_cashier_item_pkey" PRIMARY KEY ("id");
CREATE INDEX IF NOT EXISTS "idx_pay_gateway_cashier_item_bucket"
ON "public"."pay_gateway_cashier_item" ("app_id", "cashier_type", "client_env");
CREATE INDEX IF NOT EXISTS "idx_pay_gateway_cashier_item_mch"
ON "public"."pay_gateway_cashier_item" ("mch_no", "app_id");

View File

@@ -0,0 +1,93 @@
package cn.daxpay.open.payment.unipay.aop;
import cn.daxpay.open.platform.core.exception.NonceInvalidException;
import cn.daxpay.open.platform.core.exception.NonceMissingException;
import cn.daxpay.open.platform.core.exception.TimestampExpiredException;
import cn.daxpay.open.platform.iam.auth.service.IamSecurityConfigService;
import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformApiSecurityConfig;
import cn.daxpay.open.payment.unipay.param.PaymentCommonParam;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.OffsetDateTime;
/// # 开放支付防重放校验服务
///
/// 依据平台 API 安全配置([PlatformApiSecurityConfig])对开放支付接口请求做防重放校验:
/// - **Nonce 一次性消费**: 商户在签名时自行生成 `nonceStr`,平台使用 Redis SETNX 语义保证同一
/// nonce 在 TTL 内仅能被消费一次,第二次到达即判定为重放并拒绝。
/// - **请求时间窗口校验**: `reqTime` 与服务器当前时间的差值(绝对值)超过配置阈值则拒绝,
/// 双向校验既防过期请求也防未来时间请求。
///
/// 与平台登录用的 [cn.daxpay.open.platform.capability.nonce.service.NonceService] 不同:
/// 登录是「平台签发 nonce → 消费时删除」的预存模式;支付是「商户自生成 nonce → SETNX 判首次」
/// 的去重模式,两者语义不同故独立实现。
///
/// 校验顺序: Nonce 缺失检查 → Nonce 一次性消费 → 请求时间窗口。
@Slf4j
@Service
@RequiredArgsConstructor
public class PaymentReplayProtectService {
private final IamSecurityConfigService iamSecurityConfigService;
private final StringRedisTemplate stringRedisTemplate;
/// Redis key 前缀,带 `pay` 命名空间与登录 nonce 隔离
private static final String PAY_NONCE_PREFIX = "nonce:pay:";
/// 执行防重放校验(配置全关时直接放行)
///
/// @param param 支付公共参数(含 nonceStr / reqTime
/// @param mchNo 商户号(用于 nonce key 隔离,防跨商户碰撞)
public void verify(PaymentCommonParam param, String mchNo) {
PlatformApiSecurityConfig config = iamSecurityConfigService.getApiSecurityConfig();
if (config == null) {
return;
}
// Nonce 防重放
if (Boolean.TRUE.equals(config.getNonceVerifyEnabled())) {
verifyNonce(param.getNonceStr(), mchNo, config.getNonceTtlSeconds());
}
// 请求时间窗口校验
if (Boolean.TRUE.equals(config.getReqTimeoutEnabled())) {
verifyReqTime(param.getReqTime(), config.getReqTimeoutSeconds());
}
}
/// Nonce 一次性消费校验SETNX 语义)
private void verifyNonce(String nonceStr, String mchNo, Integer ttlSeconds) {
if (StrUtil.isBlank(nonceStr)) {
// Nonce 缺失
throw new NonceMissingException();
}
int ttl = ttlSeconds == null || ttlSeconds < 1 ? 300 : ttlSeconds;
// key 带商户号隔离,避免不同商户使用相同 nonceStr 时误判
String key = PAY_NONCE_PREFIX + mchNo + ":" + nonceStr;
Boolean firstOccupy = stringRedisTemplate.opsForValue()
.setIfAbsent(key, "1", Duration.ofSeconds(ttl));
if (!Boolean.TRUE.equals(firstOccupy)) {
// 已存在 = 重放请求
log.warn("支付接口Nonce重放拦截, mchNo: {}, nonce: {}", mchNo, nonceStr);
throw new NonceInvalidException();
}
}
/// 请求时间窗口校验(双向绝对值)
private void verifyReqTime(OffsetDateTime reqTime, Integer timeoutSeconds) {
if (reqTime == null) {
// reqTime 由 @NotNull 已校验,兜底
throw new TimestampExpiredException();
}
int tolerance = timeoutSeconds == null || timeoutSeconds < 1 ? 300 : timeoutSeconds;
OffsetDateTime now = OffsetDateTime.now();
long diffSeconds = Math.abs(Duration.between(reqTime, now).getSeconds());
if (diffSeconds > tolerance) {
log.warn("支付接口请求时间超出窗口, reqTime: {}, 偏差: {}s, 允许: {}s", reqTime, diffSeconds, tolerance);
throw new TimestampExpiredException();
}
}
}

View File

@@ -30,6 +30,7 @@ import java.time.ZoneOffset;
@RequiredArgsConstructor
public class PaymentVerifyAspect {
private final PaymentSignService paymentSignService;
private final PaymentReplayProtectService paymentReplayProtectService;
private final MerchantContextLoader merchantContextLoader;
/// 处理方法上的@PaymentVerify注解
@@ -59,6 +60,8 @@ public class PaymentVerifyAspect {
merchantContextLoader.initMch(paymentParam.getMchNo());
// 参数签名校验
paymentSignService.signVerify(paymentParam);
// 防重放校验Nonce一次性消费 + 请求时间窗口受平台API安全配置开关控制
paymentReplayProtectService.verify(paymentParam, paymentParam.getMchNo());
} else {
// 参数需要继承MerchantPaymentCommonParam
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.verify.paramExtendRequired");

View File

@@ -1,15 +1,17 @@
package cn.daxpay.open.platform.capability.nonce.aop;
import cn.daxpay.open.platform.capability.nonce.config.NonceVerificationConfigProvider;
import cn.daxpay.open.platform.capability.nonce.service.NonceService;
import cn.daxpay.open.platform.core.annotation.NonceVerification;
import cn.daxpay.open.platform.core.code.WebHeaderCode;
import cn.daxpay.open.platform.core.exception.NonceMissingException;
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
@@ -17,13 +19,24 @@ import org.springframework.web.context.request.ServletRequestAttributes;
/// # 防重放Nonce验证切面
///
/// 配置来源(依赖倒置): 若容器中注册了 [NonceVerificationConfigProvider] 实现:
/// - enabled=false 时跳过所有校验(配置级总开关)
/// - timestampTolerance 从配置读取,覆盖注解参数
/// 若无实现,则走注解参数默认值,保持向后兼容。
@Aspect
@Component
@Order
@RequiredArgsConstructor
@Slf4j
public class NonceVerificationAspect {
private final NonceService nonceService;
private final ObjectProvider<NonceVerificationConfigProvider> configProviderProvider;
public NonceVerificationAspect(NonceService nonceService,
ObjectProvider<NonceVerificationConfigProvider> configProviderProvider) {
this.nonceService = nonceService;
this.configProviderProvider = configProviderProvider;
}
/// 处理方法上的@NonceVerification注解
@Around("@annotation(nonceVerification)")
@@ -39,6 +52,12 @@ public class NonceVerificationAspect {
/// 验证逻辑
private Object doVerify(ProceedingJoinPoint pjp, NonceVerification nonceVerification) throws Throwable {
// 配置级总开关provider 存在且 enabled=false 时跳过校验
NonceVerificationConfigProvider provider = configProviderProvider.getIfAvailable();
if (provider != null && !provider.isEnabled()) {
return pjp.proceed();
}
HttpServletRequest request = getRequest();
String nonce = request.getHeader(WebHeaderCode.X_NONCE);
String timestampStr = request.getHeader(WebHeaderCode.X_TIMESTAMP);
@@ -56,8 +75,17 @@ public class NonceVerificationAspect {
timestamp = 0L;
}
// 时间戳容差: provider 优先(全局配置),回退注解参数(方法级)
int timestampTolerance = nonceVerification.timestampTolerance();
if (provider != null) {
int configTolerance = provider.getTimestampToleranceSeconds();
if (configTolerance > 0) {
timestampTolerance = configTolerance;
}
}
// 验证nonce和时间戳
nonceService.verify(nonce, timestamp, nonceVerification.timestampTolerance());
nonceService.verify(nonce, timestamp, timestampTolerance);
return pjp.proceed();
}

View File

@@ -0,0 +1,24 @@
package cn.daxpay.open.platform.capability.nonce.config;
/// # Nonce防重放校验配置提供者依赖倒置接口
///
/// capability-nonce 作为底层能力模块不依赖上层配置存储service-system
/// 通过此接口由上层(如 service-iam实现配置注入实现依赖倒置。
///
/// 实现方应注册为 Spring Beancapability-nonce 通过
/// [org.springframework.beans.factory.ObjectProvider] 做 optional 注入:
/// - **有实现 bean**: 从实现读取配置,实现全局可配置
/// - **无实现 bean**: 走 [cn.daxpay.open.platform.core.annotation.NonceVerification] 注解默认值,向后兼容
///
/// 配置优先级: provider全局配置> 注解参数(方法级覆盖)> 默认值 300
public interface NonceVerificationConfigProvider {
/// 是否启用防重放校验(关闭后所有 @NonceVerification 标记的接口跳过校验)
boolean isEnabled();
/// Nonce有效期签发/nonce/generate与消费共用此 TTL
int getNonceTimeoutSeconds();
/// 时间戳允许偏差(秒),请求时间戳与服务器时间差超过此值则拒绝
int getTimestampToleranceSeconds();
}

View File

@@ -1,11 +1,12 @@
package cn.daxpay.open.platform.capability.nonce.service;
import cn.daxpay.open.platform.capability.nonce.config.NonceVerificationConfigProvider;
import cn.daxpay.open.platform.capability.nonce.result.NonceResult;
import cn.daxpay.open.platform.core.exception.NonceInvalidException;
import cn.daxpay.open.platform.core.exception.TimestampExpiredException;
import cn.hutool.core.lang.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@@ -13,14 +14,40 @@ import java.time.Duration;
/// # Nonce生成与验证服务
///
/// 配置来源(依赖倒置): 若容器中注册了 [NonceVerificationConfigProvider] 实现,
/// 则从其读取 nonce 有效期等参数;否则使用默认值 300 秒,保持向后兼容。
@Slf4j
@Service
@RequiredArgsConstructor
public class NonceService {
private final StringRedisTemplate stringRedisTemplate;
private final ObjectProvider<NonceVerificationConfigProvider> configProviderProvider;
private static final String NONCE_PREFIX = "nonce:";
private static final int DEFAULT_TIMEOUT_SECONDS = 300;
public NonceService(StringRedisTemplate stringRedisTemplate,
ObjectProvider<NonceVerificationConfigProvider> configProviderProvider) {
this.stringRedisTemplate = stringRedisTemplate;
this.configProviderProvider = configProviderProvider;
}
/// 从配置提供者读取 nonce 有效期,无实现时回退默认值
private int resolveTimeout() {
NonceVerificationConfigProvider provider = configProviderProvider.getIfAvailable();
if (provider != null) {
int timeout = provider.getNonceTimeoutSeconds();
if (timeout > 0) {
return timeout;
}
}
return DEFAULT_TIMEOUT_SECONDS;
}
/// 生成Nonce并存入Redis有效期从配置读取
public NonceResult generate() {
return generate(resolveTimeout());
}
/// 生成Nonce并存入Redis
/// @param timeout nonce有效期
@@ -33,11 +60,6 @@ public class NonceService {
.setTimestamp(timestamp);
}
/// 生成Nonce使用默认5分钟有效期
public NonceResult generate() {
return generate(300);
}
/// 验证Nonce和时间戳
/// @param nonce nonce值
/// @param timestamp 请求时间戳(毫秒)
@@ -65,4 +87,3 @@ public class NonceService {
}
}

View File

@@ -6,5 +6,8 @@
"url": "System URL settings",
"website": "Website display settings",
"wechat_notify": "WeChat message notify template config",
"sensitive_word": "Sensitive word policy"
"sensitive_word": "Sensitive word policy",
"api_security": "API Security Config",
"pay_security": "Pay Security Config",
"iam_replay_protect": "IAM Anti-Replay Config"
}

View File

@@ -69,7 +69,8 @@
"codeClientEnvBlank": "Client environment code cannot be blank",
"codeClientEnvNotConfigured": "Payment method not configured for this client env and form",
"codeRuntimeMismatch": "QR program type does not match runtime",
"codeClientEnvChannelMchRequired": "Channel merchant required for each env and form in DIRECT mode"
"codeClientEnvChannelMchRequired": "Channel merchant required for each env and form in DIRECT mode",
"returnPathInvalid": "Invalid auth return path"
},
"refund": {
"processing": "Refund processing, do not repeat",

View File

@@ -759,6 +759,36 @@
},
"matchMode": {
"size": "Match mode must be at most 16 characters"
},
"nonceVerifyEnabled": {
"notNull": "Anti-replay nonce verification toggle cannot be empty"
},
"reqTimeoutEnabled": {
"notNull": "Request timestamp window verification toggle cannot be empty"
},
"reqTimeoutSeconds": {
"min": "Request timestamp tolerance cannot be less than 1 second"
},
"nonceTtlSeconds": {
"min": "Nonce TTL cannot be less than 1 second"
},
"replayProtectEnabled": {
"notNull": "Anti-replay verification toggle cannot be empty"
},
"nonceTimeoutSeconds": {
"min": "Nonce TTL cannot be less than 1 second"
},
"timestampToleranceSeconds": {
"min": "Timestamp tolerance cannot be less than 1 second"
},
"riskEnabled": {
"notNull": "Risk control master toggle cannot be empty"
},
"riskBlockBeforePay": {
"notNull": "Block-on-hit toggle cannot be empty"
},
"riskCheckAfterPay": {
"notNull": "Post-pay logging toggle cannot be empty"
}
}
}

View File

@@ -5,5 +5,7 @@
"security_two_factor_auth": "Otentikasi dua faktor",
"url": "Pengaturan URL sistem",
"website": "Pengaturan tampilan situs web",
"wechat_notify": "Pesan WeChat memberitahukan konfigurasi templat"
"wechat_notify": "Pesan WeChat memberitahukan konfigurasi templat",
"api_security": "Konfigurasi Keamanan API",
"iam_replay_protect": "Konfigurasi Anti-Replay IAM"
}

View File

@@ -69,7 +69,8 @@
"codeClientEnvBlank": "Client environment code cannot be blank",
"codeClientEnvNotConfigured": "Payment method not configured for this client env and form",
"codeRuntimeMismatch": "QR program type does not match runtime",
"codeClientEnvChannelMchRequired": "Channel merchant required for each env and form in DIRECT mode"
"codeClientEnvChannelMchRequired": "Channel merchant required for each env and form in DIRECT mode",
"returnPathInvalid": "Invalid auth return path"
},
"refund": {
"processing": "Proses pengembalian dana, jangan ulangi",

View File

@@ -746,6 +746,27 @@
},
"programType": {
"notBlank": "Program type cannot be blank"
},
"nonceVerifyEnabled": {
"notNull": "Sakelar verifikasi nonce anti-replay tidak boleh kosong"
},
"reqTimeoutEnabled": {
"notNull": "Sakelar verifikasi jendela waktu permintaan tidak boleh kosong"
},
"reqTimeoutSeconds": {
"min": "Toleransi waktu permintaan tidak boleh kurang dari 1 detik"
},
"nonceTtlSeconds": {
"min": "TTL nonce tidak boleh kurang dari 1 detik"
},
"replayProtectEnabled": {
"notNull": "Sakelar verifikasi anti-replay tidak boleh kosong"
},
"nonceTimeoutSeconds": {
"min": "TTL nonce tidak boleh kurang dari 1 detik"
},
"timestampToleranceSeconds": {
"min": "Toleransi stempel waktu tidak boleh kurang dari 1 detik"
}
}
}

View File

@@ -6,5 +6,8 @@
"url": "システムURL設定",
"website": "Webサイトの表示設定",
"wechat_notify": "WeChat メッセージ通知テンプレートの構成",
"sensitive_word": "センシティブワード設定"
"sensitive_word": "センシティブワード設定",
"api_security": "APIセキュリティ設定",
"pay_security": "決済セキュリティ設定",
"iam_replay_protect": "IAMドメインリプレイ防止設定"
}

View File

@@ -69,7 +69,8 @@
"codeClientEnvBlank": "クライアント環境コードは必須です",
"codeClientEnvNotConfigured": "この環境と形態には支払方法が設定されていません",
"codeRuntimeMismatch": "QRのプログラム種別とランタイムが一致しません",
"codeClientEnvChannelMchRequired": "DIRECT モードでは各環境・形態のチャネル加盟店番号が必須です"
"codeClientEnvChannelMchRequired": "DIRECT モードでは各環境・形態のチャネル加盟店番号が必須です",
"returnPathInvalid": "認可後の戻りパスが不正です"
},
"refund": {
"processing": "返金処理、繰り返さない",

View File

@@ -759,6 +759,36 @@
},
"matchMode": {
"size": "一致モードは16文字以内です"
},
"nonceVerifyEnabled": {
"notNull": "Nonceリプレイ防止検証の有効/無効は必須です"
},
"reqTimeoutEnabled": {
"notNull": "リクエスト時間枠検証の有効/無効は必須です"
},
"reqTimeoutSeconds": {
"min": "リクエスト時間許容誤差は1秒以上である必要があります"
},
"nonceTtlSeconds": {
"min": "Nonce有効期間は1秒以上である必要があります"
},
"replayProtectEnabled": {
"notNull": "リプレイ防止検証の有効/無効は必須です"
},
"nonceTimeoutSeconds": {
"min": "Nonce有効期間は1秒以上である必要があります"
},
"timestampToleranceSeconds": {
"min": "タイムスタンプ許容誤差は1秒以上である必要があります"
},
"riskEnabled": {
"notNull": "リスク管理のメインスイッチは必須です"
},
"riskBlockBeforePay": {
"notNull": "ヒット時ブロックスイッチは必須です"
},
"riskCheckAfterPay": {
"notNull": "決済後記録スイッチは必須です"
}
}
}

View File

@@ -6,5 +6,8 @@
"url": "시스템 URL 설정",
"website": "웹사이트 표시 설정",
"wechat_notify": "WeChat 메시지 알림 템플릿 구성",
"sensitive_word": "민감어 정책 설정"
"sensitive_word": "민감어 정책 설정",
"api_security": "API 보안 설정",
"pay_security": "결제 보안 설정",
"iam_replay_protect": "IAM 도메인 재생 방지 설정"
}

View File

@@ -69,7 +69,8 @@
"codeClientEnvBlank": "클라이언트 환경 코드는 비울 수 없습니다",
"codeClientEnvNotConfigured": "해당 환경과 형태에 결제 수단이 구성되지 않았습니다",
"codeRuntimeMismatch": "코드 프로그램 유형과 런타임이 일치하지 않습니다",
"codeClientEnvChannelMchRequired": "DIRECT 모드에서 각 환경·형태의 채널 가맹점이 필요합니다"
"codeClientEnvChannelMchRequired": "DIRECT 모드에서 각 환경·형태의 채널 가맹점이 필요합니다",
"returnPathInvalid": "인증 콜백 경로가 올바르지 않습니다"
},
"refund": {
"processing": "환불 처리 중입니다. 반복하지 마세요.",

View File

@@ -759,6 +759,36 @@
},
"matchMode": {
"size": "일치 모드는 16자 이내여야 합니다"
},
"nonceVerifyEnabled": {
"notNull": "논스 재생 방지 검증 활성화 여부는 필수입니다"
},
"reqTimeoutEnabled": {
"notNull": "요청 시간 윈도우 검증 활성화 여부는 필수입니다"
},
"reqTimeoutSeconds": {
"min": "요청 시간 허용 오차는 1초 이상이어야 합니다"
},
"nonceTtlSeconds": {
"min": "논스 유효 기간은 1초 이상이어야 합니다"
},
"replayProtectEnabled": {
"notNull": "재생 방지 검증 활성화 여부는 필수입니다"
},
"nonceTimeoutSeconds": {
"min": "논스 유효 기간은 1초 이상이어야 합니다"
},
"timestampToleranceSeconds": {
"min": "타임스탬프 허용 오차는 1초 이상이어야 합니다"
},
"riskEnabled": {
"notNull": "리스크 관리 마스터 스위치는 필수입니다"
},
"riskBlockBeforePay": {
"notNull": "히트 시 차단 스위치는 필수입니다"
},
"riskCheckAfterPay": {
"notNull": "결제 후 기록 스위치는 필수입니다"
}
}
}

View File

@@ -5,5 +5,7 @@
"security_two_factor_auth": "Pengesahan dua faktor",
"url": "Tetapan URL sistem",
"website": "Tetapan paparan laman web",
"wechat_notify": "Konfigurasi templat pemberitahuan mesej WeChat"
"wechat_notify": "Konfigurasi templat pemberitahuan mesej WeChat",
"api_security": "Konfigurasi Keselamatan API",
"iam_replay_protect": "Konfigurasi Anti-Main Semula IAM"
}

View File

@@ -69,7 +69,8 @@
"codeClientEnvBlank": "Client environment code cannot be blank",
"codeClientEnvNotConfigured": "Payment method not configured for this client env and form",
"codeRuntimeMismatch": "QR program type does not match runtime",
"codeClientEnvChannelMchRequired": "Channel merchant required for each env and form in DIRECT mode"
"codeClientEnvChannelMchRequired": "Channel merchant required for each env and form in DIRECT mode",
"returnPathInvalid": "Invalid auth return path"
},
"refund": {
"processing": "Pemprosesan bayaran balik, jangan ulangi",

View File

@@ -746,6 +746,27 @@
},
"programType": {
"notBlank": "Program type cannot be blank"
},
"nonceVerifyEnabled": {
"notNull": "Togol pengesahan nonce anti-replay tidak boleh kosong"
},
"reqTimeoutEnabled": {
"notNull": "Togol pengesahan tetingkap masa permintaan tidak boleh kosong"
},
"reqTimeoutSeconds": {
"min": "Toleransi masa permintaan tidak boleh kurang daripada 1 saat"
},
"nonceTtlSeconds": {
"min": "TTL nonce tidak boleh kurang daripada 1 saat"
},
"replayProtectEnabled": {
"notNull": "Togol pengesahan anti-main semula tidak boleh kosong"
},
"nonceTimeoutSeconds": {
"min": "TTL nonce tidak boleh kurang daripada 1 saat"
},
"timestampToleranceSeconds": {
"min": "Toleransi cap masa tidak boleh kurang daripada 1 saat"
}
}
}

View File

@@ -5,5 +5,7 @@
"security_two_factor_auth": "การรับรองความถูกต้องด้วยสองปัจจัย",
"url": "การตั้งค่า URL ของระบบ",
"website": "การตั้งค่าการแสดงผลเว็บไซต์",
"wechat_notify": "ข้อความ WeChat แจ้งเตือนการกำหนดค่าเทมเพลต"
"wechat_notify": "ข้อความ WeChat แจ้งเตือนการกำหนดค่าเทมเพลต",
"api_security": "การกำหนดค่าความปลอดภัย API",
"iam_replay_protect": "การกำหนดค่าการป้องกันการเล่นซ้ำ IAM"
}

View File

@@ -69,7 +69,8 @@
"codeClientEnvBlank": "Client environment code cannot be blank",
"codeClientEnvNotConfigured": "Payment method not configured for this client env and form",
"codeRuntimeMismatch": "QR program type does not match runtime",
"codeClientEnvChannelMchRequired": "Channel merchant required for each env and form in DIRECT mode"
"codeClientEnvChannelMchRequired": "Channel merchant required for each env and form in DIRECT mode",
"returnPathInvalid": "Invalid auth return path"
},
"refund": {
"processing": "ดำเนินการคืนเงินอย่าทำซ้ำ",

View File

@@ -746,6 +746,27 @@
},
"programType": {
"notBlank": "Program type cannot be blank"
},
"nonceVerifyEnabled": {
"notNull": "ไม่สามารถเว้นว่างการเปิด/ปิดการตรวจสอบ nonce ป้องกันการเล่นซ้ำได้"
},
"reqTimeoutEnabled": {
"notNull": "ไม่สามารถเว้นว่างการเปิด/ปิดการตรวจสอบหน้าต่างเวลาคำขอได้"
},
"reqTimeoutSeconds": {
"min": "ค่าคลาดเคลื่อนเวลาคำขอต้องไม่น้อยกว่า 1 วินาที"
},
"nonceTtlSeconds": {
"min": "อายุการใช้งาน nonce ต้องไม่น้อยกว่า 1 วินาที"
},
"replayProtectEnabled": {
"notNull": "ไม่สามารถเว้นว่างการเปิด/ปิดการตรวจสอบการป้องกันการเล่นซ้ำได้"
},
"nonceTimeoutSeconds": {
"min": "อายุการใช้งาน nonce ต้องไม่น้อยกว่า 1 วินาที"
},
"timestampToleranceSeconds": {
"min": "ค่าคลาดเคลื่อนการประทับเวลาต้องไม่น้อยกว่า 1 วินาที"
}
}
}

View File

@@ -5,5 +5,7 @@
"security_two_factor_auth": "Xác thực hai yếu tố",
"url": "Cài đặt URL hệ thống",
"website": "Cài đặt hiển thị trang web",
"wechat_notify": "Cấu hình mẫu thông báo tin nhắn WeChat"
"wechat_notify": "Cấu hình mẫu thông báo tin nhắn WeChat",
"api_security": "Cấu hình bảo mật API",
"iam_replay_protect": "Cấu hình chống phát lại IAM"
}

View File

@@ -69,7 +69,8 @@
"codeClientEnvBlank": "Client environment code cannot be blank",
"codeClientEnvNotConfigured": "Payment method not configured for this client env and form",
"codeRuntimeMismatch": "QR program type does not match runtime",
"codeClientEnvChannelMchRequired": "Channel merchant required for each env and form in DIRECT mode"
"codeClientEnvChannelMchRequired": "Channel merchant required for each env and form in DIRECT mode",
"returnPathInvalid": "Invalid auth return path"
},
"refund": {
"processing": "Xử lý hoàn tiền, không lặp lại",

View File

@@ -746,6 +746,27 @@
},
"programType": {
"notBlank": "Program type cannot be blank"
},
"nonceVerifyEnabled": {
"notNull": "Không được để trống bật/tắt xác thực nonce chống phát lại"
},
"reqTimeoutEnabled": {
"notNull": "Không được để trống bật/tắt xác thực cửa sổ thời gian yêu cầu"
},
"reqTimeoutSeconds": {
"min": "Dung sai thời gian yêu cầu không được nhỏ hơn 1 giây"
},
"nonceTtlSeconds": {
"min": "Thời gian sống của nonce không được nhỏ hơn 1 giây"
},
"replayProtectEnabled": {
"notNull": "Không được để trống bật/tắt xác thực chống phát lại"
},
"nonceTimeoutSeconds": {
"min": "Thời gian sống của nonce không được nhỏ hơn 1 giây"
},
"timestampToleranceSeconds": {
"min": "Dung sai dấu thời gian không được nhỏ hơn 1 giây"
}
}
}

View File

@@ -6,5 +6,8 @@
"url": "系统访问地址配置",
"website": "站点显示内容配置",
"wechat_notify": "微信消息通知模板配置",
"sensitive_word": "敏感词策略配置"
"sensitive_word": "敏感词策略配置",
"api_security": "API安全配置",
"pay_security": "支付安全配置",
"iam_replay_protect": "IAM域防重放配置"
}

View File

@@ -69,7 +69,8 @@
"codeClientEnvBlank": "客户端环境编码不能为空",
"codeClientEnvNotConfigured": "该客户端环境与支付形态未配置支付方式",
"codeRuntimeMismatch": "码牌落地类型与运行形态不匹配",
"codeClientEnvChannelMchRequired": "直接指定时,已填写行的通道商户号必填"
"codeClientEnvChannelMchRequired": "直接指定时,已填写行的通道商户号必填",
"returnPathInvalid": "授权回跳路径不合法"
},
"refund": {
"processing": "退款处理中,请勿重复操作",

View File

@@ -759,6 +759,36 @@
},
"matchMode": {
"size": "匹配模式不可超过16位"
},
"nonceVerifyEnabled": {
"notNull": "是否启用Nonce防重放校验不能为空"
},
"reqTimeoutEnabled": {
"notNull": "是否启用请求时间窗口校验不能为空"
},
"reqTimeoutSeconds": {
"min": "请求时间窗口容差不能小于1秒"
},
"nonceTtlSeconds": {
"min": "Nonce有效期不能小于1秒"
},
"replayProtectEnabled": {
"notNull": "是否启用防重放校验不能为空"
},
"nonceTimeoutSeconds": {
"min": "Nonce有效期不能小于1秒"
},
"timestampToleranceSeconds": {
"min": "时间戳容差不能小于1秒"
},
"riskEnabled": {
"notNull": "风控总开关不能为空"
},
"riskBlockBeforePay": {
"notNull": "命中阻断开关不能为空"
},
"riskCheckAfterPay": {
"notNull": "事后补录开关不能为空"
}
}
}

View File

@@ -6,5 +6,8 @@
"url": "系統訪問地址配置",
"website": "站點顯示內容配置",
"wechat_notify": "微信消息通知模板配置",
"sensitive_word": "敏感詞策略配置"
"sensitive_word": "敏感詞策略配置",
"api_security": "API安全配置",
"pay_security": "支付安全配置",
"iam_replay_protect": "IAM域防重放配置"
}

View File

@@ -69,7 +69,8 @@
"codeClientEnvBlank": "用戶端環境編碼不能為空",
"codeClientEnvNotConfigured": "該用戶端環境與支付形態未配置支付方式",
"codeRuntimeMismatch": "碼牌落地類型與運行形態不匹配",
"codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填"
"codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填",
"returnPathInvalid": "授權回跳路徑不合法"
},
"refund": {
"processing": "退款處理中,請勿重複操作",

View File

@@ -759,6 +759,36 @@
},
"matchMode": {
"size": "匹配模式不可超過16位"
},
"nonceVerifyEnabled": {
"notNull": "是否啟用Nonce防重放校驗不能為空"
},
"reqTimeoutEnabled": {
"notNull": "是否啟用請求時間窗口校驗不能為空"
},
"reqTimeoutSeconds": {
"min": "請求時間窗口容差不能小於1秒"
},
"nonceTtlSeconds": {
"min": "Nonce有效期不能小於1秒"
},
"replayProtectEnabled": {
"notNull": "是否啟用防重放校驗不能為空"
},
"nonceTimeoutSeconds": {
"min": "Nonce有效期不能小於1秒"
},
"timestampToleranceSeconds": {
"min": "時間戳容差不能小於1秒"
},
"riskEnabled": {
"notNull": "風控總開關不能為空"
},
"riskBlockBeforePay": {
"notNull": "命中阻斷開關不能為空"
},
"riskCheckAfterPay": {
"notNull": "事後補錄開關不能為空"
}
}
}

View File

@@ -6,5 +6,8 @@
"url": "系統訪問地址配置",
"website": "站點顯示內容配置",
"wechat_notify": "微信訊息通知模板配置",
"sensitive_word": "敏感詞策略配置"
"sensitive_word": "敏感詞策略配置",
"api_security": "API安全配置",
"pay_security": "支付安全配置",
"iam_replay_protect": "IAM域防重放配置"
}

View File

@@ -69,7 +69,8 @@
"codeClientEnvBlank": "用戶端環境編碼不能為空",
"codeClientEnvNotConfigured": "該用戶端環境與支付形態未配置支付方式",
"codeRuntimeMismatch": "碼牌落地類型與運行形態不匹配",
"codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填"
"codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填",
"returnPathInvalid": "授權回跳路徑不合法"
},
"refund": {
"processing": "退款處理中,請勿重複操作",

View File

@@ -759,6 +759,36 @@
},
"matchMode": {
"size": "匹配模式不可超過16位"
},
"nonceVerifyEnabled": {
"notNull": "是否啟用Nonce防重放校驗不能為空"
},
"reqTimeoutEnabled": {
"notNull": "是否啟用請求時間窗口校驗不能為空"
},
"reqTimeoutSeconds": {
"min": "請求時間窗口容差不能小於1秒"
},
"nonceTtlSeconds": {
"min": "Nonce有效期不能小於1秒"
},
"replayProtectEnabled": {
"notNull": "是否啟用防重放校驗不能為空"
},
"nonceTimeoutSeconds": {
"min": "Nonce有效期不能小於1秒"
},
"timestampToleranceSeconds": {
"min": "時間戳容差不能小於1秒"
},
"riskEnabled": {
"notNull": "風控總開關不能為空"
},
"riskBlockBeforePay": {
"notNull": "命中阻斷開關不能為空"
},
"riskCheckAfterPay": {
"notNull": "事後補錄開關不能為空"
}
}
}

View File

@@ -370,6 +370,12 @@ public interface PermCodes {
/// 菜单编码
String MENU = "develop:auth";
}
/// 网关支付调试 menuCode=develop:gateway
interface Gateway {
/// 菜单编码
String MENU = "develop:gateway";
}
}
/// 设备管理(开源仅码牌)

View File

@@ -0,0 +1,42 @@
package cn.daxpay.open.platform.iam.auth.service;
import cn.daxpay.open.platform.capability.nonce.config.NonceVerificationConfigProvider;
import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformIamReplayProtectConfig;
import cn.daxpay.open.platform.system.service.config.security.PlatformSecurityConfigService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/// # IAM域 Nonce 防重放配置提供者
///
/// 实现 capability-nonce 的 [NonceVerificationConfigProvider] 接口(依赖倒置),
/// 从 [PlatformSecurityConfigService] 读取 IAM 域防重放配置,
/// 供 [cn.daxpay.open.platform.capability.nonce.aop.NonceVerificationAspect] 消费。
///
/// 默认值策略: 配置缺失时保持启用登录接口已在使用nonce 有效期/时间戳容差回退 300 秒。
@Service
@RequiredArgsConstructor
public class IamNonceVerificationConfigProvider implements NonceVerificationConfigProvider {
private final PlatformSecurityConfigService platformSecurityConfigService;
@Override
public boolean isEnabled() {
Boolean enabled = platformSecurityConfigService.getIamReplayProtectConfig().getEnabled();
// 默认启用,保持登录接口现有行为
return enabled == null || enabled;
}
@Override
public int getNonceTimeoutSeconds() {
PlatformIamReplayProtectConfig config = platformSecurityConfigService.getIamReplayProtectConfig();
Integer timeout = config.getNonceTimeoutSeconds();
return timeout == null || timeout < 1 ? 300 : timeout;
}
@Override
public int getTimestampToleranceSeconds() {
PlatformIamReplayProtectConfig config = platformSecurityConfigService.getIamReplayProtectConfig();
Integer tolerance = config.getTimestampToleranceSeconds();
return tolerance == null || tolerance < 1 ? 300 : tolerance;
}
}

View File

@@ -1,5 +1,6 @@
package cn.daxpay.open.platform.iam.auth.service;
import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformApiSecurityConfig;
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;
@@ -37,5 +38,10 @@ public class IamSecurityConfigService {
public PlatformTwoFactorAuthConfig getTwoFactorAuthConfig() {
return platformSecurityConfigService.getTwoFactorAuthConfig();
}
/// 获取API安全配置防重放走多级缓存
public PlatformApiSecurityConfig getApiSecurityConfig() {
return platformSecurityConfigService.getApiSecurityConfig();
}
}

View File

@@ -92,4 +92,49 @@ public class PlatformSecurityConfigController {
platformSecurityConfigService.updateTwoFactorAuthConfig(param);
return Res.ok();
}
@PermCode(code = PermCodes.Action.VIEW)
@Operation(summary = "获取API安全配置")
@GetMapping("/api-security/get")
public Result<PlatformApiSecurityConfigResult> getApiSecurityConfig() {
return Res.ok(platformSecurityConfigService.findApiSecurityConfig());
}
@PermCode(code = PermCodes.Action.MANAGE)
@Operation(summary = "更新API安全配置")
@PostMapping("/api-security/update")
public Result<Void> updateApiSecurityConfig(@RequestBody @Validated PlatformApiSecurityConfigParam param) {
platformSecurityConfigService.updateApiSecurityConfig(param);
return Res.ok();
}
@PermCode(code = PermCodes.Action.VIEW)
@Operation(summary = "获取IAM域防重放配置")
@GetMapping("/iam-replay-protect/get")
public Result<PlatformIamReplayProtectConfigResult> getIamReplayProtectConfig() {
return Res.ok(platformSecurityConfigService.findIamReplayProtectConfig());
}
@PermCode(code = PermCodes.Action.MANAGE)
@Operation(summary = "更新IAM域防重放配置")
@PostMapping("/iam-replay-protect/update")
public Result<Void> updateIamReplayProtectConfig(@RequestBody @Validated PlatformIamReplayProtectConfigParam param) {
platformSecurityConfigService.updateIamReplayProtectConfig(param);
return Res.ok();
}
@PermCode(code = PermCodes.Action.VIEW)
@Operation(summary = "获取支付安全配置")
@GetMapping("/pay-security/get")
public Result<PlatformPaySecurityConfigResult> getPaySecurityConfig() {
return Res.ok(platformSecurityConfigService.findPaySecurityConfig());
}
@PermCode(code = PermCodes.Action.MANAGE)
@Operation(summary = "更新支付安全配置")
@PostMapping("/pay-security/update")
public Result<Void> updatePaySecurityConfig(@RequestBody @Validated PlatformPaySecurityConfigParam param) {
platformSecurityConfigService.updatePaySecurityConfig(param);
return Res.ok();
}
}

View File

@@ -33,4 +33,19 @@ public interface PlatformSecurityConfigConvert {
PlatformTwoFactorAuthConfigResult toTwoFactorAuthResult(PlatformTwoFactorAuthConfig data);
void copy(PlatformTwoFactorAuthConfigParam param, @MappingTarget PlatformTwoFactorAuthConfig data);
// ========== API安全配置转换 ==========
PlatformApiSecurityConfigResult toApiSecurityResult(PlatformApiSecurityConfig data);
void copy(PlatformApiSecurityConfigParam param, @MappingTarget PlatformApiSecurityConfig data);
// ========== IAM域防重放配置转换 ==========
PlatformIamReplayProtectConfigResult toIamReplayProtectResult(PlatformIamReplayProtectConfig data);
void copy(PlatformIamReplayProtectConfigParam param, @MappingTarget PlatformIamReplayProtectConfig data);
// ========== 支付安全配置(风控开关)转换 ==========
PlatformPaySecurityConfigResult toPaySecurityResult(PlatformPaySecurityConfig data);
void copy(PlatformPaySecurityConfigParam param, @MappingTarget PlatformPaySecurityConfig data);
}

View File

@@ -0,0 +1,26 @@
package cn.daxpay.open.platform.system.entity.config.platform.security;
import lombok.Data;
import lombok.experimental.Accessors;
/// # API安全配置
///
/// 控制开放支付接口([cn.daxpay.open.payment.unipay.aop.PaymentVerify])的防重放校验:
/// - Nonce 一次性消费: 防止相同请求被重放
/// - 请求时间窗口校验: 防止过期请求被重放
@Data
@Accessors(chain = true)
public class PlatformApiSecurityConfig {
/// 是否启用 Nonce 防重放校验(商户自生成 nonceStr平台 SETNX 一次性消费)
private Boolean nonceVerifyEnabled = Boolean.FALSE;
/// 是否启用请求时间窗口校验reqTime 与服务器时间差值超过阈值则拒绝)
private Boolean reqTimeoutEnabled = Boolean.FALSE;
/// 请求时间窗口容差(秒),双向绝对值校验
private Integer reqTimeoutSeconds = 300;
/// Nonce 有效期(秒),即 Redis 缓存 TTL
private Integer nonceTtlSeconds = 300;
}

View File

@@ -0,0 +1,26 @@
package cn.daxpay.open.platform.system.entity.config.platform.security;
import lombok.Data;
import lombok.experimental.Accessors;
/// # IAM域防重放配置
///
/// 控制平台内部敏感操作(登录、注册、忘记密码、修改密码、关闭双因素认证等)的防重放校验,
/// 基于 [cn.daxpay.open.platform.capability.nonce.annotation] 体系(平台签发 nonce + 一次性消费)。
///
/// 与支付域的 [PlatformApiSecurityConfig] 区别:
/// - 本配置面向平台内部接口IAM 域nonce 由平台签发(挑战-应答模式)
/// - 支付配置面向开放支付 APInonce 由商户自生成(幂等去重模式)
@Data
@Accessors(chain = true)
public class PlatformIamReplayProtectConfig {
/// 是否启用防重放校验(默认开启,登录等接口已在使用)
private Boolean enabled = Boolean.TRUE;
/// Nonce有效期签发/nonce/generate与消费共用此 TTL
private Integer nonceTimeoutSeconds = 300;
/// 时间戳允许偏差(秒),请求时间戳与服务器时间差超过此值则拒绝
private Integer timestampToleranceSeconds = 300;
}

View File

@@ -0,0 +1,27 @@
package cn.daxpay.open.platform.system.entity.config.platform.security;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 支付安全配置
///
/// 控制支付交易链路的风控开关。风控检查器实现见
/// [cn.daxpay.open.platform.plugin.risk.strategy.DefaultPayRiskChecker]
/// 契约见 [cn.daxpay.open.payment.strategy.risk.PayRiskChecker]。
///
/// - 风控总开关: 关闭后支付主链路跳过所有风控检查
/// - 命中阻断: 支付前命中黑名单是否拒绝下单(关闭则仅记录不拦截)
/// - 事后补录: 支付成功后是否补充记录命中(用于事后分析,不阻断资金态)
@Data
@Accessors(chain = true)
public class PlatformPaySecurityConfig {
/// 风控总开关(关闭后所有风控检查跳过)
private Boolean riskEnabled = Boolean.TRUE;
/// 命中黑名单后是否阻断下单false=仅记录不拦截)
private Boolean riskBlockBeforePay = Boolean.TRUE;
/// 支付成功后是否补录命中(用于事后分析)
private Boolean riskCheckAfterPay = Boolean.TRUE;
}

View File

@@ -21,6 +21,12 @@ public enum PlatformConfigTypeEnum implements I18nSupport {
SECURITY_SESSION("security_session"),
/// 双因素认证配置
SECURITY_TWO_FACTOR_AUTH("security_two_factor_auth"),
/// API安全配置开放支付接口防重放
API_SECURITY("api_security"),
/// 支付安全配置(支付风控开关)
PAY_SECURITY("pay_security"),
/// IAM域防重放配置登录/注册/改密等敏感操作)
IAM_REPLAY_PROTECT("iam_replay_protect"),
/// 系统访问地址配置
URL("url"),
/// 站点显示内容配置(系统名/Logo/备案/版权等)

View File

@@ -0,0 +1,31 @@
package cn.daxpay.open.platform.system.param.config.security;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
/// # API安全配置参数
///
@Data
@Accessors(chain = true)
@Schema(title = "API安全配置参数")
public class PlatformApiSecurityConfigParam {
@Schema(description = "是否启用Nonce防重放校验")
@NotNull(message = "{validation.field.nonceVerifyEnabled.notNull}")
private Boolean nonceVerifyEnabled;
@Schema(description = "是否启用请求时间窗口校验")
@NotNull(message = "{validation.field.reqTimeoutEnabled.notNull}")
private Boolean reqTimeoutEnabled;
@Schema(description = "请求时间窗口容差(秒)")
@Min(value = 1, message = "{validation.field.reqTimeoutSeconds.min}")
private Integer reqTimeoutSeconds;
@Schema(description = "Nonce有效期")
@Min(value = 1, message = "{validation.field.nonceTtlSeconds.min}")
private Integer nonceTtlSeconds;
}

View File

@@ -0,0 +1,27 @@
package cn.daxpay.open.platform.system.param.config.security;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
/// # IAM域防重放配置参数
///
@Data
@Accessors(chain = true)
@Schema(title = "IAM域防重放配置参数")
public class PlatformIamReplayProtectConfigParam {
@Schema(description = "是否启用防重放校验")
@NotNull(message = "{validation.field.replayProtectEnabled.notNull}")
private Boolean enabled;
@Schema(description = "Nonce有效期")
@Min(value = 1, message = "{validation.field.nonceTimeoutSeconds.min}")
private Integer nonceTimeoutSeconds;
@Schema(description = "时间戳允许偏差(秒)")
@Min(value = 1, message = "{validation.field.timestampToleranceSeconds.min}")
private Integer timestampToleranceSeconds;
}

View File

@@ -0,0 +1,26 @@
package cn.daxpay.open.platform.system.param.config.security;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 支付安全配置参数
///
@Data
@Accessors(chain = true)
@Schema(title = "支付安全配置参数")
public class PlatformPaySecurityConfigParam {
@Schema(description = "风控总开关(关闭后所有风控检查跳过)")
@NotNull(message = "{validation.field.riskEnabled.notNull}")
private Boolean riskEnabled;
@Schema(description = "命中黑名单后是否阻断下单false=仅记录不拦截)")
@NotNull(message = "{validation.field.riskBlockBeforePay.notNull}")
private Boolean riskBlockBeforePay;
@Schema(description = "支付成功后是否补录命中(用于事后分析)")
@NotNull(message = "{validation.field.riskCheckAfterPay.notNull}")
private Boolean riskCheckAfterPay;
}

View File

@@ -0,0 +1,28 @@
package cn.daxpay.open.platform.system.result.config.security;
import cn.daxpay.open.platform.core.result.BaseResult;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # API安全配置结果
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "API安全配置结果")
public class PlatformApiSecurityConfigResult extends BaseResult {
@Schema(description = "是否启用Nonce防重放校验")
private Boolean nonceVerifyEnabled;
@Schema(description = "是否启用请求时间窗口校验")
private Boolean reqTimeoutEnabled;
@Schema(description = "请求时间窗口容差(秒)")
private Integer reqTimeoutSeconds;
@Schema(description = "Nonce有效期")
private Integer nonceTtlSeconds;
}

View File

@@ -0,0 +1,25 @@
package cn.daxpay.open.platform.system.result.config.security;
import cn.daxpay.open.platform.core.result.BaseResult;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # IAM域防重放配置结果
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "IAM域防重放配置结果")
public class PlatformIamReplayProtectConfigResult extends BaseResult {
@Schema(description = "是否启用防重放校验")
private Boolean enabled;
@Schema(description = "Nonce有效期")
private Integer nonceTimeoutSeconds;
@Schema(description = "时间戳允许偏差(秒)")
private Integer timestampToleranceSeconds;
}

View File

@@ -0,0 +1,25 @@
package cn.daxpay.open.platform.system.result.config.security;
import cn.daxpay.open.platform.core.result.BaseResult;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/// # 支付安全配置结果
///
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@Schema(title = "支付安全配置结果")
public class PlatformPaySecurityConfigResult extends BaseResult {
@Schema(description = "风控总开关(关闭后所有风控检查跳过)")
private Boolean riskEnabled;
@Schema(description = "命中黑名单后是否阻断下单false=仅记录不拦截)")
private Boolean riskBlockBeforePay;
@Schema(description = "支付成功后是否补录命中(用于事后分析)")
private Boolean riskCheckAfterPay;
}

View File

@@ -1,25 +1,43 @@
package cn.daxpay.open.platform.system.service.config.security;
import cn.daxpay.open.platform.common.json.util.JacksonUtil;
import cn.daxpay.open.platform.system.convert.config.security.PlatformSecurityConfigConvert;
import cn.daxpay.open.platform.system.entity.config.platform.security.*;
import cn.daxpay.open.platform.system.enums.PlatformConfigTypeEnum;
import cn.daxpay.open.platform.system.param.config.security.*;
import cn.daxpay.open.platform.system.result.config.security.*;
import cn.daxpay.open.platform.system.service.config.SystemPlatformConfigService;
import lombok.RequiredArgsConstructor;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
/// # 平台安全配置服务
///
/// 统一管理密码策略、登录安全、会话管理、双因素认证等安全类配置
/// 统一管理密码策略、登录安全、会话管理、双因素认证、API安全等安全类配置
/// API安全配置在支付切面高频读取使用多级缓存[system:api-security-config]
/// 缓存 JSON 字符串避免 L2 Redis 反序列化为 LinkedHashMap 的类型问题。
@Slf4j
@Service
@RequiredArgsConstructor
public class PlatformSecurityConfigService {
/// API安全配置缓存名L1 Caffeine + L2 Redis
public static final String API_SECURITY_CACHE_NAME = "system:api-security-config";
private final SystemPlatformConfigService systemConfigService;
/// 自注入,保证读 API 安全配置走 Spring 缓存代理
private final PlatformSecurityConfigService self;
public PlatformSecurityConfigService(
SystemPlatformConfigService systemConfigService,
@Lazy PlatformSecurityConfigService self) {
this.systemConfigService = systemConfigService;
this.self = self;
}
/// 获取密码策略配置
public PlatformPasswordPolicyConfig getPasswordPolicyConfig() {
return systemConfigService.getOrCreateConfig(PlatformConfigTypeEnum.SECURITY_PASSWORD_POLICY,
@@ -106,4 +124,141 @@ public class PlatformSecurityConfigService {
PlatformSecurityConfigConvert.CONVERT.copy(param, data);
systemConfigService.updateConfig(PlatformConfigTypeEnum.SECURITY_TWO_FACTOR_AUTH, data);
}
// ========== API安全配置防重放高频读取走多级缓存 ==========
/// 缓存配置 JSONL2 安全类型: Stringkey 带 :json 避免命中旧版 POJO 缓存)
@Cacheable(value = API_SECURITY_CACHE_NAME, key = "'current:json'")
public String getApiSecurityConfigJson() {
PlatformApiSecurityConfig config = systemConfigService.getOrCreateConfig(
PlatformConfigTypeEnum.API_SECURITY,
PlatformApiSecurityConfig.class,
defaultApiSecurityConfig());
if (config == null) {
config = defaultApiSecurityConfig();
}
return JacksonUtil.toJson(config);
}
/// 获取API安全配置实体从缓存JSON显式还原类型
public PlatformApiSecurityConfig getApiSecurityConfig() {
// 走 self 代理以命中缓存
String json = self.getApiSecurityConfigJson();
if (StrUtil.isBlank(json)) {
return defaultApiSecurityConfig();
}
PlatformApiSecurityConfig config = JacksonUtil.toBean(json, PlatformApiSecurityConfig.class);
return config == null ? defaultApiSecurityConfig() : config;
}
/// API安全配置默认值: 默认不启用,避免影响存量商户
private PlatformApiSecurityConfig defaultApiSecurityConfig() {
return new PlatformApiSecurityConfig()
.setNonceVerifyEnabled(false)
.setReqTimeoutEnabled(false)
.setReqTimeoutSeconds(300)
.setNonceTtlSeconds(300);
}
/// 获取API安全配置(结果)
public PlatformApiSecurityConfigResult findApiSecurityConfig() {
return PlatformSecurityConfigConvert.CONVERT.toApiSecurityResult(self.getApiSecurityConfig());
}
/// 更新API安全配置
@CacheEvict(value = API_SECURITY_CACHE_NAME, allEntries = true)
public void updateApiSecurityConfig(PlatformApiSecurityConfigParam param) {
PlatformApiSecurityConfig data = systemConfigService.getOrCreateConfig(
PlatformConfigTypeEnum.API_SECURITY,
PlatformApiSecurityConfig.class,
defaultApiSecurityConfig());
PlatformSecurityConfigConvert.CONVERT.copy(param, data);
systemConfigService.updateConfig(PlatformConfigTypeEnum.API_SECURITY, data);
}
// ========== IAM域防重放配置登录/注册/改密等敏感操作,走多级缓存) ==========
/// IAM域防重放配置缓存名L1 Caffeine + L2 Redis
public static final String IAM_REPLAY_PROTECT_CACHE_NAME = "system:iam-replay-protect-config";
/// 缓存配置 JSON
@Cacheable(value = IAM_REPLAY_PROTECT_CACHE_NAME, key = "'current:json'")
public String getIamReplayProtectConfigJson() {
PlatformIamReplayProtectConfig config = systemConfigService.getOrCreateConfig(
PlatformConfigTypeEnum.IAM_REPLAY_PROTECT,
PlatformIamReplayProtectConfig.class,
defaultIamReplayProtectConfig());
if (config == null) {
config = defaultIamReplayProtectConfig();
}
return JacksonUtil.toJson(config);
}
/// 获取IAM域防重放配置实体从缓存JSON显式还原类型
public PlatformIamReplayProtectConfig getIamReplayProtectConfig() {
String json = self.getIamReplayProtectConfigJson();
if (StrUtil.isBlank(json)) {
return defaultIamReplayProtectConfig();
}
PlatformIamReplayProtectConfig config = JacksonUtil.toBean(json, PlatformIamReplayProtectConfig.class);
return config == null ? defaultIamReplayProtectConfig() : config;
}
/// IAM域防重放配置默认值: 默认启用(登录接口已在使用),保持向后兼容
private PlatformIamReplayProtectConfig defaultIamReplayProtectConfig() {
return new PlatformIamReplayProtectConfig()
.setEnabled(true)
.setNonceTimeoutSeconds(300)
.setTimestampToleranceSeconds(300);
}
/// 获取IAM域防重放配置(结果)
public PlatformIamReplayProtectConfigResult findIamReplayProtectConfig() {
return PlatformSecurityConfigConvert.CONVERT.toIamReplayProtectResult(self.getIamReplayProtectConfig());
}
/// 更新IAM域防重放配置
@CacheEvict(value = IAM_REPLAY_PROTECT_CACHE_NAME, allEntries = true)
public void updateIamReplayProtectConfig(PlatformIamReplayProtectConfigParam param) {
PlatformIamReplayProtectConfig data = systemConfigService.getOrCreateConfig(
PlatformConfigTypeEnum.IAM_REPLAY_PROTECT,
PlatformIamReplayProtectConfig.class,
defaultIamReplayProtectConfig());
PlatformSecurityConfigConvert.CONVERT.copy(param, data);
systemConfigService.updateConfig(PlatformConfigTypeEnum.IAM_REPLAY_PROTECT, data);
}
// ========== 支付安全配置(风控开关,支付主链路按此决定是否调用风控检查器) ==========
/// 获取支付安全配置实体
public PlatformPaySecurityConfig getPaySecurityConfig() {
PlatformPaySecurityConfig config = systemConfigService.getOrCreateConfig(
PlatformConfigTypeEnum.PAY_SECURITY,
PlatformPaySecurityConfig.class,
defaultPaySecurityConfig());
if (config == null) {
config = defaultPaySecurityConfig();
}
return config;
}
/// 支付安全配置默认值: 默认开启风控、命中阻断、事后补录
private PlatformPaySecurityConfig defaultPaySecurityConfig() {
return new PlatformPaySecurityConfig()
.setRiskEnabled(true)
.setRiskBlockBeforePay(true)
.setRiskCheckAfterPay(true);
}
/// 获取支付安全配置(结果)
public PlatformPaySecurityConfigResult findPaySecurityConfig() {
return PlatformSecurityConfigConvert.CONVERT.toPaySecurityResult(this.getPaySecurityConfig());
}
/// 更新支付安全配置
public void updatePaySecurityConfig(PlatformPaySecurityConfigParam param) {
PlatformPaySecurityConfig data = this.getPaySecurityConfig();
PlatformSecurityConfigConvert.CONVERT.copy(param, data);
systemConfigService.updateConfig(PlatformConfigTypeEnum.PAY_SECURITY, data);
}
}