spring mvc rest服务重定向/转发/代理

Posted

技术标签:

【中文标题】spring mvc rest服务重定向/转发/代理【英文标题】:spring mvc rest service redirect / forward / proxy 【发布时间】:2013-01-21 11:23:46 【问题描述】:

我已经使用 spring mvc 框架构建了一个 Web 应用程序来发布 REST 服务。 例如:

@Controller
@RequestMapping("/movie")
public class MovieController 

@RequestMapping(value = "/id", method = RequestMethod.GET)
public @ResponseBody Movie getMovie(@PathVariable String id, @RequestBody user) 

    return dataProvider.getMovieById(user,id);


现在我需要部署我的应用程序,但我遇到了以下问题: 客户端无法直接访问应用程序所在的计算机(有防火墙)。因此,我需要在调用实际休息服务的代理机器(可由客户端访问)上的重定向层。

我尝试使用 RestTemplate 拨打新电话: 例如:

@Controller
@RequestMapping("/movieProxy")
public class MovieProxyController 

    private String address= "http://xxx.xxx.xxx.xxx:xx/MyApp";

    @RequestMapping(value = "/id", method = RequestMethod.GET)
    public @ResponseBody Movie getMovie(@PathVariable String id,@RequestBody user,final HttpServletResponse response,final HttpServletRequest request) 

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate.exchange( address+ request.getPathInfo(), request.getMethod(), new HttpEntity<T>(user, headers), Movie.class);


这没关系,但我需要重写控制器中的每个方法以使用 resttemplate。此外,这会导致代理机器上的冗余序列化/反序列化。

我尝试使用 restemplate 编写一个泛型函数,但没有成功:

@Controller
@RequestMapping("/movieProxy")
public class MovieProxyController 

    private String address= "http://xxx.xxx.xxx.xxx:xx/MyApp";

    @RequestMapping(value = "/**")
    public ? redirect(final HttpServletResponse response,final HttpServletRequest request)         
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate.exchange( address+ request.getPathInfo(), request.getMethod(), ? , ?);


我找不到适用于请求和响应对象的 resttemplate 方法。

我还尝试了弹簧重定向和转发。但是重定向不会更改请求的客户端 IP 地址,所以我认为在这种情况下它是无用的。我也无法转发到另一个 URL。

有没有更合适的方法来实现这一点? 提前致谢。

【问题讨论】:

为什么不能使用 Apache w/ mod_rewrite 或 mod_proxy 之类的东西来执行此操作?您基本上可以在防火墙外放置一个网络服务器(通常我们称之为 DMZ),并在 FW 中设置允许该服务器与防火墙后面的服务器通信的规则。这是大多数公司解决此问题的方法。 谢谢,如果您的解决方案适用于我们的案例,我会尝试与系统管理员交谈。同时我将使用resttemplate并将json数据序列化/反序列化为字符串.. 【参考方案1】:

您可以使用此镜像/代理所有请求:

private String server = "localhost";
private int port = 8080;

@RequestMapping("/**")
@ResponseBody
public String mirrorRest(@RequestBody String body, HttpMethod method, HttpServletRequest request) throws URISyntaxException

    URI uri = new URI("http", null, server, port, request.getRequestURI(), request.getQueryString(), null);

    ResponseEntity<String> responseEntity =
        restTemplate.exchange(uri, method, new HttpEntity<String>(body), String.class);

    return responseEntity.getBody();

这不会反映任何标题。

【讨论】:

我期待得到像你这样的答案,但已经很久了,我不再从事那个项目了。不幸的是,我现在无法进行全面测试并确认您的代码。 我猜这不适用于多部分请求!如果您还想处理多部分请求,则必须实现完整的 HTTP 代理。 您可以传递标题。只是返回不是String,而是ResponseEntity。您可以使用 responseEntity 标头的某些子集或一些自定义标头创建自己的 ResponseEntity 实例。希望这会对某人有所帮助。 @k-den 此实现不适用于多部分,因为文件不在正文中。您必须为此实施特殊处理。 如果 REST API 返回 404 响应或类似的响应,是否也会通过?还是代码只会抛出异常?【参考方案2】:

这是我对原始答案的修改版本,有四点不同:

    它不会强制请求正文,因此不会让 GET 请求失败。 它复制原始请求中存在的所有标头。如果您使用另一个代理/Web 服务器,这可能会由于内容长度/gzip 压缩而导致问题。将标题限制为您真正需要的标题。 它确实重新编码查询参数或路径。我们希望它们无论如何都会被编码。请注意,您的 URL 的其他部分也可能被编码。如果您是这种情况,请充分利用UriComponentsBuilder 的潜力。 它确实从服务器正确返回错误代码。

