Spring Bootで認証を行う方法

# ■環境
Spring Boot 1.2.5.Release
Java 8
Maven 3.3.1

# ■概要
Spring Bootで認証を行うために必要なモジュール、設定をまとめます。
Spring Securityの認証、認可の仕組みを使います。

# ■pom.xml

```xml:pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
<!-- ■Spring Boot本体 -->
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
 		<version>1.2.5.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>1.8</java.version>
	</properties>

<!-- ■Spring Boot関連 -->
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
<!-- ■Javaライブラリ -->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.16.4</version>
			<scope>provided</scope>
		</dependency>

<!-- ■フロントエンドフレームワーク -->
		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>jquery</artifactId>
			<version>2.1.4</version>
		</dependency>
	    <dependency>
	        <groupId>org.webjars</groupId>
	        <artifactId>bootstrap</artifactId>
	        <version>3.3.1</version>
	    </dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

```

# ■詳細
### １．ログイン画面を作成する

ログインボタンが押されると、/loginにPOSTされます。

```html:index.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>ログイン</title>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <link rel="stylesheet" href="../webjars/bootstrap/3.3.1/css/bootstrap.min.css" type="text/css"></link>
    <link rel="stylesheet" href="../css/common.css" type="text/css"></link>
<!--[if lt IE 9]>
	<script type="text/javascript" src="../javascript/ie8/html5shiv.js"></script>
	<script type="text/javascript" src="../javascript/ie8/respond.js"></script>
	<script type="text/javascript" src="../javascript/ie8/jquery-1.11.3.min.js"></script>
<![endif]-->
<!--[if gte IE 9]><!-->
	<script type="text/javascript" src="../webjars/jquery/2.1.4/jquery.min.js"></script>
<!--<![endif]-->
	<script type="text/javascript" src="../webjars/bootstrap/3.3.1/js/bootstrap.min.js"></script>
</head>
<body>
	<div class="container">
		<div class="row">
			<p><h1 id="title">ログイン</h1></p>
		</div>
		<div class="row">
 			<div class="col-md-3 col-lg-3"></div>
			<div class="col-md-6 col-lg-6">
				<div class="well well-sm">
					<form id="login_form" method="post" th:action="@{'/login'}">
						<fieldset id="login_field">
							<div class="form-group">
								<label>ログインID</label>
 								<input type="text" class="form-control" id="login_id" name="login_id" placeholder="ログインIDを入力してください" autofocus="" required="" />
							</div>
							<div class="form-group">
								<label>パスワード</label>
 								<input type="password" class="form-control" id="login_password" name="login_password" placeholder="パスワードを入力してください" required="" />
							</div>
							<div class="form-group">
								<input id="login_button" type="submit" value="ログイン" />
							</div>
						</fieldset>
					</form>
				</div>
			</div>
 			<div class="col-md-3 col-lg-3"></div>
		</div>
	</div>
</body>
</html>
```

### ２．Spring Securityの設定クラスを作成する

/loginへのPOSTはこの設定クラスで拾われます。

```java:SecurityConfig.java
package com.sample.config;

import com.sample.framework.security.SamplAuthenticationFailureHandler;
import com.sample.login.service.UserDetailsServiceImpl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
/**
 * Spring Security設定クラス.
 */
@Configuration
@EnableWebMvcSecurity	// Spring Securityの基本設定
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	public void configure(WebSecurity web) throws Exception {
		// セキュリティ設定を無視するリクエスト設定
		// 静的リソース(images、css、javascript)に対するアクセスはセキュリティ設定を無視する
		web.ignoring().antMatchers(
							"/images/**",
							"/css/**",
							"/javascript/**",
							"/webjars/**");
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 認可の設定
		http.authorizeRequests()
			.antMatchers("/", "/index").permitAll()	// indexは全ユーザーアクセス許可
			.anyRequest().authenticated();	// それ以外は全て認証無しの場合アクセス不許可

		// ログイン設定
		http.formLogin()
			.loginProcessingUrl("/login")	// 認証処理のパス
			.loginPage("/index")			// ログインフォームのパス
			.failureHandler(new SampleAuthenticationFailureHandler())		// 認証失敗時に呼ばれるハンドラクラス
			.defaultSuccessUrl("/menu")		// 認証成功時の遷移先
			.usernameParameter("login_id").passwordParameter("login_password")	// ユーザー名、パスワードのパラメータ名
			.and();

		// ログアウト設定
		http.logout()
			.logoutRequestMatcher(new AntPathRequestMatcher("/logout**"))		// ログアウト処理のパス
			.logoutSuccessUrl("/index");										// ログアウト完了時のパス

	}

	@Configuration
	protected static class AuthenticationConfiguration
	extends GlobalAuthenticationConfigurerAdapter {
		@Autowired
		UserDetailsServiceImpl userDetailsService;

		@Override
		public void init(AuthenticationManagerBuilder auth) throws Exception {
			// 認証するユーザーを設定する
			auth.userDetailsService(userDetailsService)
			// 入力値をbcryptでハッシュ化した値でパスワード認証を行う
			.passwordEncoder(new BCryptPasswordEncoder());

		}
	}
}
```

