You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SpringBoot拦截器实现跨域请求转发解决Safari Cookie限制问题

后端转发解决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 {
        // 在请求完成后的操作
    }
}

问题分析

原代码存在以下导致乱码和功能缺失的问题:

  1. 请求体使用getReader()读取,只能读取一次,且未指定编码;
  2. 未拼接URL查询参数,导致转发时参数丢失;
  3. RestTemplate接收String类型响应时,未正确处理目标接口的响应编码;
  4. 直接转发所有请求头(如Host、Content-Length),可能导致目标服务器处理异常;
  5. 响应未设置字符编码,默认编码不统一导致乱码。

修正方案

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 20:34:55