Spring Bootでリダイレクト先のURLを組み立てる

# 概要

下図のようにSpring Bootで作ったアプリケーションの前にnginx/apacheなどのhttpサーバーがあって、そこからプロキシされている場合、

```text
              nginx                        Spring boot
            +----------------------+      +------------------------+
            |                      |      |                        |
client ---> | https://example.com/ | ---> | http://localhost:9000/ |
            |                      |      |                        |
            +----------------------+      +------------------------+
```

Spring Boot側で、例えば下記のようにリダイレクト先を相対URLで指定すると`http://localhost:9000/fuga` のようになってしまいます。

```java
@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メソッド

```java
@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で確認します。

**ヘッダー無し**

```bat
> 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**

```bat
> 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**

```bat
> 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が得られます。

```bat
> 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ヘッダーも影響します。

```bat
> 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を使用する

```java
@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を使用する

```java
@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を使用する

```java
@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を使用する

```java
@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を使用する

```java
@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で指定できます。

```java
@RequestMapping(value = "/fuga/{id}", method = RequestMethod.GET, name = "indexFugo")
```

### relativeToを使用する

```java
@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を有効化します。

```java
@SpringBootApplication
public class App {

  ...省略...

  @Bean
  public ForwardedHeaderFilter forwardedHeaderFilter() {
    ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
    return filter;
  }

}
```

### 検証用のHandlerメソッド

```java
@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:" + "/";
}
```

### 検証結果

```bat
> 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**  |