configureメソッドの下記で入力したログインID、パスワードを拾い、認証を行います。

```java
		// ログイン設定
		http.formLogin()
			.loginProcessingUrl("/login")	// 認証処理のパス
			.loginPage("/index")			// ログインフォームのパス
			.failureHandler(new SampleAuthenticationFailureHandler())		// 認証失敗時に呼ばれるハンドラクラス
			.defaultSuccessUrl("/menu")		// 認証成功時の遷移先
			.usernameParameter("login_id").passwordParameter("login_password")	// ユーザー名、パスワードのパラメータ名
			.and();
```

実際の認証処理はインナークラスAuthenticationConfigurationが暗黙のうちに呼ばれます。
AutowiredしているUserDetailsServiceImplクラスは２．で出てきます。

```java
	@Configuration
	protected static class AuthenticationConfiguration
	extends GlobalAuthenticationConfigurerAdapter {
		@Autowired
		UserDetailsServiceImpl userDetailsService;

		@Override
		public void init(AuthenticationManagerBuilder auth) throws Exception {
			// 認証するユーザーを設定する
			auth.userDetailsService(userDetailsService)
			// 入力値をbcryptでハッシュ化した値でパスワード認証を行う
			.passwordEncoder(new BCryptPasswordEncoder());

		}
	}
```

解説はしませんが認証失敗時に呼ばれるハンドラクラスも貼っておきます。

```java:SampleAuthenticationFailureHandler.java
package com.sample.framework.security;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.sample.framework.util.SampleErrorMessageMap;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

/**
 * Spring Securityの認証失敗時に呼ばれるハンドラクラス
 */
public class SampleAuthenticationFailureHandler implements
		AuthenticationFailureHandler {

	/* (非 Javadoc)
	 * @see org.springframework.security.web.authentication.AuthenticationFailureHandler
	 * #onAuthenticationFailure(javax.servlet.http.HttpServletRequest,
	 * javax.servlet.http.HttpServletResponse, org.springframework.security.core.AuthenticationException)
	 */
	@Override
	public void onAuthenticationFailure(
			HttpServletRequest httpServletRequest,
			HttpServletResponse httpServletResponse,
			AuthenticationException authenticationException)
					throws IOException, ServletException {

		String errorId = "";
		// ExceptionからエラーIDをセットする
		if(authenticationException instanceof BadCredentialsException){
			errorId = SampleErrorMessageMap.MSG_ERROR_0001;
		}

		// ログイン画面にリダイレクトする
		httpServletResponse.sendRedirect(httpServletRequest.getContextPath() + "/"
				+ httpServletRequest.getParameter("identifier") + "/index?error=" + errorId);
	}
}
```

### ３．UserDetailsServiceクラスの実装クラスを作成する

２．で作成した設定クラスの中で呼ばれます。
OverrideしているloadUserByUsernameメソッドが暗黙のうちに呼ばれますので、このメソッドの中でログイン画面で入力されたログインIDから認証するユーザー情報を取得します。

