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

问题描述 投票:0回答:9

我已经使用 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方法。

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

有没有更合适的方法来实现这个目标?

java spring-mvc spring-boot resttemplate
9个回答
86
投票

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

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();
}

这不会镜像任何标题。


52
投票

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

  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());
    }
}

31
投票

您可以使用 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>

应用程序.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


7
投票

@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;
}

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);
    }



3
投票

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


2
投票

我受到 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());
    }
}

0
投票

您需要像

jetty transparent proxy
这样的东西,它实际上会重定向您的呼叫,并且您有机会在需要时覆盖该请求。您可以在 http://reanimatter.com/2016/01/25/embedded-jetty-as-http-proxy/

获取其详细信息

0
投票

RequestEntity
,在 Spring Web 4.1 中添加,捕获进行 REST 调用所需的所有相关 HTTP 状态。可以使用带有更新 URL 的
RequestEntity
副本进行 REST 调用。数据可以作为
byte[]
检索,并作为
ResponseEntity<byte[]>
从控制器返回。这实际上是一个直接代理,无需任何应用内数据转换。

@Controller
@RequestMapping("/movieProxy")
public class MovieProxyController {
    private String address= "http://xxx.xxx.xxx.xxx:xx/MyApp";

    @RequestMapping(value = "/**")
    public ResponseEntity<byte[]> redirect(RequestEntity<byte[]> request) {
        RequestEntity<byte[]> requestCopy = new RequestEntity<>(
                request.getBody(), request.getHeaders(), request.getMethod(),
                uri, request.getType());
        try {
            return restTemplate.exchange(requestCopy, byte[].class);
        } catch (RestClientResponseException e) {
            return new ResponseEntity<>(e.getResponseBodyAsByteArray(),
                    e.getResponseHeaders(), e.getStatusCode());
        }
}
© www.soinside.com 2019 - 2024. All rights reserved.