fix: enforce a single global cors policy

This commit is contained in:
malei 2026-08-02 02:59:34 +08:00
parent 339c793855
commit 752fcaa38d
20 changed files with 133 additions and 29 deletions

View File

@ -34,6 +34,8 @@
| `SPRING_JPA_SHOW_SQL` | 否 | `false` | 生产 SQL 日志开关,必须为 `false` | | `SPRING_JPA_SHOW_SQL` | 否 | `false` | 生产 SQL 日志开关,必须为 `false` |
| `LOG_LEVEL_PETSTORE` | 否 | `info` | `com.petstore` 包日志级别 | | `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` > ⚠️ **密钥轮换**`application.yml` 历史版本曾提交过真实 DB 密码与微信 AppSecret。这些凭据已在仓库历史中暴露**必须按安全流程轮换**:改 DB 密码、重置微信 AppSecret、更换 `PETSTORE_SESSION_SECRET`
## 本地开发 ## 本地开发

View File

@ -2,6 +2,8 @@
set -euo pipefail set -euo pipefail
API_ORIGIN="${API_ORIGIN:-}" 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:-}" SMOKE_BEARER_TOKEN="${SMOKE_BEARER_TOKEN:-}"
[[ "$API_ORIGIN" == https://* ]] || { [[ "$API_ORIGIN" == https://* ]] || {
@ -18,23 +20,48 @@ SMOKE_BEARER_TOKEN="${SMOKE_BEARER_TOKEN:-}"
} }
API_ORIGIN="${API_ORIGIN%/}" 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() { json_assert_status_up() {
python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("status") == "UP", d; print("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 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 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")" 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"])')" 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")" 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"])')" 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 if date -v+1d +%F >/dev/null 2>&1; then
TOMORROW="$(date -v+1d +%F)" 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 "storeId=$STORE_ID" \
--data-urlencode "date=$TOMORROW" \ --data-urlencode "date=$TOMORROW" \
--data-urlencode "serviceType=$SERVICE_NAME" \ --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 if [[ -n "$SMOKE_BEARER_TOKEN" ]]; then
AUTH_HEADER="Authorization: Bearer $SMOKE_BEARER_TOKEN" AUTH_HEADER="Authorization: Bearer $SMOKE_BEARER_TOKEN"

View File

@ -6,7 +6,6 @@ import com.petstore.service.AdminBusinessEventService;
import com.petstore.service.AdminWorkbenchService; import com.petstore.service.AdminWorkbenchService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat; 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.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@ -19,7 +18,6 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/api/admin/workbench") @RequestMapping("/api/admin/workbench")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class AdminBusinessEventController { public class AdminBusinessEventController {
private final AdminBusinessEventService adminBusinessEventService; private final AdminBusinessEventService adminBusinessEventService;

View File

@ -6,7 +6,6 @@ import com.petstore.service.FlowException;
import com.petstore.service.FollowUpTaskService; import com.petstore.service.FollowUpTaskService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat; 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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -22,7 +21,6 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/api/admin/follow-up-tasks") @RequestMapping("/api/admin/follow-up-tasks")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class AdminFollowUpTaskController { public class AdminFollowUpTaskController {
private final FollowUpTaskService followUpTaskService; private final FollowUpTaskService followUpTaskService;

View File

@ -5,7 +5,6 @@ import com.petstore.auth.CurrentUserContext;
import com.petstore.service.FlowException; import com.petstore.service.FlowException;
import com.petstore.service.StoreOnboardingService; import com.petstore.service.StoreOnboardingService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -17,7 +16,6 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/api/admin/onboarding") @RequestMapping("/api/admin/onboarding")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class AdminOnboardingController { public class AdminOnboardingController {
private final StoreOnboardingService onboardingService; private final StoreOnboardingService onboardingService;

View File

@ -7,7 +7,6 @@ import com.petstore.service.FlowException;
import com.petstore.service.StoreCustomerTimelineService; import com.petstore.service.StoreCustomerTimelineService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat; 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.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@ -23,7 +22,6 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/api/admin") @RequestMapping("/api/admin")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class AdminServiceCustomerController { public class AdminServiceCustomerController {
private final AdminServiceCustomerService adminServiceCustomerService; private final AdminServiceCustomerService adminServiceCustomerService;

