Spring Boot で server.tomcat.use-relative-redirects 設定による相対リダイレクトの挙動を見る

## 概要

- Spring Boot の server.tomcat.use-relative-redirects の設定によって、指定したリダイレクトが Location ヘッダにてどのような値でセットされるか検証する

## 検証環境

- Spring Boot 2.3.3
- Spring WebMVC 5.2.8
- Apache Tomcat Embed 9.0.37
- Java 14 (AdoptOpenJDK 14.0.2)
- macOS Catalina

## server.tomcat.use-relative-redirects の設定

HttpServletResponse#sendRedirect の呼び出しによって生成された HTTP 1.1 以降の Location ヘッダーが相対リダイレクトを使用するか絶対リダイレクトを使用するかを制御する設定。application.properties などで設定できる。

```properties
server.tomcat.use-relative-redirects=false
```

true を指定すると、HttpServletResponse#sendRedirect に指定された相対リダイレクトはそのまま Location ヘッダに指定される。

false を指定すると、HttpServletResponse#sendRedirect に指定された相対リダイレクトは絶対リダイレクトに変換されて Location ヘッダに指定される。

server.tomcat.use-relative-redirects のデフォルト値は false になっている。

[Common Application properties](https://docs.spring.io/spring-boot/docs/2.3.3.RELEASE/reference/html/appendix-application-properties.html)

> Whether HTTP 1.1 and later location headers generated by a call to sendRedirect will use relative or absolute redirects.

server.tomcat.use-relative-redirects は Apache Tomcat の useRelativeRedirects 設定と同様のもの。

[Apache Tomcat 9 Configuration Reference \(9\.0\.37\) \- The Context Container](https://tomcat.apache.org/tomcat-9.0-doc/config/context.html)

> Controls whether HTTP 1.1 and later location headers generated by a call to javax.servlet.http.HttpServletResponse#sendRedirect(String) will use relative or absolute redirects. Relative redirects are more efficient but may not work with reverse proxies that change the context path. It should be noted that it is not recommended to use a reverse proxy to change the context path because of the multiple issues it creates. Absolute redirects should work with reverse proxies that change the context path but may cause issues with the org.apache.catalina.filters.RemoteIpFilter if the filter is changing the scheme and/or port. If the org.apache.catalina.STRICT_SERVLET_COMPLIANCE system property is set to true, the default value of this attribute will be false, else the default value will be true.

## 検証するリダイレクトの指定方法

### ビュー名に redirect: プレフィックスを使用

ビュー名に redirect: プレフィックスを使用することでリダイレクト先を指定することができる。

```java
@GetMapping("/controller/relative")
public ModelAndView relative() {
  // ビュー名に相対リダイレクトを指定
  return new ModelAndView("redirect:/controller/hello");
}
```
server.tomcat.use-relative-redirects=true の場合はそのまま相対リダイレクトが Location ヘッダに指定される。

server.tomcat.use-relative-redirects=false の場合は相対リダイレクトが絶対リダイレクトに変換されて Location ヘッダに指定される。

Spring 内部で HttpServletResponse#sendRedirect を呼び出していると思われる。

[Spring Web MVC サーブレットスタック \- ドキュメント](https://spring.pleiades.io/spring/docs/5.2.8.RELEASE/spring-framework-reference//web.html#mvc-redirecting-redirect-prefix)

> ビュー名の特別な redirect: プレフィックスを使用すると、リダイレクトを実行できます。UrlBasedViewResolver （およびそのサブクラス）は、これをリダイレクトが必要な命令として認識します。ビュー名の残りはリダイレクト URL です。
> 
> 最終的な効果は、コントローラーが RedirectView を返した場合と同じですが、コントローラー自体が論理ビュー名の観点から動作できるようになりました。論理ビュー名（redirect:/myapp/some/resource など）は現在のサーブレットコンテキストに関連してリダイレクトし、redirect:https://myhost.com/some/arbitrary/path などの名前は絶対 URL にリダイレクトします。
> 
> コントローラーメソッドに @ResponseStatus のアノテーションが付けられている場合、アノテーション値は RedirectView によって設定されたレスポンスステータスよりも優先されることに注意してください。

### ResponseEntity.BodyBuilder#location

ResponseEntity.BodyBuilder の location メソッドでリダイレクト先を指定することができる。

```java
@GetMapping("/restcontroller/relative")
public ResponseEntity<?> relative() {
  // Location ヘッダの値に相対リダイレクトを指定
  return ResponseEntity.status(HttpStatus.FOUND)
    .location(URI.create("/restcontroller/hello"))
    .build();
}
```

server.tomcat.use-relative-redirects が true でも false でも、指定した相対リダイレクトはそのまま Location ヘッダに指定される。

Spring 内部で HttpServletResponse#sendRedirect を呼び出していないと思われる。

### HttpServletResponse#sendRedirect

HttpServletResponse の sendRedirect メソッドでリダイレクト先を指定することができる。

```java
@GetMapping("/restcontroller/sendRedirect/relative")
public void sendRedirectRelative(HttpServletResponse res) throws IOException {
  // 相対リダイレクトを指定
  res.sendRedirect("/restcontroller/sendRedirect/hello");
}
```

server.tomcat.use-relative-redirects=true の場合はそのまま相対リダイレクトが Location ヘッダに指定される。

server.tomcat.use-relative-redirects=false の場合は相対リダイレクトが絶対リダイレクトに変換されて Location ヘッダに指定される。

## 検証用のコントローラークラスを2つ用意

```java
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class SampleController {

  @GetMapping("/controller/relative")
  public ModelAndView relative() {
    // ビュー名に相対リダイレクトを指定
    return new ModelAndView("redirect:/controller/hello");
  }

  @GetMapping("/controller/absolute")
  public ModelAndView absolute() {
    // ビュー名に絶対リダイレクトを指定
    return new ModelAndView("redirect:http://localhost:8080/controller/hello");
  }
}
```

```java
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;

@RestController
public class SampleRestController {

  @GetMapping("/restcontroller/relative")
  public ResponseEntity<?> relative() {
    // Location ヘッダの値に相対リダイレクトを指定
    return ResponseEntity.status(HttpStatus.FOUND)
      .location(URI.create("/restcontroller/hello"))
      .build();
  }

  @GetMapping("/restcontroller/absolute")
  public ResponseEntity<?> absolute() {
    // Location ヘッダの値に絶対リダイレクトを指定
    return ResponseEntity.status(HttpStatus.FOUND)
      .location(URI.create("http://localhost:8080/restcontroller/hello"))
      .build();
  }

  @GetMapping("/restcontroller/sendRedirect/relative")
  public void sendRedirectRelative(HttpServletResponse res) throws IOException {
    // 相対リダイレクトを指定
    res.sendRedirect("/restcontroller/sendRedirect/hello");
  }

  @GetMapping("/restcontroller/sendRedirect/absolute")
  public void sendRedirectAbsolute(HttpServletResponse res) throws IOException {
    // 絶対リダイレクトを指定
    res.sendRedirect("http://localhost:8080/restcontroller/sendRedirect/hello");
  }
}
```

## Location ヘッダの値を検証する

### server.tomcat.use-relative-redirects=true の場合

ModelAndView に指定した相対リダイレクト /controller/hello がそのまま Location ヘッダに指定される。

```
$ curl --include http://localhost:8080/controller/relative
HTTP/1.1 302 
Location: /controller/hello
Content-Language: ja-JP
Content-Length: 0
Date: Tue, 25 Aug 2020 11:10:27 GMT
```

ModelAndView に指定した絶対リダイレクト http://localhost:8080/controller/hello がそのまま Location ヘッダに指定される。

```
$ curl --include http://localhost:8080/controller/absolute
HTTP/1.1 302 
Location: http://localhost:8080/controller/hello
Content-Language: ja-JP
Content-Length: 0
Date: Tue, 25 Aug 2020 11:10:31 GMT
```

ResponseEntity に指定した相対リダイレクト /restcontroller/hello がそのまま Location ヘッダに指定される。

```
$ curl --include http://localhost:8080/restcontroller/relative
HTTP/1.1 302 
Location: /restcontroller/hello
Content-Length: 0
Date: Tue, 25 Aug 2020 11:10:35 GMT
```

ResponseEntity に指定した絶対リダイレクト http://localhost:8080/restcontroller/hello がそのまま Location ヘッダに指定される。

```
$ curl --include http://localhost:8080/restcontroller/absolute
HTTP/1.1 302 
Location: http://localhost:8080/restcontroller/hello
Content-Length: 0
Date: Tue, 25 Aug 2020 11:10:38 GMT
```

HttpServletResponse#sendRedirect に指定した相対リダイレクト /restcontroller/sendRedirect/hello がそのまま Location ヘッダに指定される。

```
$ curl --include http://localhost:8080/restcontroller/sendRedirect/relative
HTTP/1.1 302 
Location: /restcontroller/sendRedirect/hello
Content-Length: 0
Date: Tue, 25 Aug 2020 11:10:41 GMT
```

HttpServletResponse#sendRedirect に指定した絶対リダイレクト http://localhost:8080/restcontroller/sendRedirect/hello がそのまま Location ヘッダに指定される。

```
$ curl --include http://localhost:8080/restcontroller/sendRedirect/absolute
HTTP/1.1 302 
Location: http://localhost:8080/restcontroller/sendRedirect/hello
Content-Length: 0
Date: Tue, 25 Aug 2020 11:10:45 GMT
```

### server.tomcat.use-relative-redirects=false の場合

ModelAndView に指定した相対リダイレクト /controller/hello が絶対リダイレクト http://localhost:8080/controller/hello に変換されて Location ヘッダに指定される。

```
$ curl --include http://localhost:8080/controller/relative
HTTP/1.1 302 
Location: http://localhost:8080/controller/hello
Content-Language: ja-JP
Content-Length: 0
Date: Tue, 25 Aug 2020 11:09:21 GMT
```

ModelAndView に指定した絶対リダイレクト http://localhost:8080/controller/hello がそのまま Location ヘッダに指定される。

```
$ curl --include http://localhost:8080/controller/absolute
HTTP/1.1 302 
Location: http://localhost:8080/controller/hello
Content-Language: ja-JP
Content-Length: 0
Date: Tue, 25 Aug 2020 11:09:25 GMT
```

ResponseEntity に指定した相対リダイレクト /restcontroller/hello がそのまま Location ヘッダに指定される。

```
$ curl --include http://localhost:8080/restcontroller/relative
HTTP/1.1 302 
Location: /restcontroller/hello
Content-Length: 0
Date: Tue, 25 Aug 2020 11:09:29 GMT
```

ResponseEntity に指定した絶対リダイレクト http://localhost:8080/restcontroller/hello がそのまま Location ヘッダに指定される。

```
$ curl --include http://localhost:8080/restcontroller/absolute
HTTP/1.1 302 
Location: http://localhost:8080/restcontroller/hello
Content-Length: 0
Date: Tue, 25 Aug 2020 11:09:35 GMT
```

HttpServletResponse#sendRedirect に指定した相対リダイレクト /restcontroller/sendRedirect/hello が絶対リダイレクト http://localhost:8080/restcontroller/sendRedirect/hello に変換されて Location ヘッダに指定される。

```
$ curl --include http://localhost:8080/restcontroller/sendRedirect/relative
HTTP/1.1 302 
Location: http://localhost:8080/restcontroller/sendRedirect/hello
Content-Length: 0
Date: Tue, 25 Aug 2020 11:09:42 GMT
```

HttpServletResponse#sendRedirect に指定した絶対リダイレクト http://localhost:8080/restcontroller/sendRedirect/hello がそのまま Location ヘッダに指定される。

```
$ curl --include http://localhost:8080/restcontroller/sendRedirect/absolute
HTTP/1.1 302 
Location: http://localhost:8080/restcontroller/sendRedirect/hello
Content-Length: 0
Date: Tue, 25 Aug 2020 11:09:46 GMT
```

## 参考資料

- [ModelAndView \(Spring Framework 5\.2\.8\.RELEASE API\) \- Javadoc 日本語訳](https://spring.pleiades.io/spring/docs/5.2.8.RELEASE/javadoc-api/org/springframework/web/servlet/ModelAndView.html)
- [ResponseEntity \(Spring Framework 5\.2\.8\.RELEASE API\) \- Javadoc 日本語訳](https://spring.pleiades.io/spring/docs/5.2.8.RELEASE/javadoc-api/org/springframework/http/ResponseEntity.html)
- [HttpServletResponse \(Servlet 4\.0 API Documentation \- Apache Tomcat 9\.0\.37\)](http://tomcat.apache.org/tomcat-9.0-doc/servletapi/javax/servlet/http/HttpServletResponse.html)
- [Spring Boot アプリケーションプロパティ一覧 \- ドキュメント](https://spring.pleiades.io/spring-boot/docs/2.3.3.RELEASE/reference/html/appendix-application-properties.html)
- [Spring Boot Reference Documentation \- Common Application properties](https://docs.spring.io/spring-boot/docs/2.3.3.RELEASE/reference/html/appendix-application-properties.html)
- [URL とは何か \- ウェブ開発を学ぶ \| MDN](https://developer.mozilla.org/ja/docs/Learn/Common_questions/What_is_a_URL)
- [Location \- HTTP \| MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Location)