@RequestMapping("/**")
public ResponseEntity mirrorRest(@RequestBody(required = false) String body, 
    HttpMethod method, HttpServletRequest request, HttpServletResponse response) 
    throws URISyntaxException 
    String requestUrl = request.getRequestURI();

    URI uri = new URI("http", null, server, port, null, null, null);
    uri = UriComponentsBuilder.fromUri(uri)
                              .path(requestUrl)
                              .query(request.getQueryString())
                              .build(true).toUri();

    HttpHeaders headers = new HttpHeaders();
    Enumeration<String> headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) 
        String headerName = headerNames.nextElement();
        headers.set(headerName, request.getHeader(headerName));
    

    HttpEntity<String> httpEntity = new HttpEntity<>(body, headers);
    RestTemplate restTemplate = new RestTemplate();
    try 
        return restTemplate.exchange(uri, method, httpEntity, String.class);
     catch(HttpStatusCodeException e) 
        return ResponseEntity.status(e.getRawStatusCode())
                             .headers(e.getResponseHeaders())
                             .body(e.getResponseBodyAsString());
    

【讨论】:

我试过这段代码,它几乎完美。除了如何从代理服务器返回真正的错误代码?您的代码总是返回 500 而不是真实的状态错误代码。 我还修改了我的答案以包含这种行为,因为我实际上也错过了它。 @Jeppz 当您不复制标题时也会发生这种情况吗?我在我的帖子中特别提到了这一点,因为我遇到了类似的问题,所以在我的例子中,我只是将内容类型设置为 application/json ,仅此而已。 @Veluria 抱歉,那天我显然无法正确阅读。然而,我确实遇到了 haproxy 的问题,这些问题通过我们旧的代理 servlet 实现 headerName != null &amp;&amp; !"Transfer-Encoding".equals(headerName); 中的一些代码解决了 @Veluria 这不是我写过的最漂亮的代码,但它在我们的案例中可以工作pastebin.com/LApUmpxx【参考方案3】:

您可以使用 Netflix Zuul 将来自 Spring 应用程序的请求路由到另一个 Spring 应用程序。

假设你有两个应用程序:1.songs-app,2.api-gateway

在api-gateway应用中,先添加zuul依赖,然后在application.yml中简单定义你的路由规则如下:

pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    <version>LATEST</version>
</dependency>

application.yml

server:
  port: 8080
zuul:
  routes:
    foos:
      path: /api/songs/**
      url: http://localhost:8081/songs/

最后运行 api-gateway 应用程序,如:

@EnableZuulProxy
@SpringBootApplication
public class Application 
    public static void main(String[] args) 
        SpringApplication.run(Application.class, args);
    

现在,网关会将所有/api/songs/ 请求路由到http://localhost:8081/songs/

这里有一个工作示例:https://github.com/muatik/spring-playground/tree/master/spring-api-gateway

另一个资源:http://www.baeldung.com/spring-rest-with-zuul-proxy

【讨论】:

这不会给应用程序带来所有云依赖项的负担吗?尝试单独使用 Zuul 没有任何效果。有在没有 Cloud 的情况下使用 Zuul 的示例吗? Zuul 似乎已被弃用。 Spring Cloud Gateway 是现在推荐的方式。【参考方案4】:

带有 oauth2 的代理控制器

@RequestMapping("v9")
@RestController
@EnableConfigurationProperties
public class ProxyRestController 
    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails;

    @Autowired
    private ClientCredentialsResourceDetails clientCredentialsResourceDetails;

    @Autowired
    OAuth2RestTemplate oAuth2RestTemplate;


    @Value("$gateway.url:http://gateway/")
    String gatewayUrl;

    @RequestMapping(value = "/proxy/**")
    public String proxy(@RequestBody(required = false) String body, HttpMethod method, HttpServletRequest request, HttpServletResponse response,
                        @RequestHeader HttpHeaders headers) throws ServletException, IOException, URISyntaxException 

        body = body == null ? "" : body;
        String path = request.getRequestURI();
        String query = request.getQueryString();
        path = path.replaceAll(".*/v9/proxy", "");
        StringBuffer urlBuilder = new StringBuffer(gatewayUrl);
        if (path != null) 
            urlBuilder.append(path);
        
        if (query != null) 
            urlBuilder.append('?');
            urlBuilder.append(query);
        
        URI url = new URI(urlBuilder.toString());
        if (logger.isInfoEnabled()) 
            logger.info("url:  ", url);
            logger.info("method:  ", method);
            logger.info("body:  ", body);
            logger.info("headers:  ", headers);
        
        ResponseEntity<String> responseEntity
                = oAuth2RestTemplate.exchange(url, method, new HttpEntity<String>(body, headers), String.class);
        return responseEntity.getBody();
    


    @Bean
    @ConfigurationProperties("security.oauth2.client")
    @ConditionalOnMissingBean(ClientCredentialsResourceDetails.class)
    public ClientCredentialsResourceDetails clientCredentialsResourceDetails() 
        return new ClientCredentialsResourceDetails();
    

    @Bean
    @ConditionalOnMissingBean
    public OAuth2RestTemplate oAuth2RestTemplate() 
        return new OAuth2RestTemplate(clientCredentialsResourceDetails);
    


