読者です 読者をやめる 読者になる 読者になる

WEB API The Good Parts 4章の自分的まとめ

前々回からの続きで、今回は以下の本の4章を自分的にまとめたいと思う。

Web API: The Good Parts

Web API: The Good Parts

もしAPI を開発している、もしくはAPI を日々触っているのであれば、 是非買って読んでみてほしい。

HTTP の仕様を最大限利用する

4.2 ステータスコードを正しく使う

この章についてはHTTP のステータスコードを解説しているが、 内容はwiki を見るだけで十分。

HTTPステータスコード - Wikipedia

4.4 メディアタイプの指定

メディアタイプとはデータ本体の形式を表すもので、 リクエスト時にはレスポンスがこの形式でほしいと指定する場合はAccecptヘッダー、 リクエスト、レスポンス時に送信するデータ本体はこの形式であると指定する場合はContent-Typeヘッダーで表す。

代表的なメディアタイプは以下の表を参照する。

メディアタイプ データ形式
text/plain プレーンテキスト
text/html HTML 文書
application/xml XML 文書
text/css CSS 文書
application/javascript JavaScript
application/json JSON 文書
application/rss+xml RSS フィード
application/atom+xml Atom フィード
application/octet-stream バイナリデータ
application/zip zip ファイル
image/jpeg JPEG 画像
image/png PNG 画像
image/svg+xml SVG 画像
multipart/form-data 複数のデータで構成されるウェブフォームデータ
video/mp4 MP4 動画ファイル
application/vnd.ms-exel Excel ファイル
application/x-msgpack MessagePage
application/x-yaml YAML
application/x-plist プロパティリスト
application/x-www-form-urlencoded HTMLのフォームデータ

トップレベルのapplication, text が混同しやすいが、 appllication のほうが主流。
plain, html ,css 以外はapplication となる。

サブタイプがx-で始まるものはIANA に登録されていない、 もしくはもしくは過去登録されていなかった歴史的経緯が残っているもの。

自分で作る場合は以下の表を参考にするとよい。
いまはあまりx-は推奨されないよう。。。だが一般的にはx-を付与している。

ツリー名 接頭辞
Standards tree(標準ツリー) なし
Vendor tree(ベンダツリー) vnd
Personal tree(パーソナルツリー) prs.
Unregistered tree(未登録ツリー) x.

vnd.ms-excelマイクロソフトというベンダーのexcel という意味。
ただし、通常は以下のように会社名を入れるらしい。

  application/vnd.companyname.awesomeformat

JSONXML を用いた新しいデータ形式を定義する場合は+ で記載する。
例えばXML をベースとしたRSSフィードの場合は以下のようになっている。

  application/rss+xml

4.3 キャッシュとHTTP の仕様

キャッシュタイプ タイプ説明 対応ヘッダ 説明
Expiration Model(期限切れモデル) あらかじめレスポンスデータに保存期間を決めておき、期限が切れたら再度アクセスをして取得する Expires: Fri, 01 Jan 2016 00:00:00 GMT 指定した時間以降はキャッシュ切れと判断する
Cache-Control: max-age=3600 指定した秒数が経過した以降はキャッシュ切れと判断する
Validation Model(検証モデル) いま保持しているキャッシュが最新であるかを問い合わせて、データが更新されていた場合に取得する response:Last-Modified: Tue, 01 Jul 2014 00:00:00 GMT → request:If-Modified-Since:Tue, 01 Jul 2014 00:00:00 GMT 最終更新時刻を記述する。時刻が一致すれば最新キャッシュと判断する。
response:ETag: "1234567890123456" → request:If-None-Match:"1234567890123456" 任意の文字列。通常はハッシュ値など。値が一致すれば最新キャッシュと判断する。
キャッシュさせない キャッシュを全くさせたくない場合に用いる Cache-Control: no-cache キャッシュしない

キャッシュを行う際に、URI 以外にどのリクエストヘッダ項目をデータを一意に特定するために利用するか特定するために、Varyヘッダを指定する。
例えば、Accept-Language: jaAccept-Language: en を指定して送信されたデータは異なるものになるため、 Vary: Accept-Language と指定する。

4.5 同一生成元ポリシーとクロスオリジンリソース共有

XHTTPRequest では異なるドメインに対してアクセスを行い、レスポンスデータを読み込むことができない。 これは同一生成元ポリシー(Same Origin Policy)というセキュリティ上のポリシーによるものである。

これを回避するために、クロスオリジンリソース共有(CORS:Cross-Origin Resource Sharing)がある。

実装方法はSpring-Bootならここを参考にしてほしい。

Getting Started · Enabling Cross Origin Requests for a RESTful Web Service

実装ができればこのような動きになる。

リクエストヘッダ サーバ処理 レスポン
Originヘッダに送信元のドメイン 許可する一覧かチェックする 許可されている場合:Access-Control-Allow-Originヘッダに許可されたドメイン
許可されてない場合:403 エラー

CORS にはプリフライトリクエストというものがあり、 これはリクエストを行う前にそのリクエストが受け入れられるかどうかを事前にチェックするもので、 OPTIONS メソッドを使ってリクエストすると、許可されているメソッドやヘッダーが表示される。

