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 && !"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)的陷阱也更少,它们试图通过添加/更改标题来提供帮助。 这里如何定义restTemplate
和entity
?
HttpEntity<String> entity = new HttpEntity<>(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服务重定向/转发/代理的主要内容,如果未能解决你的问题,请参考以下文章
使用 @EnableWebMvc 和 WebMvcConfigurerAdapter 进行静态资源定位的 Spring MVC 配置会导致“转发:”和“重定向:”出现问题
Spring MVC @Controller中转发或者重定向到其他页面的信息怎么携带和传递(Servlet API对象)HttpServletRequestHttpServletRespose
Spring MVC @Controller中转发或者重定向到其他页面的信息怎么携带和传递(Servlet API对象)HttpServletRequestHttpServletRespose(代码片