View File

@ -5,7 +5,6 @@ import com.petstore.auth.CurrentUserContext;
import com.petstore.entity.Store; import com.petstore.entity.Store;
import com.petstore.service.StoreService; import com.petstore.service.StoreService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -20,7 +19,6 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/api/admin") @RequestMapping("/api/admin")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class AdminStoreController { public class AdminStoreController {
private final StoreService storeService; private final StoreService storeService;

View File

@ -17,7 +17,6 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/api/appointment") @RequestMapping("/api/appointment")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class AppointmentController { public class AppointmentController {
private final AppointmentService appointmentService; private final AppointmentService appointmentService;
private final UserService userService; private final UserService userService;

View File

@ -26,7 +26,6 @@ import java.util.UUID;
@Slf4j @Slf4j
@RestController @RestController
@RequestMapping("/api/upload") @RequestMapping("/api/upload")
@CrossOrigin
public class FileController { public class FileController {
private static final Set<String> IMAGE_EXT = Set.of(".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".heic", ".heif"); private static final Set<String> IMAGE_EXT = Set.of(".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".heic", ".heif");

View File

@ -5,7 +5,6 @@ import com.petstore.service.MerchantOnboardingService;
import com.petstore.service.WechatMiniProgramService; import com.petstore.service.WechatMiniProgramService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -19,7 +18,6 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/api/onboarding") @RequestMapping("/api/onboarding")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class MerchantOnboardingController { public class MerchantOnboardingController {
private final MerchantOnboardingService onboardingService; private final MerchantOnboardingService onboardingService;

View File

@ -12,7 +12,6 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/api/pet") @RequestMapping("/api/pet")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class PetController { public class PetController {
private final PetService petService; private final PetService petService;

View File

@ -30,7 +30,6 @@ import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/api/report") @RequestMapping("/api/report")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class ReportController { public class ReportController {
private static final Logger log = LoggerFactory.getLogger(ReportController.class); private static final Logger log = LoggerFactory.getLogger(ReportController.class);
private final ReportService reportService; private final ReportService reportService;

View File

@ -20,7 +20,6 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/api/report") @RequestMapping("/api/report")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class ReportLeadController { public class ReportLeadController {
private final ReportLeadService reportLeadService; private final ReportLeadService reportLeadService;

View File

@ -16,7 +16,6 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/api/schedule") @RequestMapping("/api/schedule")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class ScheduleController { public class ScheduleController {
private final ScheduleService scheduleService; private final ScheduleService scheduleService;

View File

@ -17,7 +17,6 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/api/service-type") @RequestMapping("/api/service-type")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class ServiceTypeController { public class ServiceTypeController {
private final ServiceTypeService serviceTypeService; private final ServiceTypeService serviceTypeService;

View File

@ -17,7 +17,6 @@ import java.util.Random;
@RestController @RestController
@RequestMapping("/api/sms") @RequestMapping("/api/sms")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class SmsController { public class SmsController {
/** /**

View File

@ -7,7 +7,6 @@ import com.petstore.service.StaffInvitationService;
import com.petstore.service.WechatMiniProgramService; import com.petstore.service.WechatMiniProgramService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -24,7 +23,6 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/api") @RequestMapping("/api")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class StaffInvitationController { public class StaffInvitationController {
private final StaffInvitationService invitationService; private final StaffInvitationService invitationService;

View File

@ -16,7 +16,6 @@ import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/api/store") @RequestMapping("/api/store")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class StoreController { public class StoreController {
private final StoreService storeService; private final StoreService storeService;
private final AuditLogService auditLogService; private final AuditLogService auditLogService;

View File

@ -18,7 +18,6 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/api/user") @RequestMapping("/api/user")
@RequiredArgsConstructor @RequiredArgsConstructor
@CrossOrigin
public class UserController { public class UserController {
private final UserService userService; private final UserService userService;
private final WechatMiniProgramService wechatMiniProgramService; private final WechatMiniProgramService wechatMiniProgramService;

View File

@ -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;
}
}