Spring Boot で 認証機能を実装してみる

# はじめに
Spring Boot で Web アプリケーション環境を構築します。
今回は認証処理が必要なページを作ってみます。
認証情報（ユーザー名とパスワード）はちょっとだけ実践的に MySQL で管理することにします。

[前回](http://qiita.com/knamae/items/7a585b625629ff90b74c) に続き、Eclipse Neon 上で実装をしています。

# 環境
* Windows 10
* Eclipse Neon
* Java 1.8.0_111
* MySQL 5.7

# 必要なモジュールを指定する
Spring Security というモジュールで認証処理を実現できます。
データベース接続は Spring Data JPA というモジュールを使います。
ページ作成はテンプレートエンジン(Thymeleaf)を利用するので、これらのモジュールについても読み込んでおきます。

Eclipse で新規プロジェクト作成時に読み込むモジュールを指定できますが、常に新規プロジェクトからスタートできるとは限りませんよね・・・。ということで、今回は直接、build.gradle ファイルに定義を書き込みます。
build.gradle に dependencies という項目がありますので、このような感じに設定します。

```build.gradle
dependencies {
  compile('org.springframework.boot:spring-boot-starter-security')
  compile('org.springframework.boot:spring-boot-starter-data-jpa')
  compile('org.springframework.boot:spring-boot-starter-thymeleaf')
  compile('org.springframework.boot:spring-boot-starter-web')
  compile('org.springframework.boot:spring-boot-devtools')
  compile('mysql:mysql-connector-java')
  compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.5'
  testCompile('org.springframework.boot:spring-boot-starter-test')
}
```

# データベース接続の設定
次にデータベース接続の設定です。
application.properties に設定します。必要に応じてポート番号も書いてください。

```application.properties
spring.datasource.url=jdbc:mysql://[MySQL のサーバ名]/[データベース名]
spring.datasource.username=[ユーザ名]
spring.datasource.password=[パスワード]
spring.datasource.driverClassName=com.mysql.jdbc.Driver
```

# 実装
下準備が終わったので、いよいよ実装です。
フォーム認証が成功したらトップページを表示する仕組みを実装します。
認証が行われていない場合はログインページを表示します。

## メンバー情報にアクセス
データベースにあるメンバー情報にアクセスする処理を実装します。
その前に、データベースにメンバー情報を管理するテーブルを作成しておきます。

```create.sql
create table member(
    id BIGINT auto_increment,
    username VARCHAR(20) NOT NULL,
    password VARCHAR(20) NOT NULL,
    unique index (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO member (username, password)
    VALUES ('user', 'pass')
;
```

*このソースはサンプルなので平文でパスワードを保存してますが、本物のデータが入る場合にこんな実装しちゃいかんです*

### エンティティクラスを用意
作成したテーブルに対応したエンティティクラスを用意します。認証情報に利用するクラスなので、UserDetails を継承しておきます。いろいろとメソッドがついてきます。メソッド名からわかるとおりロック状態などを返すものですが、今回は軒並み true を返すことにします。

```MemberEntity.java
package com.example.entity;

import java.util.Collection;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

@Entity
@Table(name = "member")
public class MemberEntity implements UserDetails
{
    private static final long serialVersionUID = 1667698003975566301L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities()
    {
        return null;
    }

    @Override
    public String getPassword()
    {
        return this.password;
    }

    @Override
    public String getUsername()
    {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired()
    {
        return true;
    }

    @Override
    public boolean isAccountNonLocked()
    {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired()
    {
        return true;
    }

    @Override
    public boolean isEnabled()
    {
        return true;
    }
}
```

### レポジトリインターフェイスとサービスを作成
メンバー情報を取得するためのクラス（インターフェイス）を作成します。

```MemberRepository.java
package com.example.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.entity.MemberEntity;

public interface MemberRepository extends JpaRepository<MemberEntity, Long>
{
    public MemberEntity findByUsername(String username);
}
```

さらに、レポジトリを呼び出すサービスクラスを作成します。

```MemberServiceImpl.java
package com.example.service.impl;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.example.entity.MemberEntity;
import com.example.repository.MemberRepository;

@Service
public class MemberServiceImpl implements UserDetailsService
{
    private MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        if (StringUtils.isEmpty(username))
        {
            throw new UsernameNotFoundException("");
        }

        MemberEntity memberEntity = memberRepository.findByUsername(username);
        if (memberEntity == null)
        {
            throw new UsernameNotFoundException("");
        }

        return memberEntity;
    }

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository)
    {
        this.memberRepository = memberRepository;
    }
}
```

## トップページを作成

認証が必要なページを作成します。

```TopController.java
package com.example.web;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class TopController
{
    public static final String PAGE = "/";
    private static final String HTML = "top";

    @RequestMapping(value = TopController.PAGE, method = RequestMethod.GET)
    public String top(Model model)
    {
        return TopController.HTML;
    }
}
```

テンプレートファイルも作成しておきます。templates ディレクトリ以下に置いておきます。

```top.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-type" content="text/html; charset=UTF-8" />
</head>
<body>
  こんにちは！
  <form action="#" th:action="@{/logout}" method="POST">
    <input type="submit" value="ログアウト" />
  </form>
</body>
</html>
```

## アクセス権を設定
コンフィグクラスを作成して、アクセス権に関する設定を行います。
設定情報をソースに記載するのがイマドキなのです！

```WebSecurityConfig.java
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder)
            throws Exception
    {
        authenticationManagerBuilder.userDetailsService(this.userDetailsService);
    }

    @Autowired
    public void setUserDetailsService(UserDetailsService userDetailsService)
    {
        this.userDetailsService = userDetailsService;
    }
}
```

# 動作確認
Run > Run As > Spring Boot App で起動します。
起動後、ブラウザで以下のURLにアクセスしてください。

http://localhost:8080/

いかがでしょうか、無事に動きましたか？

# いろいろカスタマイズできるよ

## ログインページ
ここまでは標準のログインページを利用しましたが、もちろん、独自に作成することができます。
ログインページのコントローラーとテンプレートを用意して・・・。

```LoginController.java
package com.example.web;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController
{
    public static final String PAGE = "/login";
    private static final String HTML = "login";

    @RequestMapping(value = LoginController.PAGE)
    public String top(Model model)
    {
        return LoginController.HTML;
    }
}
```

```login.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>ログイン</title>
<meta http-equiv="Content-type" content="text/html; charset=UTF-8" />
</head>
<body>
  <form action="" th:action="@{/login}" method="post">
    <p>
      ユーザーID:
      <input type="text" name="user" />
    </p>
    <p>
      パスワード:
      <input type="password" name="pass" />
    </p>
    <p>
      <input type="submit" value="ログイン" />
    </p>
  </form>
  <div th:if="${session['SPRING_SECURITY_LAST_EXCEPTION']} != null">
    <span th:text="${session['SPRING_SECURITY_LAST_EXCEPTION'].message}"></span>
  </div>
</body>
</html>
```

設定を追加します。
定義内にユーザ名とパスワードのパラメータ名を記載する箇所がありますので、HTMLと一致するように記載してください。


```WebSecurityConfig.java
package com.example.config;

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.security.core.userdetails.UserDetailsService;

import com.example.web.LoginController;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        httpSecurity.authorizeRequests().anyRequest().authenticated();
        httpSecurity.formLogin().loginPage(LoginController.PAGE).usernameParameter("user")
                .passwordParameter("pass").permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder)
            throws Exception
    {
        authenticationManagerBuilder.userDetailsService(this.userDetailsService);
    }

    @Autowired
    public void setUserDetailsService(UserDetailsService userDetailsService)
    {
        this.userDetailsService = userDetailsService;
    }
}
```

# ちょっと解説
## Spring Security
Spring Security はセキュリティ対策を行うモジュールです。今回は認証機能を実装しましたが、CSRF対策など、いろいろな機能を持っています。認証機能も、BASIC認証にも対応していたり、認証対象外のパスを指定したりとカスタマイズが可能です。

## Spring Data JPA
Spring Data JPA は SQL を書かずにデータベースにアクセスを行うモジュールです。基本的な処理（今回の場合は username という項目に該当するレコードを取得）はメソッド定義だけで完結することができます。便利！
奥深い世界が広がっているので詳しくはググっていただければ・・・と思いますが、ビリヤード協会とか、パラグライダー協会とか、パワーリフティング協会などの情報がヒットしてしまうので、ご注意を。。

# 参考
* [GETTING STARTED Securing a Web Application](http://spring.io/guides/gs/securing-web/)
* [Spring SecurityでWebの認証と認可を制御する](http://dev.classmethod.jp/server-side/server/spring-boot-web-security/)
* [Spring BootとSpring Securityでユーザ認証(インメモリ＆独自のユーザテーブル)](http://wannabe-jellyfish.hatenablog.com/entry/2016/05/15/142600)