82
79

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-10-09

概要

下図のように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] (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
82
79
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
82
79

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?