Spring Security 使い方メモ　CORS


:::note warn
Spring Security は 5.4 以降、設定の書き方に大幅な変更が入っています。
詳しくは @suke_masa さんの [Spring Security 5.7でセキュリティ設定の書き方が大幅に変わる件 - Qiita](https://qiita.com/suke_masa/items/908805dd45df08ba28d8) を参照してください。
:::


[基礎・仕組み的な話](http://qiita.com/opengl-8080/items/c105152c9ca48509bd0c)
[認証・認可の話](http://qiita.com/opengl-8080/items/032ed0fa27a239bdc1cc)
[Remember-Me の話](http://qiita.com/opengl-8080/items/7c34053c74448d39e8f5)
[CSRF の話](http://qiita.com/opengl-8080/items/3720ed415c2df26251a7)
[セッション管理の話](http://qiita.com/opengl-8080/items/ad9159910501d1989876)
[レスポンスヘッダーの話](http://qiita.com/opengl-8080/items/4e800eb6990653b0395c)
[メソッドセキュリティの話](http://qiita.com/opengl-8080/items/76715fd3a92b6ca680d0)
[Run-As の話](http://qiita.com/opengl-8080/items/49aa6381c16c8b9a7f3c)
[ACL の話](http://qiita.com/opengl-8080/items/24a3118ef36bcf55ba71)
[テストの話](http://qiita.com/opengl-8080/items/eaa8f4eb9286a3df7986)
[MVC, Boot との連携の話](http://qiita.com/opengl-8080/items/4824d4a8c012f7ec53db)

番外編
[Spring Security にできること・できないこと](http://qiita.com/opengl-8080/items/6dc37f8b77abb5ae1642)


# CORS とは
@tomoyukilabs さんの [CORSまとめ - Qiita](http://qiita.com/tomoyukilabs/items/81698edd5812ff6acb34) が勉強になりました。

雑にまとめると、

- ブラウザは、あるオリジンから読み込まれたリソースから別のオリジンにアクセスできないようになっている
    - 同一オリジンポリシーというらしい
- サーバー側で、「このオリジンからのリクエストなら別オリジンであっても許可する」みたいな設定をすることで、クライアントはオリジンを超えてリクエストを送信できるようになる
- それが CORS (Cross-Origin Resource Sharing)

みたいな感じ。

「オリジン」が何なのかは [オリジンの定義 | 同一オリジンポリシー - Web セキュリティ | MDN](https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy#Definition_of_an_origin) とかを参照。

# Hello World
## クロスオリジンでアクセスするページの準備
以下の html を GitHub 上に配置し、静的ページとしてブラウザから開けるようにしておく。

```html:
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>cors test</title>
  </head>
  <body>
    <div>
      <select id="contextPath">
        <option value="namespace" selected>namespace</option>
        <option value="java-config">java-config</option>
      </select>
    </div>

    <div>
      <input id="headerName" placeholder="Header-Name" />
      <input id="headerValue" placeholder="Header-Value" />
    </div>

    <div>
      <input id="responseHeaderName" placeholder="responseHeaderName" />
    </div>

    <button type="button" id="sendButton">Send Request</button>

    <script src="https://code.jquery.com/jquery-3.2.1.min.js"
            integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
            crossorigin="anonymous"></script>

    <script>
      $(function() {
        $('#sendButton').on('click', function() {
          var headers = {};
          var headerName = $('#headerName').val();
          var headerValue = $('#headerValue').val();
          if (headerName) {
            headers[headerName] = headerValue;
          }

          var contextPath = $('#contextPath').val();

          $.ajax({
            url: 'http://localhost:8080/' + contextPath + '/cors',
            type: 'GET',
            dataType: 'text',
            headers: headers
          })
          .done(function(text, status, jqXhr) {
            console.log("text = " + text);

            var responseHeaderName = $('#responseHeaderName').val();
            if (responseHeaderName) {
              console.log("[ResponseHeader] " + responseHeaderName + " : " + jqXhr.getResponseHeader(responseHeaderName));
            }
          })
          .fail(function() {
            console.error(arguments);
          });
        });
      });
    </script>
  </body>
</html>
```

[実際に GitHub 上に上げたやつ](http://opengl-8080.github.io/Samples/java/spring-security/cors.html)

`localhost` に対して Ajax でアクセスするようにしている。

## サーバー側の実装
```java:CorsServlet.java
package sample.spring.security.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/cors")
public class CorsServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setHeader("Original-Header", "CORS!!");
        try (PrintWriter writer = resp.getWriter()) {
            writer.println("Hello CORS!!");
        }
    }
}
```

適当なヘッダーとテキストのボディを返すだけの実装。


## まずは CORS の設定なしでアクセスしてみる

**namespace**

```xml:applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
         http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
         http://www.springframework.org/schema/security
         http://www.springframework.org/schema/security/spring-security.xsd">
    
    <sec:http>
        <sec:intercept-url pattern="/login" access="permitAll" />
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <sec:form-login />
        <sec:logout />
    </sec:http>

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <sec:user name="hoge" password="hoge" authorities="" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>
```

**Java Configuration**

```java:MySpringSecurityConfig.java
package sample.spring.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin();
    }

    @Autowired
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.eraseCredentials(false)
            .inMemoryAuthentication()
                .withUser("hoge")
                .password("hoge")
                .roles();
    }
}
```

CORS 用の設定などは一切していない状態。

[先ほどの検証用ページ](http://opengl-8080.github.io/Samples/java/spring-security/cors.html) で `Send Request` ボタンをクリックすると、次のようなエラーが発生する。

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/9887d5ed-58a3-6956-37a7-f6afbfb01444.jpeg)

Spring Security の認証チェックに阻まれて、ログインページにリダイレクトさせられている。

また、 CORS の仕様で定義されている `Access-Control-Allow-Origin` のヘッダーがレスポンスにないため、アクセスできません、と言っている。

## Spring Security の設定を変更してアクセスできるようにする
**namespace**

```xml:applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
         http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
         http://www.springframework.org/schema/security
         http://www.springframework.org/schema/security/spring-security.xsd">
    
    <bean id="corsConfiguration" class="org.springframework.web.cors.CorsConfiguration">
        <property name="allowedOrigins">
            <list>
                <value>http://opengl-8080.github.io</value>
            </list>
        </property>
        <property name="allowedMethods">
            <list>
                <value>GET</value>
            </list>
        </property>
    </bean>

    <bean id="corsSource" class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">
        <property name="corsConfigurations">
            <map>
                <entry key="/cors" value-ref="corsConfiguration" />
            </map>
        </property>
    </bean>
    
    <sec:http>
        <sec:intercept-url pattern="/login" access="permitAll" />
        <sec:intercept-url pattern="/cors" access="permitAll" />
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <sec:form-login />
        <sec:logout />
        <sec:cors configuration-source-ref="corsSource" />
    </sec:http>

    ...
</beans>
```

**Java Configuration**

```java:MySpringSecurityConfig.java
package sample.spring.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll()
                .antMatchers("/cors").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .and()
            .cors()
                .configurationSource(this.corsConfigurationSource());
    }

    private CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedMethod("GET");
        corsConfiguration.addAllowedOrigin("http://opengl-8080.github.io");

        UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
        corsSource.registerCorsConfiguration("/cors", corsConfiguration);

        return corsSource;
    }

    ...
}
```

もう一度実行する。

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/7f7c726d-aabd-6511-f585-ed3fec9461d3.jpeg)

今度はアクセスが成功してサーバーが返したテキストを出力できている。

## 説明
### CORS を有効にする
```xml:applicationContext.xml
    <bean id="corsSource" class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">
        ...
    </bean>
    
    <sec:http>
        ...
        <sec:cors configuration-source-ref="corsSource" />
    </sec:http>
```

- `<http>` タグの下に `<cors>` を追加することで CORS の制御が有効になる
- このとき、 `configuration-source-ref` 属性に CORS の細かい設定を定義した `CorsConfigurationSource` の Bean を指定する


```java:MySpringSecurityConfig.java
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http...
                .and()
            .cors()
                .configurationSource(this.corsConfigurationSource());
    }

    private CorsConfigurationSource corsConfigurationSource() {
        ...
    }
```

- Java Configuration の場合は `.cors().configurationSource(CorsConfigurationSource)` で指定する

### CORS の設定
```xml:applicationContext.xml
    <bean id="corsConfiguration" class="org.springframework.web.cors.CorsConfiguration">
        <property name="allowedOrigins">
            <list>
                <value>http://opengl-8080.github.io</value>
            </list>
        </property>
        <property name="allowedMethods">
            <list>
                <value>GET</value>
            </list>
        </property>
    </bean>

    <bean id="corsSource" class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">
        <property name="corsConfigurations">
            <map>
                <entry key="/cors" value-ref="corsConfiguration" />
            </map>
        </property>
    </bean>
```

- CORS の設定には２つのクラスを使用する
    - `CorsConfigurationSource` と `CorsConfiguration`
- `CorsConfiguration` にどの HTTP メソッドを許可するか、どのオリジンからのアクセスを許可するか、といった詳細なルールを設定する
    - 上記設定の場合は、 `allowedOrigins` プロパティで許可するオリジンのリストを、 `allowedMethods` で許可する HTTP メソッドを指定している
    - `CorsConfiguration` は、デフォルトだとすべてのリクエストを**許可しない**設定になっているので、何かしら許可の設定をする必要がある
- `CorsConfigurationSource` は複数の `CorsConfiguration` を保持し、現在のリクエストに対してどの `CorsConfiguration` を適用すべきかを判断する
    - 上記設定では、実装クラスとして `UrlBasedCorsConfigurationSource` を使用している
    - `UrlBasedCorsConfigurationSource` は、どの `CorsConfiguration` を適用すべきかどうかをパスパターンと紐づけることで管理している
    - 上記設定の場合は、 `/cors` にアクセスが来た場合に `corsConfiguration` Bean で設定したルールを適用するようにしている
    - パスパターンは Ant 形式で指定できる

```java:MySpringSecurityConfig.java
    private CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedMethod("GET");
        corsConfiguration.addAllowedOrigin("http://opengl-8080.github.io");

        UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
        corsSource.registerCorsConfiguration("/cors", corsConfiguration);

        return corsSource;
    }
```

- Java Configuration の場合は `addAllowedMethod(String)` や `registerCorsConfiguration(String, CorsConfiguration)` など、１つずつ設定できるメソッドが利用できる
    - （XML でも利用できなくはないが、記述が煩雑になるだけなので普通は使わないと思う）

# preflight request へ対応させる
CORS の仕様では、デフォルトで許可されている HTTP メソッドや設定可能なヘッダーは限られている。

デフォルトで許可されていないヘッダーを送りたい、いった場合は、 prefilight request という「許可されているか確認する」ための事前リクエストを送ることになっている。

このリクエストに対してサーバーが「OKよ」と返した場合に限り、クライアントは本番のリクエストを送信する。

## まずは許可されていない HTTP ヘッダーを送信してみる
![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/354bb19d-3656-c1bb-0c4b-2331aa2b2722.jpeg)

`Hoge` という許可されていないヘッダーを載せてリクエストを送信してみる。

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/1ff377e4-222e-0078-fb60-41c804fd98a6.jpeg)

むっちゃ怒られた。

preflight request で許可されていないヘッダーがあったのでアクセスできなかった、と言っている。

リクエストとレスポンスのヘッダーを確認してみる。

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/9597ba56-80e2-cfce-2f4c-b5e9c435e45d.jpeg)

リクエストヘッダーに、 `Access-Control-Request-Headers: hoge` とあり、 `hoge` というヘッダーは許可されるか確認している。
しかし、レスポンスには `hoge` は許可しているよ、ということを表すヘッダーがない。

このため、ブラウザは `hoge` は許可されないヘッダーだと判断して処理を中断している。

## Hoge ヘッダーを許可する
**namespace**

```xml:applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>
    
    <bean id="corsConfiguration" class="org.springframework.web.cors.CorsConfiguration">
        <property name="allowedOrigins">
            <list>
                <value>http://opengl-8080.github.io</value>
            </list>
        </property>
        <property name="allowedMethods">
            <list>
                <value>GET</value>
            </list>
        </property>
        <property name="allowedHeaders"> ★追加
            <list>
                <value>Hoge</value>
            </list>
        </property>
    </bean>

    ...
</beans>
```

**Java Configuration**

```java:MySpringSecurityConfig.java
package sample.spring.security;

...

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {
    
    ...

    private CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        ...
        corsConfiguration.addAllowedHeader("Hoge"); ★追加

        ...
    }

    ...
}
```

`CorsConfiguration` の `allowedHeader` に `Hoge` を追加している。

再度動作検証する。

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/dd71a367-772e-9dd5-70d0-ce562981512e.jpeg)

今度はちゃんとリクエストが通った。

HTTP リクエストの様子を確認する。

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/1afacb82-187f-1384-e5b1-8937dc7f6bda.jpeg)

先に `OPTIONS` メソッドによる preflight request が実行され、その後本番の `GET` によるリクエストが送信されている。

`OPTIONS` の方のヘッダーを見てみる。

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/aca84a28-e197-10e9-8bf6-5c8792779728.jpeg)

レスポンスヘッダーに `Access-Control-Allow-Headers: hoge` が追加され、 `hoge` ヘッダーが許可されていることが示されている。

Spring Security は、 `CorsConfiguration` に設定した許可のルールに従い、自動的に preflight request への回答を行ってくれるようになっている。

クライアントが任意のレスポンスヘッダーにアクセスする場合もデフォルトでは制限があるが、 [exposedHeaders](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/cors/CorsConfiguration.html#addExposedHeader-java.lang.String-) を設定することで許可することができる。



# 参考
- [Spring Security Reference](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#cors)
- [CORSまとめ - Qiita](http://qiita.com/tomoyukilabs/items/81698edd5812ff6acb34#%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%81%AB%E7%8B%AC%E8%87%AA%E3%81%AEhttp%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%83%98%E3%83%83%E3%83%80%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%97%E3%81%A6%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%81%8B%E3%82%89%E8%AA%AD%E3%81%BF%E5%87%BA%E3%81%97%E3%81%9F%E3%81%84%E5%A0%B4%E5%90%88)
- [HTTP アクセス制御 (CORS) - HTTP | MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/HTTP_access_control)
- [同一オリジンポリシー - Web セキュリティ | MDN](https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy)