Spring Security 使い方メモ　メソッドセキュリティ


:::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)
[CORS の話](http://qiita.com/opengl-8080/items/db0aa1ec0be36886953b)
[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)

---

Spring Security では、 URL 指定によるアクセス制御以外にもメソッドレベルでのアクセス制御をサポートしている。
そして、 URL によるアクセス制御だけに依存せず、サービス層でメソッドレベルのアクセス制御をすることを推奨している。

http://docs.spring.io/spring-security/site/docs/4.2.1.RELEASE/reference/htmlsingle/#request-matching

> In practice we recommend that you use method security at your service layer, to control access to your application, and do not rely entirely on the use of security constraints defined at the web-application level.
> 【訳】
> 実際は、アプリケーションへのアクセス制御にはサービスレイヤでメソッドセキュリティを使用し、 Web アプリケーションレベルで定義されたセキュリティ制約の利用を完全に信頼しないことを推奨します。
> 
> URLs change and it is difficult to take account of all the possible URLs that an application might support and how requests might be manipulated.
> 【訳】
> URL が変わったときに、アプリケーションがサポートする全ての URL とリクエストをどう扱うかについて考慮することは困難です。
> 
> Security defined at the service layer is much more robust and harder to bypass, so you should always take advantage of Spring Security’s method security options.
> 【訳】
> サービスレイヤで定義されたセキュリティは、より堅牢でバイパス[^5]が困難になります。
> そのため、常に Spring Security のメソッドセキュリティオプションを利用すべきです。

[^5]: セキュリティチェックをすり抜けること


# メソッド実行前にチェック処理を入れる
## 実装
```java:MyMethodSecurityService.java
package sample.spring.security.service;

import org.springframework.security.access.prepost.PreAuthorize;

public class MyMethodSecurityService {
    
    @PreAuthorize("hasAuthority('ADMIN')")
    public String execute() {
        return "Hello Method Security!!";
    }
}
```

```java:MyMethodSecurityServlet.java
package sample.spring.security.servlet;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import sample.spring.security.service.MyMethodSecurityService;

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("/method-security")
public class MyMethodSecurityServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(req.getServletContext());
        MyMethodSecurityService service = context.getBean(MyMethodSecurityService.class);

        PrintWriter writer = resp.getWriter();
        try {
            writer.println(service.getMessage());
        } catch (AccessDeniedException e) {
            writer.println(e.getMessage());
        } finally {
            writer.close();
        }
    }
}
```

<span id="method-security-pre-authorize">**namespace**</span>

```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:global-method-security pre-post-annotations="enabled" />

    <bean class="sample.spring.security.service.MyMethodSecurityService" />

    <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="user" password="user" authorities="USER" />
                <sec:user name="admin" password="admin" authorities="ADMIN" />
            </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.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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 sample.spring.security.service.MyMethodSecurityService;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin();
    }
    
    @Bean
    public MyMethodSecurityService myMethodSecurityService() {
        return new MyMethodSecurityService();
    }

    @Autowired
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user")
                .password("user")
                .authorities("USER")
            .and()
                .withUser("admin")
                .password("admin")
                .authorities("ADMIN");
    }
}
```

## 動作確認
上が `user` でログインしたとき、下が `admin` でログインしたとき。

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/bdc1665c-c0b5-1a6b-2a42-d826f176052b.jpeg)

## 説明
```xml:applicationContext.xml
    <sec:global-method-security pre-post-annotations="enabled" />
```

```java:MySpringSecurityConfig.java
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {
```

- メソッドセキュリティを有効にするには、
    - namespace の場合は `<global-method-security>` タグを追加し `pre-post-annotations="enabled"` を設定する
    - Java Configuration の場合は `@EnableGlobalMethodSecurity` を追加し `prePostEnabled=true` を設定する

```java:MyMethodSecurityService.java
    @PreAuthorize("hasAuthority('ADMIN')")
    public String getMessage() {
        return "Hello Method Security!!";
    }
```

- セキュリティを適用したいメソッドに `@PreAuthorize` アノテーションをつける
- `value` には `<intercept-url>` の `access` と同じように式ベースのアクセス制御を指定する


# メソッドの後にチェック処理を入れる
## 実装
```java:MyMethodSecurityService.java
package sample.spring.security.service;

import org.springframework.security.access.prepost.PostAuthorize;

public class MyMethodSecurityService {
    
    @PostAuthorize("returnObject == 'hoge'")
    public String getMessage(String parameter) {
        System.out.println("parameter = " + parameter);
        return parameter;
    }
}
```

```java:MyMethodSecurityServlet.java
package sample.spring.security.servlet;

...

@WebServlet("/method-security")
public class MyMethodSecurityServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(req.getServletContext());
        MyMethodSecurityService service = context.getBean(MyMethodSecurityService.class);

        PrintWriter writer = resp.getWriter();
        try {
            String parameter = req.getParameter("parameter");
            writer.println(service.getMessage(parameter));
        } catch (AccessDeniedException e) {
            writer.println(e.getMessage());
        } finally {
            writer.close();
        }
    }
}
```

