Spring Security OAuthでRemoteTokenServicesを使う際の注意点とそのためのソースコードリーディング

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。齋藤です。 Release Itは読みましたか?あなたがWebアプリケーションの開発者で これからもWebアプリケーションを作るというのであれば 読んで損はありません。今すぐ買いましょう

今回は地味な記事です。OAuth2を使う上でいい感じにしてくれるライブラリのデフォルト設定の話です。

はじめに

Spring Security OAuthは皆さんご利用になったことはあるでしょうか? SpringとJavaでOAuth2といえばこのライブラリなのではないでしょうか。

今回はこのSpring Security OAuthを使って Resource Serverを作った時の注意点について紹介したいと思います。

ちなみにこのライブラリである、Spring Security OAuthに似た機能が Spring Security側に取り込まれることになっており 今後は主な機能追加がされないことが決まっています。

詳しくはこちらのSpringのブログをご覧ください。

前提

今回は前提として以下のような構成を考えます。

  • Authorization Serverがある
  • Authorization Serverとは別にResource Serverがある

このような状況下において ResourceServer側ではResourceServerTokenServicesのインスタンスを通じて 認証情報のやり取りを行います。

どういう注意点があるのか

こちらのドキュメントを見ると分かりますが、 このような構成の場合、RemoteTokenServicesが紹介されています。 このクラスはResource Server側で使うクラスなのですが アクセストークンから認証情報を引き出す、Spring Security OAuthのエンドポイントである、/oauth/check_tokenをHTTP接続経由で呼び出すことになります。 このRemoteTokenServicesをドキュメントの通り使った場合に注意が必要です。

こちらのRemoteTokenServicesのコードを 見てもらうと分かりますがデフォルトの設定として内部でRestTemplateのインスタンスを作成しています。

RestTemplateに詳しい皆さんならお分かりかもしれませんが ここでは詳しく追ってみます。

RestTemplateのコードを追っていく

少しだけコードを追ってみます。

RestTemplateのコンストラクタでは クラスパスのクラスの存在を元にいい感じにMessageConverterを登録してくれています。便利ですね。 ただ、今回見たいのはここではないので飛ばします。

RequestFactoryという、内部で使うHTTPクライアントの設定と生成を司るオブジェクトが デフォルトだとどうなっているかを見ていきます。

Intellij IDEAなどでコードジャンプしてsetRequestFactoryの定義元を探します。 すると継承関係にある親をたどっていくとHttpAccessorというクラスにジャンプすると思います。

このクラスのフィールド初期化を見ると したのようなコードが確認できました。

    private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();

デフォルトではSimpleClientHttpRequestFactoryというものを使っているようです。 嫌な予感がしてまいりました。 え?しませんか?

今度はSimpleClientHttpRequestFactoryを見ていきます

気を取り直して、SimpleClientHttpRequestFactoryを見ていきましょう。

この辺を見ると java.net.HttpURLConnectionを使っていることが分かります。

Javadocを見ると書いてあるんですがJDKに入っている標準のAPIを使うClientHttpRequestFactoryだそうですね。

話を戻して、コードをよく読んでみると openConnectionした後、prepareConnectionしているようです。 下のようなコードですね。

        HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
        prepareConnection(connection, httpMethod.name());

openConnectionは大した処理をしていないので飛ばします。

ここで見てほしいのはprepareConnectionです。

	protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
		if (this.connectTimeout >= 0) {
			connection.setConnectTimeout(this.connectTimeout);
		}
		if (this.readTimeout >= 0) {
			connection.setReadTimeout(this.readTimeout);
		}

		connection.setDoInput(true);

		if ("GET".equals(httpMethod)) {
			connection.setInstanceFollowRedirects(true);
		}
		else {
			connection.setInstanceFollowRedirects(false);
		}

		if ("POST".equals(httpMethod) || "PUT".equals(httpMethod) ||
				"PATCH".equals(httpMethod) || "DELETE".equals(httpMethod)) {
			connection.setDoOutput(true);
		}
		else {
			connection.setDoOutput(false);
		}

		connection.setRequestMethod(httpMethod);
	}

よく見てください。connectTimeoutとreadTimeoutの設定が0以上じゃないと HttpURLConnectionのタイムアウト値が設定されないことになっています!!!!

ではここでHttpURLConectionの親であるURLConnectionのJavadocをよく読みましょう。

これはsetConnectTimeoutのドキュメントを引用したものです。

Sets a specified timeout value, in milliseconds, to be used when opening a communications link to the resource referenced by this URLConnection. If the timeout expires before the connection can be established, a java.net.SocketTimeoutException is raised. A timeout of zero is interpreted as an infinite timeout.

最後の1文を読んでください。 「A timeout of zero is interpreted as an infinite timeout.」です。

タイムアウトしません。

setReadTimeoutの方も読んでおきましょう。

Sets the read timeout to a specified timeout, in milliseconds. A non-zero value specifies the timeout when reading from Input stream when a connection is established to a resource. If the timeout expires before there is data available for read, a java.net.SocketTimeoutException is raised. A timeout of zero is interpreted as an infinite timeout.

タイムアウトしません!!!!!!!!!

どうすべきか

こんな感じで設定しましょう。 今回はOkHttp3を選択しましたが、状況に応じてHttpClientのライブラリを選びましょう。

            RemoteTokenServices tokenServices = new RemoteTokenServices();

            ... // clientId, clientSecretなどの設定がこの辺にあるはず

            RestTemplate restTemplate = new RestTemplateBuilder()
                .requestFactory(new OkHttp3ClientHttpRequestFactory(
                        new OkHttpClient.Builder()
                            .connectTimeout(10_000) // デフォルト値を設定してるので状況に応じて変更
                            .readTimeout(10_000) // デフォルト値を設定してるので状況に応じて変更
                            .writeTimeout(10_000) // デフォルト値を設定してるので状況に応じて変更
                            .build()))
                // RemoteTokenServicesの中のコードを参考に設定
                .errorHandler(new DefaultResponseErrorHandler() {
                    @Override
                    // Ignore 400
                    public void handleError(ClientHttpResponse response) throws IOException {
                        if (response.getRawStatusCode() != 400) {
                            super.handleError(response);
                        }
                    }
                })
                .build();
            tokenServices.setRestTemplate(restTemplate);

まとめ

今回の記事で調べたところ、HttpURLConnectionはデフォルトではタイムアウトしないようです。 それに伴って、RestTemplateのデフォルトで作られる、SimpleClientHttpRequestFactoryのインスタンスでは タイムアウトの設定をしていなければ、HTTP接続はタイムアウトしません。

つまり、RemoteTokenServicesをRestTemplateをカスタマイズせずに使うとHTTP接続はタイムアウトしません。 落とし穴はいろんなところに転がっています。注意してライブラリを使いましょう。 タイムアウトしない通信処理には注意しましょう。

タイムアウトしない通信処理の恐ろしさはRelease Itでご確認ください

Spring Security OAuth2にある、RemoteTokenServicesをデフォルト設定のまま使うと 認証情報を取得する部分でHTTP接続を行うがタイムアウトしない、という話でした。

また、次の記事でお会いしましょう。

タイムアウトしない通信処理の恐ろしさはRelease Itでご確認ください!!!!!!!!!!!!!!!