Spring Security＆Thymeleafで認証・認可


## Spring Security とは
Spring Security は、 Spring で作られたアプリケーションに認証・認可の機能を提供するフレームワークです。いくつか設定を追加するだけで、以下のような機能をWebアプリケーションに持たせることができます。

- 脆弱性対策
	- セッションフィクセイション対策
	- クリックジャッキング対策
	- CSRF対策
- 認証・認可
	- フォーム認証などを用いたログイン機構
	- ロールを用いたアクセス制御

## 前提
### 今回やること
今回は、Spring Security の機能を使って以下を実現します。

- 認証
	- ユーザとパスワードを使ってフォーム認証する
		- （データベースでもできるが）インメモリに持たせたユーザとパスワードを使う
		- ユーザまたはパスワードが違っていたらエラーメッセージを返す
- 認可
	- 管理者用アカウントと一般ユーザ用でロールを分ける
		- 管理者は `/admin` と `/user` にアクセス可能
		- 一般ユーザは `/user` のみにアクセス可能

なお、今回は Spring Boot アプリケーションに対してセキュリティを有効にする手順を記載します。

### ライブラリのバージョン
Spring Boot は 1.4 系、Spring Securiry は 4.x 系のものを使用します。
バージョンが違う場合、文法がまるで異なるためご注意ください。

### サンプルコード
実際に動作するサンプルコードは GitHub で公開しています。コードを読めばわかるよ、という人は直接下記にアクセスしてみてください。

- https://github.com/gushernobindsme/spring-security-sample

## 前準備
### 依存ライブラリの追加
今回はサーバサイドを Spring Boot で構築し、テンプレートエンジンに Tymeleaf を使用して画面描画を行います。そのためには以下のライブラリが必要となります。

- `spring-boot-starter-web`
- `spring-boot-starter-thymeleaf`

また Spring Securirty の機能を有効にするために、以下のライブラリが必要となります。

- `spring-boot-starter-security`
- `spring-security-web`
- `spring-security-config`

今回は Spring Security と Tymeleaf をインテグレーションさせるため、以下のライブラリも追加しましょう。

- `thymeleaf-extras-springsecurity4`

まとめると、 `build.gradle` には以下のような設定が追加されることになります。

```gradle
dependencies {
    compile("org.springframework.boot:spring-boot-starter-web:1.4.1.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf:1.4.1.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-security:1.4.1.RELEASE")
    compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity4:2.1.3.RELEASE")
    compile("org.springframework.security:spring-security-core:4.2.3.RELEASE")
    compile("org.springframework.security:spring-security-web:4.2.3.RELEASE")
    compile("org.springframework.security:spring-security-config:4.2.3.RELEASE")
}
```

### Controller
まずは、管理者用の画面と一般ユーザ用の画面を作ります。
それぞれ、管理者用は `AdminController` 一般ユーザ用は `UserController` とします。

```java
@Controller
@RequestMapping("/admin")
@Slf4j
public class AdminController {

    @RequestMapping
    public String index() {
        return "admin";
    }
}
```

```java
@Controller
@RequestMapping("/user")
@Slf4j
public class UserController {

    @RequestMapping
    public String index() {
        return "user";
    }

}
```

### Template
上記の Controller に対応する Tymeleaf テンプレートを用意します。
それぞれ、 `admin.html` と `user.html` とします（詳細はサンプルコード参照）。

あとは、 SpringBoot のお作法に従って `Application.java` を作成し、 `gradle bootRun` が通れば準備完了です。

## 実装例
事前準備ができたため、ここから Spring Security の話に移ります。

### Config
まずは、 Spring Security を有効にするための設定クラスを作ります。`WebSecurityConfigurerAdapter` を継承し、 `@EnableWebSecurity` を付与したクラスを用意することで、 Spring Security が有効になります。

```java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private static String ROLE_USER = "USER";
    private static String ROLE_ADMIN = "ADMIN";

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/user").hasAnyRole(ROLE_USER, ROLE_ADMIN)
                .antMatchers("/admin").hasRole(ROLE_ADMIN)
                .and()

                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/user")
                .usernameParameter("username")
                .passwordParameter("password")
                .permitAll()
                .and()

                .logout()
                .permitAll()
                .and()

                .csrf();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .antMatchers("/*.html", "/*.css")
                .antMatchers("/bootstrap/**");
    }

}
```

