package com.secure.demo.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; /** * Web 层配置。 * *

前后端分离后,Web 用户端(web-client)与后端跨域通信,需要放开 CORS。 * 但 不允许「任意来源 + 携带凭据」(那是严重风险:任何恶意站点都能借已登录 * 用户的凭据跨域调用受保护接口)。因此这里改为域名白名单

* * * 配置方式(二选一,环境变量优先): *
 *   # application.properties
 *   app.cors.allowed-origins=http://localhost:3000,https://app.example.com
 * 
*
 *   # 环境变量(可覆盖上面的配置)
 *   export APP_CORS_ALLOWED_ORIGINS="https://app.example.com,https://admin.example.com"
 * 
*/ @Configuration public class WebConfig implements WebMvcConfigurer { /** 本地开发默认白名单(web-client 常驻端口) */ private static final String DEFAULT_ALLOWED_ORIGINS = "http://localhost:3000,http://127.0.0.1:3000," + "http://localhost:8080,http://127.0.0.1:8080," + "http://localhost:5173,http://127.0.0.1:5173"; private final List allowedOrigins; private final DeviceAuthInterceptor deviceAuthInterceptor; public WebConfig( @Value("${app.cors.allowed-origins:}") String configuredOrigins, @Value("${APP_CORS_ALLOWED_ORIGINS:}") String envOrigins, DeviceAuthInterceptor deviceAuthInterceptor) { String raw = (envOrigins != null && !envOrigins.isBlank()) ? envOrigins : configuredOrigins; if (raw == null || raw.isBlank()) { raw = DEFAULT_ALLOWED_ORIGINS; } this.allowedOrigins = Arrays.stream(raw.split(",")) .map(String::trim) .filter(s -> !s.isEmpty()) .collect(Collectors.toList()); this.deviceAuthInterceptor = deviceAuthInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { // 设备签名认证拦截器:仅拦截需要设备认证的路径(/api/device/**、/api/photo/**), // 显式排除公开接口(注册、登录、健康检查、CORS 预检)。 registry.addInterceptor(deviceAuthInterceptor) .addPathPatterns("/api/device/**", "/api/photo/**") .excludePathPatterns( "/api/device/register", // 注册:设备未入库,用请求内公钥做 PoP 验签 "/api/photo/{photoId}/decrypt", // Web 用户端 Bearer 鉴权,非设备签名 "/api/photo/{photoId}/decrypt/stream" ); } @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") // 使用精确域名白名单(非 "*")。allowCredentials(true) 下 "*" 是非法且危险的。 .allowedOrigins(allowedOrigins.toArray(new String[0])) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }