mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-09 14:36:00 +08:00
feat(security): 平台安全配置拆分并增强 nonce/防重放
将平台安全配置拆为 API/IAM/支付多份;nonce 校验可配置化;补齐 SQL、权限码与多语 validation/i18n。
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,28 +1,105 @@
|
||||
-- 敏感词菜单(系统管理 · 方案 A):catalog + 词库 + 命中
|
||||
-- 策略开关在「平台配置」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');
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.daxpay.open.platform.capability.nonce.config;
|
||||
|
||||
/// # Nonce防重放校验配置提供者(依赖倒置接口)
|
||||
///
|
||||
/// capability-nonce 作为底层能力模块,不依赖上层配置存储(service-system)。
|
||||
/// 通过此接口由上层(如 service-iam)实现配置注入,实现依赖倒置。
|
||||
///
|
||||
/// 实现方应注册为 Spring Bean,capability-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();
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,8 @@
|
||||
"url": "システムURL設定",
|
||||
"website": "Webサイトの表示設定",
|
||||
"wechat_notify": "WeChat メッセージ通知テンプレートの構成",
|
||||
"sensitive_word": "センシティブワード設定"
|
||||
"sensitive_word": "センシティブワード設定",
|
||||
"api_security": "APIセキュリティ設定",
|
||||
"pay_security": "決済セキュリティ設定",
|
||||
"iam_replay_protect": "IAMドメインリプレイ防止設定"
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@
|
||||
"codeClientEnvBlank": "クライアント環境コードは必須です",
|
||||
"codeClientEnvNotConfigured": "この環境と形態には支払方法が設定されていません",
|
||||
"codeRuntimeMismatch": "QRのプログラム種別とランタイムが一致しません",
|
||||
"codeClientEnvChannelMchRequired": "DIRECT モードでは各環境・形態のチャネル加盟店番号が必須です"
|
||||
"codeClientEnvChannelMchRequired": "DIRECT モードでは各環境・形態のチャネル加盟店番号が必須です",
|
||||
"returnPathInvalid": "認可後の戻りパスが不正です"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "返金処理、繰り返さない",
|
||||
|
||||
@@ -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": "決済後記録スイッチは必須です"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,8 @@
|
||||
"url": "시스템 URL 설정",
|
||||
"website": "웹사이트 표시 설정",
|
||||
"wechat_notify": "WeChat 메시지 알림 템플릿 구성",
|
||||
"sensitive_word": "민감어 정책 설정"
|
||||
"sensitive_word": "민감어 정책 설정",
|
||||
"api_security": "API 보안 설정",
|
||||
"pay_security": "결제 보안 설정",
|
||||
"iam_replay_protect": "IAM 도메인 재생 방지 설정"
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@
|
||||
"codeClientEnvBlank": "클라이언트 환경 코드는 비울 수 없습니다",
|
||||
"codeClientEnvNotConfigured": "해당 환경과 형태에 결제 수단이 구성되지 않았습니다",
|
||||
"codeRuntimeMismatch": "코드 프로그램 유형과 런타임이 일치하지 않습니다",
|
||||
"codeClientEnvChannelMchRequired": "DIRECT 모드에서 각 환경·형태의 채널 가맹점이 필요합니다"
|
||||
"codeClientEnvChannelMchRequired": "DIRECT 모드에서 각 환경·형태의 채널 가맹점이 필요합니다",
|
||||
"returnPathInvalid": "인증 콜백 경로가 올바르지 않습니다"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "환불 처리 중입니다. 반복하지 마세요.",
|
||||
|
||||
@@ -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": "결제 후 기록 스위치는 필수입니다"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,7 @@
|
||||
"security_two_factor_auth": "การรับรองความถูกต้องด้วยสองปัจจัย",
|
||||
"url": "การตั้งค่า URL ของระบบ",
|
||||
"website": "การตั้งค่าการแสดงผลเว็บไซต์",
|
||||
"wechat_notify": "ข้อความ WeChat แจ้งเตือนการกำหนดค่าเทมเพลต"
|
||||
"wechat_notify": "ข้อความ WeChat แจ้งเตือนการกำหนดค่าเทมเพลต",
|
||||
"api_security": "การกำหนดค่าความปลอดภัย API",
|
||||
"iam_replay_protect": "การกำหนดค่าการป้องกันการเล่นซ้ำ IAM"
|
||||
}
|
||||
|
||||
@@ -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": "ดำเนินการคืนเงินอย่าทำซ้ำ",
|
||||
|
||||
@@ -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 วินาที"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,8 @@
|
||||
"url": "系统访问地址配置",
|
||||
"website": "站点显示内容配置",
|
||||
"wechat_notify": "微信消息通知模板配置",
|
||||
"sensitive_word": "敏感词策略配置"
|
||||
"sensitive_word": "敏感词策略配置",
|
||||
"api_security": "API安全配置",
|
||||
"pay_security": "支付安全配置",
|
||||
"iam_replay_protect": "IAM域防重放配置"
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@
|
||||
"codeClientEnvBlank": "客户端环境编码不能为空",
|
||||
"codeClientEnvNotConfigured": "该客户端环境与支付形态未配置支付方式",
|
||||
"codeRuntimeMismatch": "码牌落地类型与运行形态不匹配",
|
||||
"codeClientEnvChannelMchRequired": "直接指定时,已填写行的通道商户号必填"
|
||||
"codeClientEnvChannelMchRequired": "直接指定时,已填写行的通道商户号必填",
|
||||
"returnPathInvalid": "授权回跳路径不合法"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "退款处理中,请勿重复操作",
|
||||
|
||||
@@ -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": "事后补录开关不能为空"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,8 @@
|
||||
"url": "系統訪問地址配置",
|
||||
"website": "站點顯示內容配置",
|
||||
"wechat_notify": "微信消息通知模板配置",
|
||||
"sensitive_word": "敏感詞策略配置"
|
||||
"sensitive_word": "敏感詞策略配置",
|
||||
"api_security": "API安全配置",
|
||||
"pay_security": "支付安全配置",
|
||||
"iam_replay_protect": "IAM域防重放配置"
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@
|
||||
"codeClientEnvBlank": "用戶端環境編碼不能為空",
|
||||
"codeClientEnvNotConfigured": "該用戶端環境與支付形態未配置支付方式",
|
||||
"codeRuntimeMismatch": "碼牌落地類型與運行形態不匹配",
|
||||
"codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填"
|
||||
"codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填",
|
||||
"returnPathInvalid": "授權回跳路徑不合法"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "退款處理中,請勿重複操作",
|
||||
|
||||
@@ -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": "事後補錄開關不能為空"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,8 @@
|
||||
"url": "系統訪問地址配置",
|
||||
"website": "站點顯示內容配置",
|
||||
"wechat_notify": "微信訊息通知模板配置",
|
||||
"sensitive_word": "敏感詞策略配置"
|
||||
"sensitive_word": "敏感詞策略配置",
|
||||
"api_security": "API安全配置",
|
||||
"pay_security": "支付安全配置",
|
||||
"iam_replay_protect": "IAM域防重放配置"
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@
|
||||
"codeClientEnvBlank": "用戶端環境編碼不能為空",
|
||||
"codeClientEnvNotConfigured": "該用戶端環境與支付形態未配置支付方式",
|
||||
"codeRuntimeMismatch": "碼牌落地類型與運行形態不匹配",
|
||||
"codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填"
|
||||
"codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填",
|
||||
"returnPathInvalid": "授權回跳路徑不合法"
|
||||
},
|
||||
"refund": {
|
||||
"processing": "退款處理中,請勿重複操作",
|
||||
|
||||
@@ -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": "事後補錄開關不能為空"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,6 +370,12 @@ public interface PermCodes {
|
||||
/// 菜单编码
|
||||
String MENU = "develop:auth";
|
||||
}
|
||||
|
||||
/// 网关支付调试 menuCode=develop:gateway
|
||||
interface Gateway {
|
||||
/// 菜单编码
|
||||
String MENU = "develop:gateway";
|
||||
}
|
||||
}
|
||||
|
||||
/// 设备管理(开源仅码牌)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 由平台签发(挑战-应答模式)
|
||||
/// - 支付配置面向开放支付 API,nonce 由商户自生成(幂等去重模式)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class PlatformIamReplayProtectConfig {
|
||||
|
||||
/// 是否启用防重放校验(默认开启,登录等接口已在使用)
|
||||
private Boolean enabled = Boolean.TRUE;
|
||||
|
||||
/// Nonce有效期(秒),签发(/nonce/generate)与消费共用此 TTL
|
||||
private Integer nonceTimeoutSeconds = 300;
|
||||
|
||||
/// 时间戳允许偏差(秒),请求时间戳与服务器时间差超过此值则拒绝
|
||||
private Integer timestampToleranceSeconds = 300;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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/备案/版权等)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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安全配置(防重放,高频读取,走多级缓存) ==========
|
||||
|
||||
/// 缓存配置 JSON(L2 安全类型: String;key 带 :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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user