上記の例でやっていることをざっくり解説すると、

- `#configure(HttpSecurity)`
	- アクセス制御を有効にする
		- `/user` は一般ユーザと管理者ユーザのロールでアクセス可能とする
		- `/admin` は管理者ユーザのみアクセス可能とする
	- フォームログインを有効にする
		- ログインページの URL は `/login` とする
			- ユーザ名は `username` パスワードは `password` というプロパティ名とする
		- ログインに成功した場合は `/user` に遷移させる
		- ログインページに対しては、例外的にアクセス制御を無効にする
	- ログアウトページに対しては、例外的にアクセス制御を無効にする
	- CSRF 用のトークンを有効にする
- `#configure(WebSecurity)`
	- デフォルト設定だと静的ファイルに対してもアクセス制御がかかってしまうため、以下については除外する
		- html
		- css
		- BootStrap テンプレートのファイル

こんな感じ。なんとなく設定内容が想像できる実装になっていると思います。

今回は Tymeleaf と連携するため、`SpringTemplateEngine` に `TemplateResolver` のインスタンスを設定する必要があります。具体的な実装は以下の通りです。

```java
@Autowired
public TemplateResolver templateResolver;

@Bean
public SpringTemplateEngine templateEngine() {
    SpringTemplateEngine engine = new SpringTemplateEngine();
    engine.addDialect(new SpringSecurityDialect());
    engine.setTemplateResolver(templateResolver);
    return engine;
}
```

加えて、認証に使うユーザ名とパスワードの情報を設定します。今回は実装簡略化のため、データベースからではなく、インメモリで設定しています。 `UserDto` についてはサンプルコード参照。

```java
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    UserDto support = supportUser();
    UserDto admin = adminUser();

    auth.inMemoryAuthentication()
        .withUser(support.getUsername()).password(support.getPassword()).roles(ROLE_USER)
        .and()
        .withUser(admin.getUsername()).password(admin.getPassword()).roles(ROLE_ADMIN);
}

@Bean
@ConfigurationProperties("inmotion.admin")
public UserDto adminUser() {
    return new UserDto();
}

@Bean
@ConfigurationProperties("inmotion.user")
public UserDto supportUser() {
    return new UserDto();
}
```

実際のユーザ名、パスワードは `application.yml` に外出ししておくことにします。

```yml
inmotion:
  user:
    username: user
    password: pass12345
  admin:
    username: admin
    password: pass12345
```

### Controller
あとは、ログイン画面に対応する Controller を用意して、

```java
@Controller
public class AuthenticationController {

    @RequestMapping({"/", "/login"})
    public String login(@RequestParam(value = "error", required = false) String error, Model model) {
        if (error != null) {
            // エラーメッセージを出力する処理
        }
        return "login";
    }

}
```

### Template
`/login` で POST するためのログインフォームを用意すれば OK です。

```html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
    <form class="form-signin" method="post" th:action="@{/login}">
        <div class="alert alert-dismissible alert-danger" th:if="${errorMessage}">
            <button type="button" class="close" data-dismiss="alert">×</button>
            <p th:text="${errorMessage}"></p>
        </div>
        <h2 class="form-signin-heading">Please sign in</h2>
        <label for="username" class="sr-only">Username</label>
        <input type="text" id="username" name="username" class="form-control" placeholder="UserName"/>
        <label for="password" class="sr-only">Password</label>
        <input type="password" id="password" name="password" class="form-control" placeholder="Password"/>
        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
    </form>
</div>
</body>
</html>
```

`gradle bootRun` でアプリケーションを起動し、

- 指定したユーザ名、パスワードでログインできること
- アクセス制御が有効になっていること
	- 認可していない画面にアクセスしようとすると 403 が返ること
- CSRF対策用のトークンが生成されていること

を確認してみましょう。

## 参考資料
- [Spring Security](https://projects.spring.io/spring-security/)
- [Spring SecurityでWebの認証と認可を制御する - Developers.IO](https://dev.classmethod.jp/server-side/server/spring-boot-web-security/)
- [Spring Securityでユーザ認証を実装してみる - ももいろテクノロジー](http://inaz2.hatenablog.com/entry/2017/04/18/222721)