SpringBoot拦截器实现跨域请求转发解决Safari Cookie限制问题
场景与问题
由于Safari浏览器的严格Cookie策略,example1.com域名页面无法直接调用example2.com的接口。计划通过后端转发实现:页面请求example1.com/example2-api/user/mine,后端转发至example2.com/user/mine,需原样传递URL参数、请求体、请求头。
此前使用RestTemplate构造转发请求时,出现响应乱码问题,原代码如下:
import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.stream.Collectors; @Component public class ForwardInterceptor implements HandlerInterceptor { private RestTemplate restTemplate; public ForwardInterceptor(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 获取原始请求的URL、请求方法、请求体和请求头 String originalUrl = request.getRequestURI(); String originalMethod = request.getMethod(); String originalBody = request.getReader().lines().collect(Collectors.joining(System.lineSeparator())); org.springframework.http.HttpHeaders originalHeaders = new org.springframework.http.HttpHeaders(); java.util.Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); String headerValue = request.getHeader(headerName); originalHeaders.add(headerName, headerValue); } // 构建转发请求的URL String forwardUrl = "http://example2.com" + originalUrl; // 创建转发请求 org.springframework.http.RequestEntity<String> requestEntity = new org.springframework.http.RequestEntity<>(originalBody, originalHeaders, HttpMethod.valueOf(originalMethod), java.net.URI.create(forwardUrl)); // 发送转发请求 ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class); // 将转发请求的响应设置到原始响应中 response.setStatus(responseEntity.getStatusCodeValue()); response.setContentType(responseEntity.getHeaders().getContentType().toString()); response.getWriter().write(responseEntity.getBody()); return false; // 停止后续处理 } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // 在请求处理完成后的操作 } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 在请求完成后的操作 } }
问题分析
原代码存在以下导致乱码和功能缺失的问题:
- 请求体使用
getReader()读取,只能读取一次,且未指定编码; - 未拼接URL查询参数,导致转发时参数丢失;
- RestTemplate接收String类型响应时,未正确处理目标接口的响应编码;
- 直接转发所有请求头(如
Host、Content-Length),可能导致目标服务器处理异常; - 响应未设置字符编码,默认编码不统一导致乱码。
修正方案
1. 配置RestTemplate(处理编码)
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.client.RestTemplate; import java.nio.charset.StandardCharsets; @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); // 配置消息转换器,强制UTF-8编码 MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setDefaultCharset(StandardCharsets.UTF_8); restTemplate.getMessageConverters().add(0, converter); return restTemplate; } }
2. 修正后的拦截器代码
import org.springframework.http.*; import org.springframework.stereotype.Component; import org.springframework.util.StreamUtils; import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.Enumeration; @Component public class ForwardInterceptor implements HandlerInterceptor { private final RestTemplate restTemplate; // 转发前缀标识 private static final String FORWARD_PREFIX = "/example2-api"; // 目标服务基础地址 private static final String TARGET_BASE_URL = "http://example2.com"; public ForwardInterceptor(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { // 仅处理带指定前缀的请求,其他请求正常放行 String requestUri = request.getRequestURI(); if (!requestUri.startsWith(FORWARD_PREFIX)) { return true; } // 1. 构建目标URL:去掉前缀 + 拼接查询参数 String targetPath = requestUri.substring(FORWARD_PREFIX.length()); StringBuilder targetUrlBuilder = new StringBuilder(TARGET_BASE_URL).append(targetPath); String queryString = request.getQueryString(); if (queryString != null && !queryString.isEmpty()) { targetUrlBuilder.append("?").append(queryString); } URI targetUri = URI.create(targetUrlBuilder.toString()); // 2. 读取请求体:用字节数组避免重复读取问题,指定UTF-8编码 byte[] requestBodyBytes = StreamUtils.copyToByteArray(request.getInputStream()); String requestBody = new String(requestBodyBytes, StandardCharsets.UTF_8); // 3. 处理请求头:过滤不需要转发的系统头 HttpHeaders headers = new HttpHeaders(); Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); // 过滤Host、Content-Length等不需要转发的头 if (!"Host".equalsIgnoreCase(headerName) && !"Content-Length".equalsIgnoreCase(headerName) && !"Connection".equalsIgnoreCase(headerName)) { headers.add(headerName, request.getHeader(headerName)); } } // 强制设置请求体编码为UTF-8 headers.setContentType(MediaType.parseMediaType(request.getContentType() + ";charset=UTF-8")); // 4. 构造转发请求 HttpMethod method = HttpMethod.valueOf(request.getMethod()); RequestEntity<String> requestEntity = new RequestEntity<>(requestBody, headers, method, targetUri); // 5. 发送请求:用byte[]接收响应,避免字符串转换编码问题 ResponseEntity<byte[]> responseEntity = restTemplate.exchange(requestEntity, byte[].class); // 6. 处理响应:原样返回状态码、头、响应体 response.setStatus(responseEntity.getStatusCodeValue()); // 复制响应头 responseEntity.getHeaders().forEach((name, values) -> values.forEach(value -> response.addHeader(name, value))); // 设置响应编码 response.setCharacterEncoding(StandardCharsets.UTF_8.name()); // 直接写入字节数组,避免编码转换乱码 response.getOutputStream().write(responseEntity.getBody()); response.getOutputStream().flush(); return false; // 终止后续Spring MVC处理流程 } }
关键优化点
- URL参数保留:通过
request.getQueryString()获取并拼接查询参数,确保转发时参数不丢失; - 请求体读取:使用
StreamUtils.copyToByteArray读取字节数组,避免getReader()只能读取一次的问题; - 乱码解决:RestTemplate接收
byte[]类型响应,直接写入输出流,同时强制设置请求/响应的UTF-8编码; - 请求头过滤:排除
Host等系统级请求头,避免目标服务器处理异常; - 前缀匹配:仅处理指定前缀的请求,不影响其他接口逻辑。
内容的提问来源于stack exchange,提问作者caovv
相关产品推荐
相关产品推荐