設定とかは[前のやつ](#method-security-pre-authorize)と同じ。

## 動作確認
`/method-security?parameter=hoge` にアクセス

```text:サーバー出力
parameter = hoge
```

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/1f53c6b6-53d5-a4b6-658c-2842d22cfafa.jpeg)

`/method-security?parameter=fuga` にアクセス

```text:サーバー出力
parameter = fuga
```

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/88d03ee0-fa91-9322-f187-04d97c655706.jpeg)

## 説明
```java:MyMethodSecurityService.java
    @PostAuthorize("returnObject == 'hoge'")
    public String getMessage(String parameter) {
        System.out.println("parameter = " + parameter);
        return parameter;
    }
```

- `@PostAuthorize` でアノテートすると、メソッドの実行後にチェックが行われる
- `value` には `@PreAuthorize` 同様、式ベースのアクセス制御を指定する
- `returnObject` という名前で、メソッドの戻り値を参照できる

# メソッドの引数を式で参照する
## 実装
```java:MyMethodSecurityService.java
package sample.spring.security.service;

import org.springframework.security.access.prepost.PreAuthorize;

public class MyMethodSecurityService {

    @PreAuthorize("#strValue == 'aaa' and #intValue == 1")
    public String getMessage(String strValue, int intValue) {
        return "strValue=" + strValue + ", intValue=" + intValue;
    }
}
```

```java:MyMethodSecurityServlet.java
package sample.spring.security.servlet;

...

@WebServlet("/method-security")
public class MyMethodSecurityServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(req.getServletContext());
        MyMethodSecurityService service = context.getBean(MyMethodSecurityService.class);

        PrintWriter writer = resp.getWriter();
        try {
            String strValue = req.getParameter("strValue");
            int intValue = Integer.parseInt(req.getParameter("intValue"));

            writer.println(service.getMessage(strValue, intValue));
        } catch (AccessDeniedException e) {
            writer.println(e.getMessage());
        } finally {
            writer.close();
        }
    }
}
```

## 動作確認
`/method-security?strValue=abc&intValue=1` にアクセス

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/a48deca1-2cb1-5a35-9643-c96d30af6ebb.jpeg)

`/method-security?strValue=aaa&intValue=1` にアクセス

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/5888d76e-1110-ac2c-be11-1a078e2a3859.jpeg)

## 説明
```java:MyMethodSecurityService.java
    @PreAuthorize("#strValue == 'aaa' and #intValue == 1")
    public String getMessage(String strValue, int intValue) {
```

- `#` を頭につけることで、パラメータを参照できる

## メソッドのパラメータ名の解決方法
メソッドのパラメータ名は、 Java 7 以下だとリフレクションで取ることができない。
一方 Java 8 以上の場合は、コンパイル時に `-parameters` オプションを指定していれば[リフレクションでパラメータ名を取ることができる](https://docs.oracle.com/javase/jp/8/docs/api/java/lang/reflect/Parameter.html)。

Spring は、これらの状況に対応できるよう様々なパラメータ名の解決方法を用意している。

### アノテーションで明示する
```java:
package sample.spring.security.service;

import org.springframework.security.access.method.P;
import org.springframework.security.access.prepost.PreAuthorize;

public class MyMethodSecurityService {
    
    @PreAuthorize("#strValue == 'aaa' and #intValue == 1")
    public String getMessage(@P("strValue") String strValue, @P("intValue") int intValue) {
        return "strValue=" + strValue + ", intValue=" + intValue;
    }
}
```

`@P` というアノテーションが用意されている。
これでパラメータをアノテートし、 `value` でパラメータ名を指定することで、パラメータ名をアノテーション経由で取得できるようになる。

実装クラスは `AnnotationParameterNameDiscoverer`。

### リフレクションで取得する
Java 8 以上で、コンパイル時に `-parameters` オプションを指定してコンパイルした場合に利用できる。

実装クラスは `StandardReflectionParameterNameDiscoverer`。

### class ファイルを解析してパラメータ名を取り出す
この機能を利用する場合は、コンパイル時にデバッグ情報を生成しておく必要がある。
具体的には、 `-g` オプションで最低限 `vars` を指定しておく必要がある。

ちなみに、 Gradle でコンパイルをしている場合はデフォルトでこのオプションが有効になっているので、特に気にすることなくこの機能を使うことができる。

実装クラスは `LocalVariableTableParameterNameDiscoverer`。

-----
上記機能は、「アノテーション」→「リフレクション」→「classファイル解析」の順番で適用され、最初に見つかった名前が使用される。


# 参考
- [Spring Security Reference](http://docs.spring.io/spring-security/site/docs/4.2.1.RELEASE/reference/htmlsingle/)
- [TERASOLUNA Server Framework for Java (5.x) Development Guideline — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.3.0.RELEASE documentation](http://terasolunaorg.github.io/guideline/5.3.0.RELEASE/ja/)