【讨论】:

【参考方案5】:

@derkoe 发布了一个很好的答案,对我有很大帮助!

在 2021 年尝试这个,我能够稍微改进一下:

    如果您的类是 @RestController,则不需要 @ResponseBody @RequestBody(required = false) 允许没有正文的请求(例如 GET) 那些 ssl 加密端点的 https 和端口 443(如果您的服务器在端口 443 上提供 https) 如果您返回整个 responseEntity 而不仅仅是正文,您还将获得标头和响应代码。 添加(可选)标题的示例,例如headers.put("Authorization", Arrays.asList(String[] "Bearer 234asdf234") 异常处理(捕获并转发 404 之类的 HttpStatuse,而不是抛出 500 服务器错误)

private String server = "localhost";
private int port = 443;

@Autowired
MultiValueMap<String, String> headers;

@Autowired
RestTemplate restTemplate;

@RequestMapping("/**")
public ResponseEntity<String> mirrorRest(@RequestBody(required = false) String body, HttpMethod method, HttpServletRequest request) throws URISyntaxException

    URI uri = new URI("https", null, server, port, request.getRequestURI(), request.getQueryString(), null);

    HttpEntity<String> entity = new HttpEntity<>(body, headers);    
    
    try 
        ResponseEntity<String> responseEntity =
            restTemplate.exchange(uri, method, entity, String.class);
            return responseEntity;
     catch (HttpClientErrorException ex) 
        return ResponseEntity
            .status(ex.getStatusCode())
            .headers(ex.getResponseHeaders())
            .body(ex.getResponseBodyAsString());
    

    return responseEntity;

【讨论】:

回顾这一点让我意识到这在 node+express 中是多么容易。嵌入式服务器(tomcat/undertow)的陷阱也更少,它们试图通过添加/更改标题来提供帮助。 这里如何定义restTemplateentity HttpEntity&lt;String&gt; entity = new HttpEntity&lt;&gt;(body, headers); restTemplate你可以让Spring Boot通过@Autowired(推荐)或者手动new RestTemplate()注入它 感谢您的澄清。【参考方案6】:

如果您可以摆脱使用像 mod_proxy 这样的较低级别的解决方案,那将是更简单的方法,但如果您需要更多控制(例如安全性、翻译、业务逻辑),您可能需要看看 Apache骆驼:http://camel.apache.org/how-to-use-camel-as-a-http-proxy-between-a-client-and-server.html

【讨论】:

【参考方案7】:

我受到 Veluria 解决方案的启发,但我遇到了从目标资源发送的 gzip 压缩问题。

目标是省略 Accept-Encoding 标头:

@RequestMapping("/**")
public ResponseEntity mirrorRest(@RequestBody(required = false) String body, 
    HttpMethod method, HttpServletRequest request, HttpServletResponse response) 
    throws URISyntaxException 
    String requestUrl = request.getRequestURI();

    URI uri = new URI("http", null, server, port, null, null, null);
    uri = UriComponentsBuilder.fromUri(uri)
                              .path(requestUrl)
                              .query(request.getQueryString())
                              .build(true).toUri();

    HttpHeaders headers = new HttpHeaders();
    Enumeration<String> headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) 
        String headerName = headerNames.nextElement();
        if (!headerName.equals("Accept-Encoding")) 
            headers.set(headerName, request.getHeader(headerName));
        
    

    HttpEntity<String> httpEntity = new HttpEntity<>(body, headers);
    RestTemplate restTemplate = new RestTemplate();
    try 
        return restTemplate.exchange(uri, method, httpEntity, String.class);
     catch(HttpStatusCodeException e) 
        return ResponseEntity.status(e.getRawStatusCode())
                             .headers(e.getResponseHeaders())
                             .body(e.getResponseBodyAsString());
    

【讨论】:

【参考方案8】:

你需要像jetty transparent proxy 这样的东西,它实际上会重定向你的电话,如果你需要,你有机会覆盖请求。你可以在http://reanimatter.com/2016/01/25/embedded-jetty-as-http-proxy/获得它的详细信息

【讨论】:

以上是关于spring mvc rest服务重定向/转发/代理的主要内容,如果未能解决你的问题,请参考以下文章

spring mvc怎么重定向

使用 @EnableWebMvc 和 WebMvcConfigurerAdapter 进行静态资源定位的 Spring MVC 配置会导致“转发:”和“重定向:”出现问题

Spring MVC @Controller中转发或者重定向到其他页面的信息怎么携带和传递(Servlet API对象)HttpServletRequestHttpServletRespose

spring mvc 重定向,地址栏带了参数,不想带怎么办

Spring MVC @Controller中转发或者重定向到其他页面的信息怎么携带和传递(Servlet API对象)HttpServletRequestHttpServletRespose(代码片

Spring MVC详解(学习总结)