diff --git a/_config/sql/iam_perm_menu.sql b/_config/sql/iam_perm_menu.sql index 4e9184726..28c728cdb 100644 --- a/_config/sql/iam_perm_menu.sql +++ b/_config/sql/iam_perm_menu.sql @@ -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 diff --git a/_config/sql/update-datas.sql b/_config/sql/update-datas.sql index ea5cc45e6..c3b3a729c 100644 --- a/_config/sql/update-datas.sql +++ b/_config/sql/update-datas.sql @@ -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'); diff --git a/_config/sql/update-tables.sql b/_config/sql/update-tables.sql index f9b713b4c..11676858b 100644 --- a/_config/sql/update-tables.sql +++ b/_config/sql/update-tables.sql @@ -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"); diff --git a/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/aop/PaymentReplayProtectService.java b/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/aop/PaymentReplayProtectService.java new file mode 100644 index 000000000..9c9105b8d --- /dev/null +++ b/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/aop/PaymentReplayProtectService.java @@ -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(); + } + } +} diff --git a/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/aop/PaymentVerifyAspect.java b/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/aop/PaymentVerifyAspect.java index 9c7ea7d74..6d25999c6 100644 --- a/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/aop/PaymentVerifyAspect.java +++ b/daxpay-payment/daxpay-payment-unipay/src/main/java/cn/daxpay/open/payment/unipay/aop/PaymentVerifyAspect.java @@ -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"); diff --git a/daxpay-platform/daxpay-platform-capability/capability-nonce/src/main/java/cn/daxpay/open/platform/capability/nonce/aop/NonceVerificationAspect.java b/daxpay-platform/daxpay-platform-capability/capability-nonce/src/main/java/cn/daxpay/open/platform/capability/nonce/aop/NonceVerificationAspect.java index f0cc29b34..4e5156aee 100644 --- a/daxpay-platform/daxpay-platform-capability/capability-nonce/src/main/java/cn/daxpay/open/platform/capability/nonce/aop/NonceVerificationAspect.java +++ b/daxpay-platform/daxpay-platform-capability/capability-nonce/src/main/java/cn/daxpay/open/platform/capability/nonce/aop/NonceVerificationAspect.java @@ -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 configProviderProvider; + + public NonceVerificationAspect(NonceService nonceService, + ObjectProvider 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(); } diff --git a/daxpay-platform/daxpay-platform-capability/capability-nonce/src/main/java/cn/daxpay/open/platform/capability/nonce/config/NonceVerificationConfigProvider.java b/daxpay-platform/daxpay-platform-capability/capability-nonce/src/main/java/cn/daxpay/open/platform/capability/nonce/config/NonceVerificationConfigProvider.java new file mode 100644 index 000000000..ba35990b6 --- /dev/null +++ b/daxpay-platform/daxpay-platform-capability/capability-nonce/src/main/java/cn/daxpay/open/platform/capability/nonce/config/NonceVerificationConfigProvider.java @@ -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(); +} diff --git a/daxpay-platform/daxpay-platform-capability/capability-nonce/src/main/java/cn/daxpay/open/platform/capability/nonce/service/NonceService.java b/daxpay-platform/daxpay-platform-capability/capability-nonce/src/main/java/cn/daxpay/open/platform/capability/nonce/service/NonceService.java index 504dc7214..00768b115 100644 --- a/daxpay-platform/daxpay-platform-capability/capability-nonce/src/main/java/cn/daxpay/open/platform/capability/nonce/service/NonceService.java +++ b/daxpay-platform/daxpay-platform-capability/capability-nonce/src/main/java/cn/daxpay/open/platform/capability/nonce/service/NonceService.java @@ -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 configProviderProvider; private static final String NONCE_PREFIX = "nonce:"; + private static final int DEFAULT_TIMEOUT_SECONDS = 300; + + public NonceService(StringRedisTemplate stringRedisTemplate, + ObjectProvider 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 { } } - diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/platform_config_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/platform_config_type.json index 7fae4d5dc..03131c1da 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/platform_config_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/platform_config_type.json @@ -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" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/error.json index adfde34c0..4df8f6ce1 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/error.json @@ -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", diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json index 5ce015acb..6483db3c5 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json @@ -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" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/platform_config_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/platform_config_type.json index 5eacee0af..80d9bc51d 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/platform_config_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/platform_config_type.json @@ -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" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/pay/error.json index aaf19e84c..0b91e0fd9 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/pay/error.json @@ -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", diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/validation.json index b38960410..204b71f9f 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/validation.json @@ -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" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/platform_config_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/platform_config_type.json index 6f7bea6df..994bb366c 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/platform_config_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/platform_config_type.json @@ -6,5 +6,8 @@ "url": "システムURL設定", "website": "Webサイトの表示設定", "wechat_notify": "WeChat メッセージ通知テンプレートの構成", - "sensitive_word": "センシティブワード設定" + "sensitive_word": "センシティブワード設定", + "api_security": "APIセキュリティ設定", + "pay_security": "決済セキュリティ設定", + "iam_replay_protect": "IAMドメインリプレイ防止設定" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/pay/error.json index 9f74ba1cb..376ac2d7d 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/pay/error.json @@ -69,7 +69,8 @@ "codeClientEnvBlank": "クライアント環境コードは必須です", "codeClientEnvNotConfigured": "この環境と形態には支払方法が設定されていません", "codeRuntimeMismatch": "QRのプログラム種別とランタイムが一致しません", - "codeClientEnvChannelMchRequired": "DIRECT モードでは各環境・形態のチャネル加盟店番号が必須です" + "codeClientEnvChannelMchRequired": "DIRECT モードでは各環境・形態のチャネル加盟店番号が必須です", + "returnPathInvalid": "認可後の戻りパスが不正です" }, "refund": { "processing": "返金処理、繰り返さない", diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/validation.json index d8c7b1d00..7e5e98412 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/validation.json @@ -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": "決済後記録スイッチは必須です" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/platform_config_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/platform_config_type.json index 33464e0e3..c803a1e94 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/platform_config_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/platform_config_type.json @@ -6,5 +6,8 @@ "url": "시스템 URL 설정", "website": "웹사이트 표시 설정", "wechat_notify": "WeChat 메시지 알림 템플릿 구성", - "sensitive_word": "민감어 정책 설정" + "sensitive_word": "민감어 정책 설정", + "api_security": "API 보안 설정", + "pay_security": "결제 보안 설정", + "iam_replay_protect": "IAM 도메인 재생 방지 설정" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/pay/error.json index 559f5f996..95ab1bdc1 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/pay/error.json @@ -69,7 +69,8 @@ "codeClientEnvBlank": "클라이언트 환경 코드는 비울 수 없습니다", "codeClientEnvNotConfigured": "해당 환경과 형태에 결제 수단이 구성되지 않았습니다", "codeRuntimeMismatch": "코드 프로그램 유형과 런타임이 일치하지 않습니다", - "codeClientEnvChannelMchRequired": "DIRECT 모드에서 각 환경·형태의 채널 가맹점이 필요합니다" + "codeClientEnvChannelMchRequired": "DIRECT 모드에서 각 환경·형태의 채널 가맹점이 필요합니다", + "returnPathInvalid": "인증 콜백 경로가 올바르지 않습니다" }, "refund": { "processing": "환불 처리 중입니다. 반복하지 마세요.", diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/validation.json index d18ebb004..d3bdad2aa 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/validation.json @@ -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": "결제 후 기록 스위치는 필수입니다" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/platform_config_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/platform_config_type.json index 072d17087..b52e81c60 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/platform_config_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/platform_config_type.json @@ -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" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/pay/error.json index 95ea59ed0..0ccdb7bed 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/pay/error.json @@ -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", diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/validation.json index f78271abb..48aeaf626 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/validation.json @@ -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" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/platform_config_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/platform_config_type.json index a3e4135b1..78138b3bd 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/platform_config_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/platform_config_type.json @@ -5,5 +5,7 @@ "security_two_factor_auth": "การรับรองความถูกต้องด้วยสองปัจจัย", "url": "การตั้งค่า URL ของระบบ", "website": "การตั้งค่าการแสดงผลเว็บไซต์", - "wechat_notify": "ข้อความ WeChat แจ้งเตือนการกำหนดค่าเทมเพลต" + "wechat_notify": "ข้อความ WeChat แจ้งเตือนการกำหนดค่าเทมเพลต", + "api_security": "การกำหนดค่าความปลอดภัย API", + "iam_replay_protect": "การกำหนดค่าการป้องกันการเล่นซ้ำ IAM" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/pay/error.json index 063c6c22b..0a5ec816c 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/pay/error.json @@ -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": "ดำเนินการคืนเงินอย่าทำซ้ำ", diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/validation.json index 4dbf74ea2..40d921a02 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/validation.json @@ -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 วินาที" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/platform_config_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/platform_config_type.json index ed7c405fa..716fe6e17 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/platform_config_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/platform_config_type.json @@ -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" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/pay/error.json index 7dfb61834..afc156441 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/pay/error.json @@ -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", diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/validation.json index 65850e2d4..ec831225a 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/validation.json @@ -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" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/platform_config_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/platform_config_type.json index 9d2bca3c4..976ae240b 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/platform_config_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/platform_config_type.json @@ -6,5 +6,8 @@ "url": "系统访问地址配置", "website": "站点显示内容配置", "wechat_notify": "微信消息通知模板配置", - "sensitive_word": "敏感词策略配置" + "sensitive_word": "敏感词策略配置", + "api_security": "API安全配置", + "pay_security": "支付安全配置", + "iam_replay_protect": "IAM域防重放配置" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/error.json index ff40bdfa9..a8c3d9f7a 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/error.json @@ -69,7 +69,8 @@ "codeClientEnvBlank": "客户端环境编码不能为空", "codeClientEnvNotConfigured": "该客户端环境与支付形态未配置支付方式", "codeRuntimeMismatch": "码牌落地类型与运行形态不匹配", - "codeClientEnvChannelMchRequired": "直接指定时,已填写行的通道商户号必填" + "codeClientEnvChannelMchRequired": "直接指定时,已填写行的通道商户号必填", + "returnPathInvalid": "授权回跳路径不合法" }, "refund": { "processing": "退款处理中,请勿重复操作", diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json index 8178afa82..e97b06dcf 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json @@ -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": "事后补录开关不能为空" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/platform_config_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/platform_config_type.json index ae139a8ea..75bc517a1 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/platform_config_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/platform_config_type.json @@ -6,5 +6,8 @@ "url": "系統訪問地址配置", "website": "站點顯示內容配置", "wechat_notify": "微信消息通知模板配置", - "sensitive_word": "敏感詞策略配置" + "sensitive_word": "敏感詞策略配置", + "api_security": "API安全配置", + "pay_security": "支付安全配置", + "iam_replay_protect": "IAM域防重放配置" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/pay/error.json index 3c5008044..4175e80fb 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/pay/error.json @@ -69,7 +69,8 @@ "codeClientEnvBlank": "用戶端環境編碼不能為空", "codeClientEnvNotConfigured": "該用戶端環境與支付形態未配置支付方式", "codeRuntimeMismatch": "碼牌落地類型與運行形態不匹配", - "codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填" + "codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填", + "returnPathInvalid": "授權回跳路徑不合法" }, "refund": { "processing": "退款處理中,請勿重複操作", diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/validation.json index 844c4d1e8..2e47dcf74 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/validation.json @@ -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": "事後補錄開關不能為空" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/platform_config_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/platform_config_type.json index 5b487c328..9baa8bd8d 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/platform_config_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/platform_config_type.json @@ -6,5 +6,8 @@ "url": "系統訪問地址配置", "website": "站點顯示內容配置", "wechat_notify": "微信訊息通知模板配置", - "sensitive_word": "敏感詞策略配置" + "sensitive_word": "敏感詞策略配置", + "api_security": "API安全配置", + "pay_security": "支付安全配置", + "iam_replay_protect": "IAM域防重放配置" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/pay/error.json index dd0d84d9e..d62d24732 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/pay/error.json @@ -69,7 +69,8 @@ "codeClientEnvBlank": "用戶端環境編碼不能為空", "codeClientEnvNotConfigured": "該用戶端環境與支付形態未配置支付方式", "codeRuntimeMismatch": "碼牌落地類型與運行形態不匹配", - "codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填" + "codeClientEnvChannelMchRequired": "直接指定時,每個環境與形態的通道商戶號必填", + "returnPathInvalid": "授權回跳路徑不合法" }, "refund": { "processing": "退款處理中,請勿重複操作", diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/validation.json index 4db5ec9e8..fac305109 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/validation.json @@ -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": "事後補錄開關不能為空" } } } diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/code/PermCodes.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/code/PermCodes.java index 57ab7bbc2..0f262d916 100644 --- a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/code/PermCodes.java +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/code/PermCodes.java @@ -370,6 +370,12 @@ public interface PermCodes { /// 菜单编码 String MENU = "develop:auth"; } + + /// 网关支付调试 menuCode=develop:gateway + interface Gateway { + /// 菜单编码 + String MENU = "develop:gateway"; + } } /// 设备管理(开源仅码牌) diff --git a/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/auth/service/IamNonceVerificationConfigProvider.java b/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/auth/service/IamNonceVerificationConfigProvider.java new file mode 100644 index 000000000..c5526b3bc --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/auth/service/IamNonceVerificationConfigProvider.java @@ -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; + } +} diff --git a/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/auth/service/IamSecurityConfigService.java b/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/auth/service/IamSecurityConfigService.java index 39387a990..f5dc983c1 100644 --- a/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/auth/service/IamSecurityConfigService.java +++ b/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/auth/service/IamSecurityConfigService.java @@ -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(); + } } diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/security/PlatformSecurityConfigController.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/security/PlatformSecurityConfigController.java index f95ad60df..bf1a95be2 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/security/PlatformSecurityConfigController.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/controller/config/security/PlatformSecurityConfigController.java @@ -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 getApiSecurityConfig() { + return Res.ok(platformSecurityConfigService.findApiSecurityConfig()); + } + + @PermCode(code = PermCodes.Action.MANAGE) + @Operation(summary = "更新API安全配置") + @PostMapping("/api-security/update") + public Result 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 getIamReplayProtectConfig() { + return Res.ok(platformSecurityConfigService.findIamReplayProtectConfig()); + } + + @PermCode(code = PermCodes.Action.MANAGE) + @Operation(summary = "更新IAM域防重放配置") + @PostMapping("/iam-replay-protect/update") + public Result updateIamReplayProtectConfig(@RequestBody @Validated PlatformIamReplayProtectConfigParam param) { + platformSecurityConfigService.updateIamReplayProtectConfig(param); + return Res.ok(); + } + + @PermCode(code = PermCodes.Action.VIEW) + @Operation(summary = "获取支付安全配置") + @GetMapping("/pay-security/get") + public Result getPaySecurityConfig() { + return Res.ok(platformSecurityConfigService.findPaySecurityConfig()); + } + + @PermCode(code = PermCodes.Action.MANAGE) + @Operation(summary = "更新支付安全配置") + @PostMapping("/pay-security/update") + public Result updatePaySecurityConfig(@RequestBody @Validated PlatformPaySecurityConfigParam param) { + platformSecurityConfigService.updatePaySecurityConfig(param); + return Res.ok(); + } } diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/convert/config/security/PlatformSecurityConfigConvert.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/convert/config/security/PlatformSecurityConfigConvert.java index 974f0f3a2..5acb12b5a 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/convert/config/security/PlatformSecurityConfigConvert.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/convert/config/security/PlatformSecurityConfigConvert.java @@ -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); } diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/security/PlatformApiSecurityConfig.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/security/PlatformApiSecurityConfig.java new file mode 100644 index 000000000..cae24c6e8 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/security/PlatformApiSecurityConfig.java @@ -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; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/security/PlatformIamReplayProtectConfig.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/security/PlatformIamReplayProtectConfig.java new file mode 100644 index 000000000..ac3d61416 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/security/PlatformIamReplayProtectConfig.java @@ -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; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/security/PlatformPaySecurityConfig.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/security/PlatformPaySecurityConfig.java new file mode 100644 index 000000000..d65da6a9b --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/security/PlatformPaySecurityConfig.java @@ -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; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/enums/PlatformConfigTypeEnum.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/enums/PlatformConfigTypeEnum.java index 6206fbd2b..40cfd04a2 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/enums/PlatformConfigTypeEnum.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/enums/PlatformConfigTypeEnum.java @@ -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/备案/版权等) diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/security/PlatformApiSecurityConfigParam.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/security/PlatformApiSecurityConfigParam.java new file mode 100644 index 000000000..ccbce7357 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/security/PlatformApiSecurityConfigParam.java @@ -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; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/security/PlatformIamReplayProtectConfigParam.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/security/PlatformIamReplayProtectConfigParam.java new file mode 100644 index 000000000..de7424727 --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/security/PlatformIamReplayProtectConfigParam.java @@ -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; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/security/PlatformPaySecurityConfigParam.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/security/PlatformPaySecurityConfigParam.java new file mode 100644 index 000000000..88d90f7bd --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/security/PlatformPaySecurityConfigParam.java @@ -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; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/security/PlatformApiSecurityConfigResult.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/security/PlatformApiSecurityConfigResult.java new file mode 100644 index 000000000..50c7ded0c --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/security/PlatformApiSecurityConfigResult.java @@ -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; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/security/PlatformIamReplayProtectConfigResult.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/security/PlatformIamReplayProtectConfigResult.java new file mode 100644 index 000000000..6d1d5dcfb --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/security/PlatformIamReplayProtectConfigResult.java @@ -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; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/security/PlatformPaySecurityConfigResult.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/security/PlatformPaySecurityConfigResult.java new file mode 100644 index 000000000..d1755752b --- /dev/null +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/security/PlatformPaySecurityConfigResult.java @@ -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; +} diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/security/PlatformSecurityConfigService.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/security/PlatformSecurityConfigService.java index d036f53df..e75c9f783 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/security/PlatformSecurityConfigService.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/security/PlatformSecurityConfigService.java @@ -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); + } }