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

  • 17
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

概要

下図のように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

参考

サンプルコードの説明

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
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