```java:UserDetailsServiceImpl.java
package com.sample.login.service;

import java.util.ArrayList;
import java.util.List;

import com.sample.db.domain.entity.loginuser.custom.MLoginUser;

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.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * UserDetailsServiceの実装クラス
 * Spring Securityでのユーザー認証に使用する
 */
@Component
public class UserDetailsServiceImpl implements UserDetailsService {


	/* (非 Javadoc)
	 * @see org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername(java.lang.String)
	 */
	@Override
	public UserDetails loadUserByUsername(String login_id)
			throws UsernameNotFoundException {

        // 認証を行うユーザー情報を格納する
		MLoginUser user = null;
		try {
			// 入力したユーザーIDから認証を行うユーザー情報を取得する
			// 処理内容は省略
		} catch (Exception e) {
			// 取得時にExceptionが発生した場合
			throw new UsernameNotFoundException("It can not be acquired User");
		}

        // ユーザー情報を取得できなかった場合
		if(user == null){
			throw new UsernameNotFoundException("User not found for login id: " + login_id);
		}

		// ユーザー情報が取得できたらSpring Securityで認証できる形で戻す
		return new LoginUser(user);
	}

}
```

### ４．認証ユーザーの情報を格納するクラスを作成する

３．で以下の記述がありました。

```java
		// ユーザー情報が取得できたらSpring Securityで認証できる形で戻す
		return new LoginUser(user);
```

Spring Securityで認証できる形というのは、Spring Securityで用意されているUserクラスもしくはそれを継承したクラスであるということです。

```java:LoginUser.java
package com.sample.login.service.data;

import java.util.List;

import com.sample.db.domain.entity.loginuser.custom.MLoginUser;

import org.springframework.security.core.authority.AuthorityUtils;

/**
 * 認証ユーザーの情報を格納するクラス
 */
public class LoginUser
extends org.springframework.security.core.userdetails.User {
	/**
	 * ログインユーザー
	 */
	private final MLoginUser mLoginUser;
	/**
	 * その他、ログイン後に利用したい情報があればここで宣言する
	 */
        ・・・

	/**
	 * コンストラクタ
	 * @param user
	 */
	public LoginUser(MLoginUser user) {
		// スーパークラスのユーザーID、パスワードに値をセットする
		// 実際の認証はスーパークラスのユーザーID、パスワードで行われる
		super(user.getLoginUserId(), user.getPassword(),
				AuthorityUtils.createAuthorityList("ROLE_USER"));
		this.mLoginUser = user;
	}

	/**
	 *
	 * @return
	 */
	public MLoginUser getUser() {
		return mLoginUser;
	}
}
```



```java:MLoginUser.java
package com.sample.db.domain.entity.loginuser.custom;

import javax.persistence.Entity;

@Entity
public class MLoginUser implements java.io.Serializable {

	private String loginUserId;
	private String password;

	public MLoginUser() {
	}
}
```

# ■まとめ
１．ログイン画面でユーザーID、パスワードを入力して認証処理へPOST

２．POSTをSpring Securityの設定クラスで拾い、認証処理を行う

３．認証に必要な情報をUserDetailsServiceクラスのloadUserByUsernameメソッドで取得してSpring Securityで認証できる形で返す

４．ログイン画面で入力されたパスワードと取得したパスワードを照合する

```java:SecurityConfig.javaより抜粋
		@Override
		public void init(AuthenticationManagerBuilder auth) throws Exception {
			// 認証するユーザーを設定する
			auth.userDetailsService(userDetailsService)
			// 入力値をbcryptでハッシュ化した値でパスワード認証を行う
			.passwordEncoder(new BCryptPasswordEncoder());

		}
```

上記では取得したパスワードがbcryptでハッシュ化されていたため、ログイン画面で入力されたパスワードをbcryptでハッシュ化して照合を行っています。

# ■参考資料
[はじめてのSpring Boot](http://www.amazon.co.jp/%E3%81%AF%E3%81%98%E3%82%81%E3%81%A6%E3%81%AESpring-Boot%E2%80%95%E3%80%8CSpring-Framework%E3%80%8D%E3%81%A7%E7%B0%A1%E5%8D%98Java%E3%82%A2%E3%83%97%E3%83%AA%E9%96%8B%E7%99%BA-I%E3%83%BBO-BOOKS/dp/4777518655)