【问题标题】:spring mvc rest service redirect / forward / proxyspring mvc rest服务重定向/转发/代理
【发布时间】: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数据序列化/反序列化为字符串..

标签: java spring-mvc spring-boot resttemplate


【解决方案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】:

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

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

@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 年尝试这个,我能够稍微改进一下:

    1. 如果您的类是 @RestController,则不需要 @ResponseBody
    2. @RequestBody(required = false) 允许没有正文的请求(例如 GET)
    3. 那些 ssl 加密端点的 https 和端口 443(如果您的服务器在端口 443 上提供 https)
    4. 如果您返回整个 responseEntity 而不仅仅是正文,您还将获得标头和响应代码。
    5. 添加(可选)标题的示例,例如headers.put("Authorization", Arrays.asList(String[] { "Bearer 234asdf234"})
    6. 异常处理(捕获并转发 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/获得它的详细信息

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-08-04
          • 2011-07-15
          • 1970-01-01
          • 1970-01-01
          • 2017-09-03
          • 2023-02-20
          • 2012-04-26
          相关资源
          最近更新 更多