概要
下図のようにSpring Bootで作ったアプリケーションの前にnginx/apacheなどのhttpサーバーがあって、そこからプロキシされている場合、
nginx Spring boot
+----------------------+ +------------------------+
| | | |
client ---> | https://example.com/ | ---> | http://localhost:9000/ |
| | | |
+----------------------+ +------------------------+
Spring Boot側で、例えば下記のようにリダイレクト先を相対URLで指定するとhttp://localhost:9000/fuga
のようになってしまいます。
@Controller
public class IndexController {
@RequestMapping(value = "/hoge", method = RequestMethod.GET)
public String hoge() {
return "redirect:/fuga";
}
}
リダイレクト先をhttps://example.com/fuga
にしたい場合、nginx/apache側でいくつかのhttpヘッダーをセットし、且つUriComponentsBuilderを使うことで期待するURLを生成できます。
環境
- Windows10 Professional
- Java 1.8.0_101
- Spring Boot 1.4.1
参考
- [22. Web MVC framework / 22.7 Building URIs] (http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-uri-building)
- [UriComponentsBuilder] (http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/util/UriComponentsBuilder.html)
- [MvcUriComponentsBuilder] (http://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.html)
- [ForwardedHeaderFilter] (http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/filter/ForwardedHeaderFilter.html)
- [RFC 7239 - Forwarded HTTP Extension] (https://tools.ietf.org/html/rfc7239)
サンプルコードの説明
https://example.com/hoge
にアクセスした場合、https://example.com/fuga
へリダイレクトする場合の例です。
UriComponentsBuilderを使ってリダイレクト先のURLを組み立てることで、環境依存(開発環境や本番環境)を排除することができます。
Handlerメソッド
@RequestMapping(value = "/hoge", method = RequestMethod.GET)
public String hoge(UriComponentsBuilder builder) {
URI location = builder.path("/fuga").build().toUri();
return "redirect:" + location.toString();
}
Httpヘッダー
UriComponentsBuilderは、下記に引用するhttpヘッダーの値を参照します。
[Class UriComponentsBuilder] (http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/util/UriComponentsBuilder.html)
Create a new UriComponents object from the URI associated with the given HttpRequest while also overlaying with values from the headers "Forwarded" (RFC 7239, or "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" if "Forwarded" is not found.
curlでテスト
指定するhttpヘッダーの値でリダイレクト先のURLがどのように変わるかcurlで確認します。
ヘッダー無し
> curl --head http://localhost:9000/hoge
HTTP/1.1 302
Location: http://localhost:9000/fuga
Content-Language: ja-JP
Transfer-Encoding: chunked
Date: Sun, 09 Oct 2016 02:28:11 GMT
X-Forwarded-Proto
> curl --header "X-Forwarded-Proto: https" --head http://localhost:9000/hoge
HTTP/1.1 302
Location: https://localhost:9000/fuga
Content-Language: ja-JP
Transfer-Encoding: chunked
Date: Sat, 08 Oct 2016 23:27:44 GMT
X-Forwarded-Host
> curl --header "X-Forwarded-Host: example.com" --head http://localhost:9000/hoge
HTTP/1.1 302
Location: http://example.com/fuga
Content-Language: ja-JP
Transfer-Encoding: chunked
Date: Sat, 08 Oct 2016 23:28:18 GMT
X-Forwarded-ProtoとX-Forwarded-Host
この2つのhttpヘッダーをセットすることで、期待するリダイレクト先URLが得られます。
> curl --header "X-Forwarded-Proto: https" --header "X-Forwarded-Host: example.com" --head http://localhost:9000/hoge
HTTP/1.1 302
Location: https://example.com/fuga
Content-Language: ja-JP
Transfer-Encoding: chunked
Date: Sat, 08 Oct 2016 23:30:24 GMT
X-Forwarded-Port
今回は使用しませんでしたが、このhttpヘッダーも影響します。
> curl --header "X-Forwarded-Port: 9001" --head http://localhost:9000/hoge
HTTP/1.1 302
Location: http://localhost:9001/fuga
Content-Language: ja-JP
Transfer-Encoding: chunked
Date: Sat, 08 Oct 2016 23:28:25 GMT
UriComponentsBuilderの使い方
上記の例では、HandlerメソッドのパラメータでUriComponentsBuilderのインスタンスを受け取っていますが、他の方法でもインスタンスを生成することができます。
fromHttpRequestを使用する
@Controller
public class IndexController {
@RequestMapping(value = "/poge", method = RequestMethod.GET)
public String poge(HttpServletRequest request) {
HttpRequest req = new ServletServerHttpRequest(request);
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpRequest(req);
URI location = builder.path("/fuga").build().toUri();
return "redirect:" + location.toString();
}
@RequestMapping(value = "/fuga", method = RequestMethod.GET)
public String fuga() {
return "fuga";
}
}
newInstanceを使用する
@Controller
public class IndexController {
@RequestMapping(value = "/poge", method = RequestMethod.GET)
public String poge() {
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
URI location = builder.scheme("https").host("example.com").path("/fuga").build().toUri();
return "redirect:" + location.toString();
}
@RequestMapping(value = "/fuga", method = RequestMethod.GET)
public String fuga() {
return "fuga";
}
}
fromUriStringを使用する
@Controller
public class IndexController {
@RequestMapping(value = "/poge", method = RequestMethod.GET)
public String poge() {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("https://example.com");
URI location = builder.path("/fuga").build().toUri();
return "redirect:" + location.toString();
}
@RequestMapping(value = "/fuga", method = RequestMethod.GET)
public String fuga() {
return "fuga";
}
}
MvcUriComponentsBuilderの使い方
コメントにて教えて頂きました。
MvcUriComponentsBuilderでも同様のことが行えますが、同じアプリケーション内のURLへリダイレクトする場合は、このクラスを使用することでより安全なコードが書けます。
fromMethodNameを使用する
@Controller
public class IndexController {
@RequestMapping(value = "/puge", method = RequestMethod.GET)
public String puge() {
UriComponents uriComponents = MvcUriComponentsBuilder
.fromMethodName(IndexController.class, "fuga", 12L).build();
URI location = uriComponents.toUri();
return "redirect:" + location.toString();
}
@RequestMapping(value = "/fugo/{id}", method = RequestMethod.GET)
public String fugo(@PathVariable("id") Long id){
return "fugo";
}
}
fromMappingNameを使用する
@Controller
public class IndexController {
@RequestMapping(value = "/puge", method = RequestMethod.GET)
public String puge(UriComponentsBuilder baseUrl) {
String location = MvcUriComponentsBuilder
.fromMappingName(baseUrl, "IC#fuga").arg(0, 12L).build();
return "redirect:" + location;
}
@RequestMapping(value = "/fuga/{id}", method = RequestMethod.GET)
public String fuga(@PathVariable("id") Long id){
return "fuga";
}
}
デフォルトのMappingName
デフォルトのマッピング名は、コントローラーのクラス名の大文字とメソッド名を"#"で区切った文字列になります。
この例の場合、デフォルトのマッピング名は"IC#fuga"です。
The configured HandlerMethodMappingNamingStrategy determines the names of controller method request mappings at startup. By default all mappings are assigned a name based on the capital letters of the class name, followed by "#" as separator, and then the method name. For example "PC#getPerson" for a class named PersonController with method getPerson. In case the naming convention does not produce unique results, an explicit name may be assigned through the name attribute of the @RequestMapping annotation.
MappingName
デフォルト名とは違うマッピング名を指定したい場合は、RequestMappingアノテーションのnameで指定できます。
@RequestMapping(value = "/fuga/{id}", method = RequestMethod.GET, name = "indexFugo")
relativeToを使用する
@Controller
public class IndexController {
@RequestMapping(value = "/puge", method = RequestMethod.GET)
public String puge(UriComponentsBuilder baseUrl) {
MvcUriComponentsBuilder builder = MvcUriComponentsBuilder.relativeTo(baseUrl);
UriComponents uriComponents = builder.withMethodName(IndexController.class, "fugo", 12L).build();
URI location = uriComponents.toUri();
return "redirect:" + location.toString();
}
@RequestMapping(value = "/fuga/{id}", method = RequestMethod.GET)
public String fuga(@PathVariable("id") Long id){
return "fuga";
}
}
ForwardedHeaderFilterを試用
コメントにて教えて頂きました。
このFilterを有効にすると、いくつかのhttpヘッダーの値をもとにHttpServletRequestのインスタンスのフィールドを上書きします。
Filter that wraps the request in order to override its getServerName(), getServerPort(), getScheme(), and isSecure() methods with values derived from "Forwarded" or "X-Forwarded-*" headers. In effect the wrapped request reflects the client-originated protocol and address.
App
デフォルトでは有効になっていないのでfilterを有効化します。
@SpringBootApplication
public class App {
...省略...
@Bean
public ForwardedHeaderFilter forwardedHeaderFilter() {
ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
return filter;
}
}
検証用のHandlerメソッド
@RequestMapping(value = "/fugu", method = RequestMethod.GET)
public String fugu(HttpServletRequest request) {
logger.info("getScheme:{}", request.getScheme());
logger.info("getServerName:{}", request.getServerName());
logger.info("getServerPort:{}", request.getServerPort());
logger.info("isSecure:{}", request.isSecure());
logger.info("getProtocol:{}", request.getProtocol());
logger.info("getMethod:{}", request.getMethod());
logger.info("getServletPath:{}", request.getServletPath());
logger.info("getLocalAddr:{}", request.getLocalAddr());
logger.info("getLocalName:{}", request.getLocalName());
logger.info("getLocalPort:{}", request.getLocalPort());
logger.info("getRemoteAddr:{}", request.getRemoteAddr());
logger.info("getRemoteHost:{}", request.getRemoteHost());
logger.info("getRemotePort:{}", request.getRemotePort());
logger.info("getRequestURI:{}", request.getRequestURI());
logger.info("getRequestURL:{}", request.getRequestURL().toString());
return "redirect:" + "/";
}
検証結果
> curl --header "X-Forwarded-Proto: https" --header "X-Forwarded-Host: example.com" --head http://localhost:9000/fugu
HTTP/1.1 302
Location: http://localhost:9000/
Content-Language: ja-JP
Transfer-Encoding: chunked
Date: Wed, 12 Oct 2016 14:30:32 GMT
Filterの有無による違い
Filter Off | Filter On | |
---|---|---|
getScheme | http | https |
getServerName | localhost | example.com |
getServerPort | 9000 | 443 |
isSecure | false | true |
getProtocol | HTTP/1.1 | HTTP/1.1 |
getMethod | HEAD | HEAD |
getServletPath | /fugu | /fugu |
getLocalAddr | 127.0.0.1 | 127.0.0.1 |
getLocalName | 127.0.0.1 | 127.0.0.1 |
getLocalPort | 9000 | 9000 |
getRemoteAddr | 127.0.0.1 | 127.0.0.1 |
getRemoteHost | 127.0.0.1 | 127.0.0.1 |
getRemotePort | 56730 | 56777 |
getRequestURI | /fugu | /fugu |
getRequestURL | http://localhost:9000/fugu | https://example.com/fugu |