From 752fcaa38d849deaafcbbad1d70cb785d2f4000b Mon Sep 17 00:00:00 2001 From: malei <> Date: Sun, 2 Aug 2026 02:59:34 +0800 Subject: [PATCH] fix: enforce a single global cors policy --- README.md | 2 + deploy/production-smoke.sh | 51 +++++++++-- .../AdminBusinessEventController.java | 2 - .../AdminFollowUpTaskController.java | 2 - .../controller/AdminOnboardingController.java | 2 - .../AdminServiceCustomerController.java | 2 - .../controller/AdminStoreController.java | 2 - .../controller/AppointmentController.java | 1 - .../petstore/controller/FileController.java | 1 - .../MerchantOnboardingController.java | 2 - .../petstore/controller/PetController.java | 1 - .../petstore/controller/ReportController.java | 1 - .../controller/ReportLeadController.java | 1 - .../controller/ScheduleController.java | 1 - .../controller/ServiceTypeController.java | 1 - .../petstore/controller/SmsController.java | 1 - .../controller/StaffInvitationController.java | 2 - .../petstore/controller/StoreController.java | 1 - .../petstore/controller/UserController.java | 1 - .../config/CorsBoundaryContractTest.java | 85 +++++++++++++++++++ 20 files changed, 133 insertions(+), 29 deletions(-) create mode 100644 src/test/java/com/petstore/config/CorsBoundaryContractTest.java diff --git a/README.md b/README.md index e6bc4cd..2046004 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ | `SPRING_JPA_SHOW_SQL` | 否 | `false` | 生产 SQL 日志开关,必须为 `false` | | `LOG_LEVEL_PETSTORE` | 否 | `info` | `com.petstore` 包日志级别 | +CORS 只由全局 `CorsFilter` 和 `CORS_ALLOWED_ORIGINS` 控制;Controller 不得声明 `@CrossOrigin`。生产只读 smoke 必须同时提供白名单中的 `SMOKE_ALLOWED_ORIGIN`,并验证一个非白名单 Origin 返回 403。 + > ⚠️ **密钥轮换**:`application.yml` 历史版本曾提交过真实 DB 密码与微信 AppSecret。这些凭据已在仓库历史中暴露,**必须按安全流程轮换**:改 DB 密码、重置微信 AppSecret、更换 `PETSTORE_SESSION_SECRET`。 ## 本地开发 diff --git a/deploy/production-smoke.sh b/deploy/production-smoke.sh index 4f062c2..a54c52f 100755 --- a/deploy/production-smoke.sh +++ b/deploy/production-smoke.sh @@ -2,6 +2,8 @@ set -euo pipefail API_ORIGIN="${API_ORIGIN:-}" +SMOKE_ALLOWED_ORIGIN="${SMOKE_ALLOWED_ORIGIN:-}" +SMOKE_BLOCKED_ORIGIN="${SMOKE_BLOCKED_ORIGIN:-https://blocked.petstore.invalid}" SMOKE_BEARER_TOKEN="${SMOKE_BEARER_TOKEN:-}" [[ "$API_ORIGIN" == https://* ]] || { @@ -18,23 +20,48 @@ SMOKE_BEARER_TOKEN="${SMOKE_BEARER_TOKEN:-}" } API_ORIGIN="${API_ORIGIN%/}" +[[ "$SMOKE_ALLOWED_ORIGIN" == https://* ]] || { + printf 'SMOKE FAIL: SMOKE_ALLOWED_ORIGIN must be an https origin\n' >&2 + exit 1 +} +[[ "$SMOKE_ALLOWED_ORIGIN" != *localhost* && "$SMOKE_ALLOWED_ORIGIN" != *127.0.0.1* \ + && "$SMOKE_ALLOWED_ORIGIN" != *.invalid* && "$SMOKE_ALLOWED_ORIGIN" != *.example* \ + && "$SMOKE_ALLOWED_ORIGIN" != *.test* ]] || { + printf 'SMOKE FAIL: SMOKE_ALLOWED_ORIGIN must use a production domain\n' >&2 + exit 1 +} +SMOKE_ALLOWED_ORIGIN="${SMOKE_ALLOWED_ORIGIN%/}" +[[ "$SMOKE_BLOCKED_ORIGIN" != "$SMOKE_ALLOWED_ORIGIN" ]] || { + printf 'SMOKE FAIL: blocked and allowed origins must differ\n' >&2 + exit 1 +} +python3 -c 'import sys,urllib.parse +for raw in sys.argv[1:]: + u=urllib.parse.urlsplit(raw) + assert u.scheme == "https" and u.hostname and not u.username and not u.password + assert u.path in ("", "/") and not u.query and not u.fragment' \ + "$SMOKE_ALLOWED_ORIGIN" "$SMOKE_BLOCKED_ORIGIN" || { + printf 'SMOKE FAIL: CORS smoke values must be explicit https origins\n' >&2 + exit 1 + } + json_assert_status_up() { python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("status") == "UP", d; print("UP")' } -printf '1/5 liveness: ' +printf '1/7 liveness: ' curl --fail --silent --show-error "$API_ORIGIN/actuator/health/liveness" | json_assert_status_up -printf '2/5 readiness: ' +printf '2/7 readiness: ' curl --fail --silent --show-error "$API_ORIGIN/actuator/health/readiness" | json_assert_status_up STORES="$(curl --fail --silent --show-error "$API_ORIGIN/api/store/list")" STORE_ID="$(printf '%s' "$STORES" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("code")==200,d; rows=d.get("data") or []; assert rows,"no pilot store"; print(rows[0]["id"])')" -printf '3/5 public store list: PASS\n' +printf '3/7 public store list: PASS\n' SERVICES="$(curl --fail --silent --show-error "$API_ORIGIN/api/service-type/list?storeId=$STORE_ID")" SERVICE_NAME="$(printf '%s' "$SERVICES" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("code")==200,d; rows=d.get("data") or []; assert rows,"no service type"; assert all(int(x.get("durationMinutes") or 0)>=30 for x in rows),rows; print(rows[0]["name"])')" -printf '4/5 service duration contract: PASS\n' +printf '4/7 service duration contract: PASS\n' if date -v+1d +%F >/dev/null 2>&1; then TOMORROW="$(date -v+1d +%F)" @@ -45,7 +72,21 @@ curl --fail --silent --show-error --get "$API_ORIGIN/api/appointment/available-s --data-urlencode "storeId=$STORE_ID" \ --data-urlencode "date=$TOMORROW" \ --data-urlencode "serviceType=$SERVICE_NAME" \ - | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("code")==200,d; data=d["data"]; assert int(data["durationMinutes"])>=30; assert int(data["bookingCapacity"])>=1; assert isinstance(data.get("slots"),list); print("5/5 capacity slot contract: PASS")' + | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("code")==200,d; data=d["data"]; assert int(data["durationMinutes"])>=30; assert int(data["bookingCapacity"])>=1; assert isinstance(data.get("slots"),list); print("5/7 capacity slot contract: PASS")' + +curl --fail --silent --show-error --dump-header - --output /dev/null \ + -H "Origin: $SMOKE_ALLOWED_ORIGIN" \ + "$API_ORIGIN/api/store/list" \ + | SMOKE_EXPECTED_ORIGIN="$SMOKE_ALLOWED_ORIGIN" python3 -c 'import os,sys; expected=os.environ["SMOKE_EXPECTED_ORIGIN"].lower(); headers=sys.stdin.read().lower(); assert f"access-control-allow-origin: {expected}" in headers,headers; assert "access-control-allow-origin: *" not in headers,headers; print("6/7 allowed CORS origin: PASS")' + +BLOCKED_STATUS="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \ + -H "Origin: $SMOKE_BLOCKED_ORIGIN" \ + "$API_ORIGIN/api/store/list")" +[[ "$BLOCKED_STATUS" == "403" ]] || { + printf 'SMOKE FAIL: blocked CORS origin returned HTTP %s\n' "$BLOCKED_STATUS" >&2 + exit 1 +} +printf '7/7 blocked CORS origin: PASS\n' if [[ -n "$SMOKE_BEARER_TOKEN" ]]; then AUTH_HEADER="Authorization: Bearer $SMOKE_BEARER_TOKEN" diff --git a/src/main/java/com/petstore/controller/AdminBusinessEventController.java b/src/main/java/com/petstore/controller/AdminBusinessEventController.java index 8917e44..1184f60 100644 --- a/src/main/java/com/petstore/controller/AdminBusinessEventController.java +++ b/src/main/java/com/petstore/controller/AdminBusinessEventController.java @@ -6,7 +6,6 @@ import com.petstore.service.AdminBusinessEventService; import com.petstore.service.AdminWorkbenchService; import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -19,7 +18,6 @@ import java.util.Map; @RestController @RequestMapping("/api/admin/workbench") @RequiredArgsConstructor -@CrossOrigin public class AdminBusinessEventController { private final AdminBusinessEventService adminBusinessEventService; diff --git a/src/main/java/com/petstore/controller/AdminFollowUpTaskController.java b/src/main/java/com/petstore/controller/AdminFollowUpTaskController.java index 988a63f..a690cf3 100644 --- a/src/main/java/com/petstore/controller/AdminFollowUpTaskController.java +++ b/src/main/java/com/petstore/controller/AdminFollowUpTaskController.java @@ -6,7 +6,6 @@ import com.petstore.service.FlowException; import com.petstore.service.FollowUpTaskService; import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -22,7 +21,6 @@ import java.util.Map; @RestController @RequestMapping("/api/admin/follow-up-tasks") @RequiredArgsConstructor -@CrossOrigin public class AdminFollowUpTaskController { private final FollowUpTaskService followUpTaskService; diff --git a/src/main/java/com/petstore/controller/AdminOnboardingController.java b/src/main/java/com/petstore/controller/AdminOnboardingController.java index ec7eca2..2801de9 100644 --- a/src/main/java/com/petstore/controller/AdminOnboardingController.java +++ b/src/main/java/com/petstore/controller/AdminOnboardingController.java @@ -5,7 +5,6 @@ import com.petstore.auth.CurrentUserContext; import com.petstore.service.FlowException; import com.petstore.service.StoreOnboardingService; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -17,7 +16,6 @@ import java.util.Map; @RestController @RequestMapping("/api/admin/onboarding") @RequiredArgsConstructor -@CrossOrigin public class AdminOnboardingController { private final StoreOnboardingService onboardingService; diff --git a/src/main/java/com/petstore/controller/AdminServiceCustomerController.java b/src/main/java/com/petstore/controller/AdminServiceCustomerController.java index 9e8beb1..50e2318 100644 --- a/src/main/java/com/petstore/controller/AdminServiceCustomerController.java +++ b/src/main/java/com/petstore/controller/AdminServiceCustomerController.java @@ -7,7 +7,6 @@ import com.petstore.service.FlowException; import com.petstore.service.StoreCustomerTimelineService; import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -23,7 +22,6 @@ import java.util.Map; @RestController @RequestMapping("/api/admin") @RequiredArgsConstructor -@CrossOrigin public class AdminServiceCustomerController { private final AdminServiceCustomerService adminServiceCustomerService; diff --git a/src/main/java/com/petstore/controller/AdminStoreController.java b/src/main/java/com/petstore/controller/AdminStoreController.java index c5f1d27..085a4a1 100644 --- a/src/main/java/com/petstore/controller/AdminStoreController.java +++ b/src/main/java/com/petstore/controller/AdminStoreController.java @@ -5,7 +5,6 @@ import com.petstore.auth.CurrentUserContext; import com.petstore.entity.Store; import com.petstore.service.StoreService; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -20,7 +19,6 @@ import java.util.Map; @RestController @RequestMapping("/api/admin") @RequiredArgsConstructor -@CrossOrigin public class AdminStoreController { private final StoreService storeService; diff --git a/src/main/java/com/petstore/controller/AppointmentController.java b/src/main/java/com/petstore/controller/AppointmentController.java index 80aea71..42d46f7 100644 --- a/src/main/java/com/petstore/controller/AppointmentController.java +++ b/src/main/java/com/petstore/controller/AppointmentController.java @@ -17,7 +17,6 @@ import java.util.Map; @RestController @RequestMapping("/api/appointment") @RequiredArgsConstructor -@CrossOrigin public class AppointmentController { private final AppointmentService appointmentService; private final UserService userService; diff --git a/src/main/java/com/petstore/controller/FileController.java b/src/main/java/com/petstore/controller/FileController.java index d7e4945..ef9fbeb 100644 --- a/src/main/java/com/petstore/controller/FileController.java +++ b/src/main/java/com/petstore/controller/FileController.java @@ -26,7 +26,6 @@ import java.util.UUID; @Slf4j @RestController @RequestMapping("/api/upload") -@CrossOrigin public class FileController { private static final Set IMAGE_EXT = Set.of(".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".heic", ".heif"); diff --git a/src/main/java/com/petstore/controller/MerchantOnboardingController.java b/src/main/java/com/petstore/controller/MerchantOnboardingController.java index 7cbf8eb..aa3685d 100644 --- a/src/main/java/com/petstore/controller/MerchantOnboardingController.java +++ b/src/main/java/com/petstore/controller/MerchantOnboardingController.java @@ -5,7 +5,6 @@ import com.petstore.service.MerchantOnboardingService; import com.petstore.service.WechatMiniProgramService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -19,7 +18,6 @@ import java.util.Map; @RestController @RequestMapping("/api/onboarding") @RequiredArgsConstructor -@CrossOrigin public class MerchantOnboardingController { private final MerchantOnboardingService onboardingService; diff --git a/src/main/java/com/petstore/controller/PetController.java b/src/main/java/com/petstore/controller/PetController.java index f0c6733..685b4a5 100644 --- a/src/main/java/com/petstore/controller/PetController.java +++ b/src/main/java/com/petstore/controller/PetController.java @@ -12,7 +12,6 @@ import java.util.Map; @RestController @RequestMapping("/api/pet") @RequiredArgsConstructor -@CrossOrigin public class PetController { private final PetService petService; diff --git a/src/main/java/com/petstore/controller/ReportController.java b/src/main/java/com/petstore/controller/ReportController.java index 48485a5..545a867 100644 --- a/src/main/java/com/petstore/controller/ReportController.java +++ b/src/main/java/com/petstore/controller/ReportController.java @@ -30,7 +30,6 @@ import java.util.stream.Collectors; @RestController @RequestMapping("/api/report") @RequiredArgsConstructor -@CrossOrigin public class ReportController { private static final Logger log = LoggerFactory.getLogger(ReportController.class); private final ReportService reportService; diff --git a/src/main/java/com/petstore/controller/ReportLeadController.java b/src/main/java/com/petstore/controller/ReportLeadController.java index 317de9b..d06bdd2 100644 --- a/src/main/java/com/petstore/controller/ReportLeadController.java +++ b/src/main/java/com/petstore/controller/ReportLeadController.java @@ -20,7 +20,6 @@ import java.util.Map; @RestController @RequestMapping("/api/report") @RequiredArgsConstructor -@CrossOrigin public class ReportLeadController { private final ReportLeadService reportLeadService; diff --git a/src/main/java/com/petstore/controller/ScheduleController.java b/src/main/java/com/petstore/controller/ScheduleController.java index f4a9047..47ec93e 100644 --- a/src/main/java/com/petstore/controller/ScheduleController.java +++ b/src/main/java/com/petstore/controller/ScheduleController.java @@ -16,7 +16,6 @@ import java.util.Map; @RestController @RequestMapping("/api/schedule") @RequiredArgsConstructor -@CrossOrigin public class ScheduleController { private final ScheduleService scheduleService; diff --git a/src/main/java/com/petstore/controller/ServiceTypeController.java b/src/main/java/com/petstore/controller/ServiceTypeController.java index 272f80e..0cbcc33 100644 --- a/src/main/java/com/petstore/controller/ServiceTypeController.java +++ b/src/main/java/com/petstore/controller/ServiceTypeController.java @@ -17,7 +17,6 @@ import java.util.Map; @RestController @RequestMapping("/api/service-type") @RequiredArgsConstructor -@CrossOrigin public class ServiceTypeController { private final ServiceTypeService serviceTypeService; diff --git a/src/main/java/com/petstore/controller/SmsController.java b/src/main/java/com/petstore/controller/SmsController.java index 69e7260..c19ec7b 100644 --- a/src/main/java/com/petstore/controller/SmsController.java +++ b/src/main/java/com/petstore/controller/SmsController.java @@ -17,7 +17,6 @@ import java.util.Random; @RestController @RequestMapping("/api/sms") @RequiredArgsConstructor -@CrossOrigin public class SmsController { /** diff --git a/src/main/java/com/petstore/controller/StaffInvitationController.java b/src/main/java/com/petstore/controller/StaffInvitationController.java index d051f92..8bc577d 100644 --- a/src/main/java/com/petstore/controller/StaffInvitationController.java +++ b/src/main/java/com/petstore/controller/StaffInvitationController.java @@ -7,7 +7,6 @@ import com.petstore.service.StaffInvitationService; import com.petstore.service.WechatMiniProgramService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -24,7 +23,6 @@ import java.util.Map; @RestController @RequestMapping("/api") @RequiredArgsConstructor -@CrossOrigin public class StaffInvitationController { private final StaffInvitationService invitationService; diff --git a/src/main/java/com/petstore/controller/StoreController.java b/src/main/java/com/petstore/controller/StoreController.java index 7cd8ebf..68fbfaa 100644 --- a/src/main/java/com/petstore/controller/StoreController.java +++ b/src/main/java/com/petstore/controller/StoreController.java @@ -16,7 +16,6 @@ import java.util.stream.Collectors; @RestController @RequestMapping("/api/store") @RequiredArgsConstructor -@CrossOrigin public class StoreController { private final StoreService storeService; private final AuditLogService auditLogService; diff --git a/src/main/java/com/petstore/controller/UserController.java b/src/main/java/com/petstore/controller/UserController.java index 74f9a64..76bc9be 100644 --- a/src/main/java/com/petstore/controller/UserController.java +++ b/src/main/java/com/petstore/controller/UserController.java @@ -18,7 +18,6 @@ import java.util.Map; @RestController @RequestMapping("/api/user") @RequiredArgsConstructor -@CrossOrigin public class UserController { private final UserService userService; private final WechatMiniProgramService wechatMiniProgramService; diff --git a/src/test/java/com/petstore/config/CorsBoundaryContractTest.java b/src/test/java/com/petstore/config/CorsBoundaryContractTest.java new file mode 100644 index 0000000..aa574c2 --- /dev/null +++ b/src/test/java/com/petstore/config/CorsBoundaryContractTest.java @@ -0,0 +1,85 @@ +package com.petstore.config; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.filter.CorsFilter; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CorsBoundaryContractTest { + + private static final String ADMIN_ORIGIN = "https://admin.petstore.local"; + private static final String REPORT_ORIGIN = "https://report.petstore.local"; + + @Test + void rejectsUnlistedOriginBeforeTheRequestReachesAController() throws Exception { + CorsFilter filter = filter(); + MockHttpServletRequest request = corsRequest("GET", "https://attacker.invalid"); + MockHttpServletResponse response = new MockHttpServletResponse(); + AtomicBoolean chainCalled = new AtomicBoolean(false); + + filter.doFilter(request, response, (req, res) -> chainCalled.set(true)); + + assertEquals(HttpStatus.FORBIDDEN.value(), response.getStatus()); + assertFalse(chainCalled.get()); + assertFalse(response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } + + @Test + void allowsOnlyTheExactConfiguredOrigin() throws Exception { + CorsFilter filter = filter(); + MockHttpServletRequest request = corsRequest("GET", ADMIN_ORIGIN); + MockHttpServletResponse response = new MockHttpServletResponse(); + AtomicBoolean chainCalled = new AtomicBoolean(false); + + filter.doFilter(request, response, (req, res) -> chainCalled.set(true)); + + assertTrue(chainCalled.get()); + assertEquals(ADMIN_ORIGIN, response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + assertEquals("true", response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + } + + @Test + void handlesAllowedPreflightWithoutInvokingBusinessCode() throws Exception { + CorsFilter filter = filter(); + MockHttpServletRequest request = corsRequest("OPTIONS", REPORT_ORIGIN); + request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); + MockHttpServletResponse response = new MockHttpServletResponse(); + AtomicBoolean chainCalled = new AtomicBoolean(false); + + filter.doFilter(request, response, (req, res) -> chainCalled.set(true)); + + assertEquals(HttpStatus.OK.value(), response.getStatus()); + assertFalse(chainCalled.get()); + assertEquals(REPORT_ORIGIN, response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } + + @Test + void controllersCannotDeclareLocalCorsPolicies() throws Exception { + Path controllerRoot = Path.of("src/main/java/com/petstore/controller"); + try (var files = Files.walk(controllerRoot)) { + for (Path path : files.filter(file -> file.toString().endsWith(".java")).toList()) { + assertFalse(Files.readString(path).contains("@CrossOrigin"), path.toString()); + } + } + } + + private static CorsFilter filter() { + return new CorsConfig(ADMIN_ORIGIN + "," + REPORT_ORIGIN).corsFilter(); + } + + private static MockHttpServletRequest corsRequest(String method, String origin) { + MockHttpServletRequest request = new MockHttpServletRequest(method, "/api/store/list"); + request.addHeader(HttpHeaders.ORIGIN, origin); + return request; + } +}