| rencaika/config/api.uts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rencaika_merchant/config/api.uts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| talent-card-server/src/main/java/com/talentcard/server/config/ApiCorsConfig.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| talent-card-server/src/main/resources/application.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| talent-card-server/src/test/java/com/talentcard/server/config/LoosenedDevelopmentCorsTests.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
rencaika/config/api.uts
@@ -6,8 +6,9 @@ // 后台提供合法域名后仅在此处填写,业务页面不得硬编码域名。 // 开发者工具模拟器可用回环地址;真机必须用本机当前的局域网地址,并放行 Windows 防火墙的 8080。 // 本机以太网 IP 会随网络环境变化,换网后这里要跟着改(192.168.110.15 曾经写在这里但并非本机)。 const developmentBaseUrl = 'http://192.168.110.26:8080' // 本机局域网 IP 会随网络环境变化(见过 192.168.110.26 与 192.168.110.15),换网后这里要跟着改; // 写错了请求会直接失败——连不上那台机器,表现得像"登录不上"。 const developmentBaseUrl = 'http://192.168.110.15:8080' const stagingBaseUrl = '' const productionBaseUrl = '' rencaika_merchant/config/api.uts
@@ -15,9 +15,10 @@ export const useMerchantManagementDemoDataInDevelopment = false // 后台提供微信小程序合法 HTTPS 域名后,在对应环境填写完整基础地址。 // 真机(本机 WLAN 192.168.110.15)必须用局域网地址并放行 Windows 防火墙 8080; // 原值 192.168.110.26 不是本机,请求会直接失败。 const developmentBaseUrl = 'http://192.168.110.26:8080' // 真机与微信开发者工具都必须用**本机当前**的局域网地址,并放行 Windows 防火墙 8080。 // 本机 WLAN 地址会随网络环境变化(见过 192.168.110.26 与 192.168.110.15), // 换网后这里要跟着改;写错了请求会直接失败(连不上那台机器)。 const developmentBaseUrl = 'http://192.168.110.15:8080' const stagingBaseUrl = '' const productionBaseUrl = '' talent-card-server/src/main/java/com/talentcard/server/config/ApiCorsConfig.java
@@ -13,10 +13,14 @@ @Configuration public class ApiCorsConfig { @Bean public CorsFilter apiCorsFilter(@Value("${app.cors.allowed-origins:}") String allowedOrigins) { public CorsFilter apiCorsFilter( @Value("${app.cors.allowed-origins:}") String allowedOrigins, @Value("${app.cors.allowed-origin-patterns:}") String allowedOriginPatterns) { CorsConfiguration cors = new CorsConfiguration(); cors.setAllowedOrigins(Arrays.stream(allowedOrigins.split(",")) .map(String::trim).filter(origin -> !origin.isEmpty()).toList()); cors.setAllowedOrigins(split(allowedOrigins)); // 本地开发工具的端口是随机的(微信开发者工具每次启动都换一个),写死端口必然漏。 // 用 origin 通配覆盖本机各种调试端口,真实域名仍走上面的精确白名单。 cors.setAllowedOriginPatterns(split(allowedOriginPatterns)); cors.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); cors.setAllowedHeaders(List.of("Content-Type", "Authorization", "X-Request-Id", "X-Client-Platform", "X-Client-Version", "X-Merchant-Store-Id", "X-Idempotency-Key")); @@ -27,4 +31,8 @@ // Complete browser preflight before MVC authentication interceptors run. return new CorsFilter(source); } private static List<String> split(String values) { return Arrays.stream(values.split(",")).map(String::trim).filter(value -> !value.isEmpty()).toList(); } } talent-card-server/src/main/resources/application.yml
@@ -41,7 +41,11 @@ cors: # 后台管理端(vite dev,默认 5273)与 HBuilderX Web 调试来源;部署时用 CORS_ALLOWED_ORIGINS 覆盖为真实域名。 # localhost 与局域网地址都要列:换网段后局域网 IP 会变,localhost 是最稳的调试入口。 allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:5273,http://127.0.0.1:5273,http://localhost:7893,http://localhost:7894,http://127.0.0.1:7893,http://127.0.0.1:7894,http://192.168.110.26:5273,http://192.168.110.26:5173} allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:5273,http://127.0.0.1:5273,http://localhost:7893,http://localhost:7894,http://127.0.0.1:7893,http://127.0.0.1:7894,http://192.168.110.26:5273,http://192.168.110.26:,http://192.168.110.15:5273,http://192.168.110.15:5173} # 本机调试来源的端口是随机的:微信开发者工具每次启动都换一个端口(见过 42665), # HBuilderX 内置浏览器同理。写死端口必然漏,所以按 origin 通配匹配任意端口。 # 只放行 localhost / 127.0.0.1,不对公网来源开口子;真实域名请加进上面的精确白名单。 allowed-origin-patterns: ${CORS_ALLOWED_ORIGIN_PATTERNS:http://localhost:[*],http://127.0.0.1:[*]} # 核销后把延迟入账的钱确认给商户;确认失败或超时时由这个补偿任务重试,避免钱永远冻在延迟户。 privacy: # Production must provide APP_PRIVACY_ENCRYPTION_KEY through the deployment secret store. talent-card-server/src/test/java/com/talentcard/server/config/LoosenedDevelopmentCorsTests.java
New file @@ -0,0 +1,109 @@ package com.talentcard.server.config; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.ResponseEntity; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.filter.CorsFilter; import org.springframework.web.servlet.config.annotation.EnableWebMvc; /** * 本机调试来源的端口是随机的:微信开发者工具每次启动都换一个端口(实际见过 42665), * HBuilderX 内置浏览器同理。白名单里写死 7893/7894 这类端口必然漏,表现就是 * 小程序"登录不上"——预检返回 403、响应里没有 Access-Control-Allow-Origin。 * * <p>所以除精确白名单外,再用 origin 通配覆盖 localhost / 127.0.0.1 的任意端口。 * 这里同时钉住两点:通配**能**放行本机任意端口,且**不能**把公网来源一起放进来。 */ @SpringJUnitConfig({ApiCorsConfig.class, LoosenedDevelopmentCorsTests.TestMvcConfig.class}) @WebAppConfiguration @TestPropertySource(properties = { "app.cors.allowed-origins=http://192.168.110.15:5173", "app.cors.allowed-origin-patterns=http://localhost:[*],http://127.0.0.1:[*]" }) class LoosenedDevelopmentCorsTests { @Autowired private WebApplicationContext context; private MockMvc mvc; @BeforeEach void setup() { mvc = MockMvcBuilders.webAppContextSetup(context) .addFilters(context.getBeansOfType(CorsFilter.class).values().toArray(CorsFilter[]::new)) .build(); } @Test void wechatDevToolsRandomPortCanPreflightLogin() throws Exception { mvc.perform(options("/api/app/auth/login") .header("Origin", "http://127.0.0.1:42665") .header("Access-Control-Request-Method", "POST") .header("Access-Control-Request-Headers", "content-type,x-request-id")) .andExpect(status().isOk()) .andExpect(header().string("Access-Control-Allow-Origin", "http://127.0.0.1:42665")); } @Test void localhostRandomPortCanPreflightLogin() throws Exception { mvc.perform(options("/api/merchant/auth/login") .header("Origin", "http://localhost:52731") .header("Access-Control-Request-Method", "POST")) .andExpect(status().isOk()) .andExpect(header().string("Access-Control-Allow-Origin", "http://localhost:52731")); } @Test void theExactAllowListStillWorks() throws Exception { mvc.perform(options("/api/admin/auth/login") .header("Origin", "http://192.168.110.15:5173") .header("Access-Control-Request-Method", "POST")) .andExpect(status().isOk()) .andExpect(header().string("Access-Control-Allow-Origin", "http://192.168.110.15:5173")); } @Test void thePatternDoesNotOpenUpToPublicOrigins() throws Exception { // 通配只覆盖本机来源,外部域名必须继续被拒绝。 mvc.perform(post("/api/merchant/auth/login").header("Origin", "https://untrusted.example")) .andExpect(status().isForbidden()) .andExpect(header().doesNotExist("Access-Control-Allow-Origin")); mvc.perform(post("/api/merchant/auth/login").header("Origin", "http://192.168.110.99:8080")) .andExpect(status().isForbidden()) .andExpect(header().doesNotExist("Access-Control-Allow-Origin")); } @Configuration @EnableWebMvc static class TestMvcConfig { @Bean ProtectedEndpoint endpoint() { return new ProtectedEndpoint(); } } @RestController static class ProtectedEndpoint { @PostMapping("/api/merchant/auth/login") ResponseEntity<Void> login() { return ResponseEntity.status(401).build(); } } }