Spring Framework勉強メモ 3 - Spring Securityを用いる

# はじめに
Spring FrameworkにはSpring Securityというライブラリがあり、認証や認可などの仕組みを簡単に作成することができます。ここでは、Spring Securityを用いて、ユーザ認証を行う例を実現してみたいと思います。

http://projects.spring.io/spring-security/

Spring Frameworkの基本をご存知ない方は、この記事の前に以下の記事に目を通しておいて頂けると後の記事が幾分読みやすくなるかと思います。

http://qiita.com/yokobonbon/items/c0c6f8d9e7b80afdf986

# Spring Security最初の一歩
まず、build.gradleに以下を追加し、Spring Securityを使用可能とします。

```java
dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("org.springframework.boot:spring-boot-starter-freemarker")
}
```

アプリケーションが提供するページにアクセス時にID, Passwordを求める実装を行います。これはとても簡単で、@EnableWebSecurityアノテーションを付加し、以下のようなSecurityConfigクラスを実装すれば良いだけです。 以下は、ID:user, Password:passwordを求める例です（全てのページに対して)

```java
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user").password("password").roles("USER");
    }
}
```

Loginページの記述はしていませんが、Spring Securityが以下のようなページを作成し、ID, Passwordを求めてくれます。

<img width="292" alt="スクリーンショット 2016-02-15 11.47.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/85182/c955524b-699d-7d8b-3395-4f916a8e1bd3.png">

ID, Passwordを間違った場合は、エラーも返してくれます。

<img width="316" alt="スクリーンショット 2016-02-15 11.49.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/85182/9e9fa804-6537-7761-02f0-3129a9d5c616.png">

# ログイン、ログアウト
先ほどはSpring Securityが作成してくれるログインページを利用しましたが、実際のケースではデフォルトのログインUIを使うことはないかと思います。次は、自前のログインページを作成して使うようにします。またログアウトの設定も行います。

この設定には、HttpSecurityクラスを用います。先ほど作成したSecurityConfigクラスに以下のメソッドを追加して下さい。@Overrideで記述しているように、configureメソッドは、WebSecurityConfigurerAdapterクラスに含まれるメソッドです。

```java
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/login");
    }
```

まず一行目のhttp.authorizedRequests().anyRequest().authenticated()で原則、ページのアクセスに認証が必要ということを記述しています。

次に .formLogin().loginPage("/login).failureUrl("login?error").permitAll()でログインページがこれから用意する/loginページであること、失敗時は/login?errorに遷移すること、そしてこのページに関しては認証なしでアクセスできることを記述しています。

最後に.logout().logoutUrl("/logout").logoutSuccessUrl("/login")でログアウトを実施するためのパスが/logoutであり、ログアウト処理終了後に/loginページに遷移することを記述しています。

次に/loginが指定された場合のControllerの記述は以下のようになります。

LoginController.java

```java
@Controller
public class LoginController {
    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public ModelAndView showLoginPage(@RequestParam Optional<String> error) {
        return new ModelAndView("login", "error", error);
    }
}
```

ModelAndViewを返しています。これは、先ほどの設定でID, Passwordが一致しない場合に/login?errorに遷移するのですが、その時に?errorの部分をパラメータで受けて、それをログインページのUIに渡しています。

次にUsername, Passwordを入力するLoginページです。

login.ftl

```html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Hello Spring</title>
    </head>
    <body>
        <h1> My Login Page </h1>
        <form role="form" action="/login" method="post">
            <div>
                <label for="username">Username</label>
                <input type="text" id="username" name="username"/>
            </div>
            <div>
                <label for="password">Password</label>
                <input type="password" id="password" name="password"/>
            </div>
            <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
            <button type="submit" class="btn">Log in</button>
        </form>
        <#if error.isPresent()>
            <p>The username or password you have entered is invalid, try again.</p>
        </#if>
    </body>
</html>
```

formで指定しているusername, passwordはSpring Securityでデフォルトに指定されている変数です。これ以外を使いたい場合は、先ほどのHttpSecurityの設定をしなくてはいけません。(以下の例は、usernameをemailにしたい場合。

```java
.usernameParameter("email")
```

<#if error.isPresent()>の部分で、/login?errorで起動された場合は上記のエラーメッセージが出ます。

最後にWeb Appの/にあたるindex.ftlにLogout処理を追加します。

index.ftl

```
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Hello Spring</title>
    </head>
    <body>
        <h1> Welcome To Top Page </h1>
        <form action="/logout" method="post">
            <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
            <button type="submit">Log out</button>
        </form>
    </body>
</html>
```

formのアクションで/logoutを指定することで/logout処理がなされます

# 作成したアプリケーションの画面遷移
以下正常ケースの場合です。ログイン -> ホーム -> ログイン画面に戻るです

http://localhost:8080/login
<img width="253" alt="スクリーンショット 2016-02-15 23.09.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/85182/3091c201-0b12-db47-7582-0863650a1c90.png">

http://localhost:8080
<img width="307" alt="スクリーンショット 2016-02-15 23.09.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/85182/b4c95063-477a-2a26-087e-4723d271ecea.png">

Log outボタンを押すとログインページに戻る
http://localhost:8080/login
<img width="217" alt="スクリーンショット 2016-02-15 23.09.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/85182/a090e7d3-b71f-cb3e-d775-bb9374f139e4.png">

username, passwordを間違えると
<img width="209" alt="スクリーンショット 2016-02-15 23.09.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/85182/75c72ef4-ada1-4037-1357-94e49afa18bc.png">

http://localhost:8080/login?error に遷移する
<img width="419" alt="スクリーンショット 2016-02-15 23.09.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/85182/415cd992-eab0-daa1-f16a-784b08d7a483.png">

# ロールによる制御
先ほどのHttpSecurityの設定では、全てのページの設定が均一でした。HttpSecurityでは、ユーザのロールによってアクセス可能なページの変更が可能です。代表的なところでは、Administratorのみがアクセス可能なページを用意することがあるかと思います。

まずロールを以下のように定義します。（これは便宜上Enumにしているだけで、最終的に文字列になれば何でもかまいません）

public enum Role {
    USER, ADMIN
}

先ほどのSecurityConfigにadminユーザを以下のように追加します

```java
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user").password("password").roles(Role.USER.name())
                .and()
                .withUser("admin").password("admin").roles(Role.USER.name(), Role.ADMIN.name());
    }
```

そして、/adminページはADMINロールを持つユーザのみにhasRole()関数を用いて行います。また/ページはUSERロールでもアクセス可能とします。adminユーザはUSERロールも持ちますので、/にアクセスも可能です。

```java
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin").hasRole(Role.ADMIN.name())
                .antMatchers("/").hasRole(Role.USER.name())
                .and()
                .formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/login");
    }
```