fix: enforce a single global cors policy
This commit is contained in:
parent
339c793855
commit
752fcaa38d
@ -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`。
|
||||||
|
|
||||||
## 本地开发
|
## 本地开发
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user