キャッシュヘッダの実装

今回、全然使ったことがないヘッダはキャッシュのところ。
なので、自分で実装を試してみた。

環境

  • Java 1.8
  • Spring Boot 1.3.6

そもそもヘッダー関連の実装って?

そう、レスポンスに毎回同じ設定をするのって大変だなぁ。。。
Spring-Boot でカバーしてないかなぁ?って思ったら、カバーしてました!

Spring Security
Spring Security Reference

さっそくpom.xml に追加してみる。

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>

ここからはSpring Security 初心者の私につきあってもらいたい。。。

適当にAPI を作成してアクセスしてみると、Basic 認証が表示され、アクセスできない。。。

f:id:AHA_oretama:20160710164704j:plain

なんてこった。。。

Spring Security のページを見ると大半がAuthentication, Authorization とかについて書かれている。
デフォルトでBasic 認証が有効になってしまうのか。。。

一つ勉強になったところで、Basic 認証を無効にしてみる。

以下のクラスを追加することで、Basic 認証していたコードを上書きし、 全てのリクエストを許可するようになる。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll();
    }

}

@EnableWebSecurityWebSecurityConfigurer を設定するときのアノテーションで、 WebSecurityConfigurerAdapterconfigure(HttpSecurity http) がデフォルトでBasic 認証をしているコードとなるので、 それをOverrideして上書きする。 ここではBasic 認証をしている親クラスのメソッドsuper.configure(http) を呼び出してはいけない。

Sping Security を導入したことで、レスポンスヘッダーが以下のようになって、 API のデフォルトのキャッシュは無効になっている。

Cache-Control:no-cache, no-store, max-age=0, must-revalidate
Content-Type:application/json;charset=UTF-8
Date:Sun, 10 Jul 2016 08:00:56 GMT
Expires:0
Pragma:no-cache
Server:Apache-Coyote/1.1
Transfer-Encoding:chunked
X-Content-Type-Options:nosniff
X-Frame-Options:DENY
X-XSS-Protection:1; mode=block

Pragma:no-cache は古いヘッダーのようで、Cache-Controlが効かないときの予備だそう。
それ以外にもXSS対策などもついている。

では、URI ごとのヘッダーの設定は?

まずは独自のヘッダーを記述するStaticHeadersWriter を継承したクラスを作成する。

public class CacheControlHeadersWriter extends StaticHeadersWriter {

    /**
     * Creates a new instance
     */
    public CacheControlHeadersWriter() {
        super(createHeaders());
    }

    private static List<Header> createHeaders() {
        List<Header> headers = new ArrayList<Header>();
        headers.add(new Header("Cache-Control", "Cache-Control: max-age=3600"));
        headers.add(new Header("Pragma", "cache"));
        return headers;
    }
}

ここではmax-ageを使用して1日、キャッシュを保持させるようにしている。

さきほどのSecurityConfigクラスに対象のURIパスを指定して、 一度キャッシュを無効にしてから上記のCacheControlHeadersWriter を追加する。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll();

        CacheControlHeadersWriter headersWriter = new CacheControlHeadersWriter();
        http.antMatcher("/api/v1/prefectures/**").headers().cacheControl().disable().addHeaderWriter(headersWriter);
    }

}

これで特定のURI のときだけキャッシュが効くようになる。

ではではChrome で確かめてみよう!! あれ?効かない。。。

調べたらなんと!同じタブで直接アクセスするとキャッシュは効かないという!!

iis - Is Chrome ignoring Control-Cache: max-age? - Stack Overflow

別タブなら効くというなんということだ!
親切すぎて、逆にはまりやすいポイントになった!

たしかに別タブならキャッシュが効く。よかった。。。

RestTemplate によるキャッシュは?

RestTemplate はSpirng でAPI へリクエストを送信するためのクラスである。
このクラスでキャッシュを判別してキャッシュが有効であればそのキャッシュを利用する、ということをしたい。

しかし、どうやらまだ未対応なようだ。。。

[SPR-5821] HTTP cache and conditional requests support in RestTemplate - Spring JIRA

ただ他のGitHub に対応するためのMaven があるようだ。

GitHub - jirutka/spring-http-client-cache: A very simple HTTP cache for the Spring’s RestTemplate.

今回は上記は使用しないが、今後のSpring-Bootの開発に期待している。

【2016/08/02追記】Validation Model(検証モデル)

検証モデルの場合は簡単でSpring にその機能が提供されている

 @RequestMapping(value = "/get", method = RequestMethod.GET)
 public String myHandleMethod(WebRequest webRequest, Model model) {
    long lastModified = // application-specific calculation
    if (request.checkNotModified(lastModified)) {
      // shortcut exit - no further processing necessary
      return null;
    }
    // further request processing, actually building content
    model.addAttribute(...);
    return "myViewName";
}

上記のようにcheckNotModified を呼ぶだけで、
更新があった場合はLast-Modifiedヘッダーをつけてくれ、 更新がなかった場合はHTTPステータスを302 にして返却してくれる。

サンプル

いつものように作成したリポジトリを公開しているので、参考にしてほしい。

github.com