Spring Boot Securityを使ってみる

#環境
Spring Boot 1.5.9.RELEASE
Java 8
Maven 4.0.0

#概要
Spring Securityを使用し、DBにあるログイン情報と照合し認証をする。
DBはH2DBを使用し、ORMはDomaを使用します。

#はじめに
[Spring Inirializr](https://start.spring.io/)でプロジェクトを生成します。
![spring-initializr.png](https://qiita-image-store.s3.amazonaws.com/0/189711/1faa1d71-015a-7da7-e2af-9452eae2da55.png)

今回はORMにDomaを使用するので、注釈処理を設定しておきます。
![注釈処理.png](https://qiita-image-store.s3.amazonaws.com/0/189711/72767e8f-5b00-9df8-90b8-c90800554f68.png)
![ファクトリー・パス.png](https://qiita-image-store.s3.amazonaws.com/0/189711/0a104585-f6de-d973-9b2c-9cc4884e41ff.png)


#コード
デフォルトから変更しているもののみ記載します。

```pom.xml(※一部抜粋)
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</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>

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.seasar.doma.boot/doma-spring-boot-starter -->
		<dependency>
			<groupId>org.seasar.doma.boot</groupId>
			<artifactId>doma-spring-boot-starter</artifactId>
			<version>1.1.1</version>
		</dependency>
	</dependencies>
```

```UserEntity.java
package com.example.springbootsecuritysample.entity;

import java.util.Collection;

import org.seasar.doma.Entity;
import org.seasar.doma.Id;
import org.seasar.doma.jdbc.entity.NamingType;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import lombok.Getter;
import lombok.Setter;

/**
 * USERテーブルのEntity
 * @author T.Harao
 *
 */
@Entity(naming = NamingType.SNAKE_UPPER_CASE)
@Getter
@Setter
public class UserEntity implements UserDetails {

	@Id
	private String userId;
	private String password;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return null;
	}
	@Override
	public String getUsername() {
		return userId;
	}
	/**
	 * UserDetailsServiceでチェックするパスワードを返却する
	 * Lombokを使用している場合、フィールドに「password」があれば、
	 * ＠GetterでgetPassword()を生成してくれる為、不要
	 */
	@Override
	public String getPassword() {
		return password;
	}
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}
	@Override
	public boolean isEnabled() {
		return true;
	}

}
```

```UserDao.java
package com.example.springbootsecuritysample.dao;

import org.seasar.doma.Dao;
import org.seasar.doma.Select;
import org.seasar.doma.boot.ConfigAutowireable;

import com.example.springbootsecuritysample.entity.UserEntity;

/**
 * USERテーブルにアクセスするDAO
 * @author T.Harao
 *
 */
@Dao
@ConfigAutowireable
public interface UserDao {

	@Select
	public UserEntity selectByUserId(String userId);

}
```

```AuthService.java
package com.example.springbootsecuritysample.service;

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.springbootsecuritysample.dao.UserDao;
import com.example.springbootsecuritysample.entity.UserEntity;

/**
 * 認証を扱うService
 * @author T.Harao
 *
 */
@Service
public class AuthService implements UserDetailsService {

	@Autowired
	private UserDao dao;

	/**
	 * ユーザー読み込み
	 */
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

		if(username == null || "".equals(username)) {
			throw new UsernameNotFoundException("ユーザーIDが未入力です");
		}

		UserEntity user = dao.selectByUserId(username);
		if(user == null) {
			throw new UsernameNotFoundException("ユーザーIDが不正です。");
		}

		return user;
	}

}
```

```IndexForm.java
package com.example.springbootsecuritysample.form;

import org.hibernate.validator.constraints.NotEmpty;

import lombok.Getter;
import lombok.Setter;

/**
 * IndexControllerで使用するForm
 * @author T.Harao
 *
 */
@Getter
@Setter
public class IndexForm {

	@NotEmpty
	private String userId;
	@NotEmpty
	private String password;

}
```

```IndexController.java
package com.example.springbootsecuritysample.controller;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

import com.example.springbootsecuritysample.form.IndexForm;

/**
 * ログイン
 * @author T.Harao
 *
 */
@Controller
@RequestMapping({"/", "/index"})
public class IndexController {

	@ModelAttribute
	public IndexForm initForm(){
		return new IndexForm();
	}

	/**
	 * 初期表示
	 * @param mv
	 * @return
	 */
	@RequestMapping(value = {"/", "/index"}, method = RequestMethod.GET)
	public ModelAndView index(ModelAndView mv) {
		mv.setViewName("index/index");
		return mv;
	}

	/**
	 * 認証エラー時
	 * @param mv
	 * @return
	 */
	@RequestMapping(value = {"/", "/index"}, method = RequestMethod.POST)
	public ModelAndView login(@ModelAttribute @Validated IndexForm form, BindingResult result, ModelAndView mv) {

		if(!result.hasErrors()) {
			mv.addObject("errorMessage", "ログイン情報が間違っています");
		}

		mv.setViewName("index/index");
		return mv;
	}

}
```

```MenuController.java
package com.example.springbootsecuritysample.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

/**
 * メニュー
 * @author T.Harao
 *
 */
@Controller
@RequestMapping("/menu")
public class MenuController {

	/**
	 * 初期表示
	 * @param mv
	 * @return
	 */
	@RequestMapping(value = {"/", "/index"}, method = RequestMethod.GET)
	public ModelAndView index(ModelAndView mv) {
		mv.setViewName("menu/index");
		return mv;
	}

}
```

```FailureHandler.java
package com.example.springbootsecuritysample.config.handler;

import java.io.IOException;

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

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

/**
 * 認証失敗時のハンドラ
 * @author T.Harao
 *
 */
@Component
public class FailureHandler implements AuthenticationFailureHandler {

	/**
	 * 認証失敗時
	 */
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {

		//「/」にForwardする
		RequestDispatcher dispatch = request.getRequestDispatcher("/");
		dispatch.forward(request, response);

	}

}
```

```SuccessHandler.java
package com.example.springbootsecuritysample.config.handler;

import java.io.IOException;

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

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
/**
 * 認証成功時のハンドラ
 * @author T.Harao
 *
 */
@Component
public class SuccessHandler implements AuthenticationSuccessHandler {

	/**
	 * 認証成功時
	 */
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {

		//「/menu/」にリダイレクトする
		response.sendRedirect(request.getContextPath() + "/menu/");

	}

}
```

```SecurityConfig.java
package com.example.springbootsecuritysample.config;

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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;

import com.example.springbootsecuritysample.config.handler.FailureHandler;
import com.example.springbootsecuritysample.config.handler.SuccessHandler;
import com.example.springbootsecuritysample.service.AuthService;

/**
 * セキュリティ設定
 * @author T.Harao
 *
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	private AuthService service;

	@Autowired
	private FailureHandler failureHandler;

	@Autowired
	private SuccessHandler successHandler;

	/**
	 * WebSecurityの設定
	 */
	@Override
    public void configure(WebSecurity web) throws Exception {

		// 静的リソース(images、css、javascript)とH2DBのコンソールに対するアクセスはセキュリティ設定を無視する
		web.ignoring().antMatchers("/css/**", "/fonts/**", "/images/**", "/js/**", "/h2-console/**");

	}

	/**
	 * HttpSecurityの設定
	 */
	@Override
	protected void configure(HttpSecurity http) throws Exception {

		//認可の設定
		http.authorizeRequests()
			//認証無しでアクセスできるURLを設定
			.antMatchers("/", "/index/**").permitAll()
			//上記以外は認証が必要にする設定
			.anyRequest().authenticated();

		//ログイン設定
		http.formLogin()
			//認証処理のパスを設定
			.loginProcessingUrl("/index/login")
			//ログインフォームのパスを設定
			.loginPage("/")
			.loginPage("/index/**")
			//認証成功時にリダイレクトするURLを設定
			.defaultSuccessUrl("/menu/")
			//認証失敗時にforwardするURLを設定
			.failureForwardUrl("/")
			//認証成功時にforwardするURLを設定
			//.successForwardUrl("/")
			//認証成功時に呼ばれるハンドラクラスを設定
			//.successHandler(successHandler)
			//認証失敗時にリダイレクトするURLを設定
			//.failureUrl("/menu/")
			//認証失敗時に呼ばれるハンドラクラスを設定
			//.failureHandler(failureHandler)
			//ユーザー名、パスワードのパラメータ名を設定
			.usernameParameter("userId").passwordParameter("password");

	}

	/**
	 * 設定
	 */
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {

		//パスワードは平文でDBに登録する為、「NoOpPasswordEncoder」を設定する
		auth.userDetailsService(service)
			.passwordEncoder(NoOpPasswordEncoder.getInstance());

	}


}
```

```layout.html
<!DOCTYPE html>
<html
	xmlns        = "http://www.w3.org/1999/xhtml"
	xmlns:th     = "http://www.thymeleaf.org"
	xmlns:layout = "http://www.ultraq.net.nz/thymeleaf/layout"
>
<head>
	<meta charset="UTF-8" />
	<title layout:title-pattern="$DECORATOR_TITLE - $CONTENT_TITLE">Spring Securityテスト</title>
	<link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" media="all" />
	<link rel="stylesheet" type="text/css" href="/css/bootstrap-theme.min.css" th:href="@{/css/bootstrap-theme.min.css}" media="all" />

	<script type="text/javascript" src="/js/jquery-1.12.4.min.js" th:src="@{/js/jquery-1.12.4.min.js}"></script>
	<script type="text/javascript" src="/js/bootstrap.min.js" th:src="@{/js/bootstrap.min.js}"></script>
</head>
<body>
	<div class="contents" layout:fragment="contents"></div>
</body>
</html>
```

```index/index.html
<!DOCTYPE html>
<html
	xmlns        = "http://www.w3.org/1999/xhtml"
	xmlns:th     = "http://www.thymeleaf.org"
	xmlns:layout = "http://www.ultraq.net.nz/thymeleaf/layout"
	layout:decorator="layout"
	>
<head>
	<title>ログイン</title>
</head>
<body>
	<div layout:fragment="contents">
		<form class="form-horizontal" method="POST" action="/index/login/" th:action="@{/index/login}" th:object="${indexForm}">
			<div th:text="${errorMessage}?: ''" class="col-sm-offset-2 text-danger"></div>
			<div class="form-group">
				<p th:if="${#fields.hasErrors('*{userId}')}" th:errors="*{userId}" class="col-sm-offset-2 text-danger"></p>
				<label for="user-id" class="col-sm-2 control-label">ユーザーID</label>
				<div class="col-sm-5">
					<input type="text" class="form-control" id="user-id" th:field="*{userId}" placeholder="ユーザーID" />
				</div>
			</div>
			<div class="form-group">
				<p th:if="${#fields.hasErrors('*{password}')}" th:errors="*{password}" class="col-sm-offset-2 text-danger"></p>
				<label for="password" class="col-sm-2 control-label">パスワード</label>
				<div class="col-sm-5">
					<input type="password" class="form-control" id="password" th:field="*{password}" placeholder="パスワード" />
				</div>
			</div>
			<div class="form-group">
				<input type="submit" class="btn btn-primary col-sm-2 col-sm-offset-2" name="login" value="ログイン" />
				<input type="reset" class="btn btn-default col-sm-2 col-sm-offset-1" name="clear" value="クリア" />
			</div>
		</form>
	</div>
</body>
</html>
```

```menu/index.html
<!DOCTYPE html>
<html
	xmlns        = "http://www.w3.org/1999/xhtml"
	xmlns:th     = "http://www.thymeleaf.org"
	xmlns:layout = "http://www.ultraq.net.nz/thymeleaf/layout"
	layout:decorator="layout"
	>
<head>
	<title>メニュー</title>
</head>
<body>
	<div layout:fragment="contents">
		<h2>メニュー</h2>
	</div>
</body>
</html>
```

```application.yaml
#spring
spring:
  profiles:
    active: dev
  datasource:
    url: jdbc:h2:./db

#server
server:
  contextPath: /security-sample

#doma
doma:
  dialect: h2
```

```application-dev.yaml
#spring
spring:
  h2:
    console:
      enabled: true
  thymeleaf:
    cache: false
```

```application-production.yaml
#spring
spring:
  h2:
    console:
      enabled: false
  thymeleaf:
    cache: true
```

```selectByUserId.sql
select
	user_id
	,password
from
	user
where
	user_id = /*userId*/''
```

```schema.sql
--drop table if exists user;
create table if not exists user (
	user_id		varchar(30)	not null	primary key
	,password	varchar(30)	not null
);
```

```data.sql
insert into user (user_id,password) values ('test','pass');
```

フォルダ構成は以下の通りです。
![パッケージエクスプローラー.PNG](https://qiita-image-store.s3.amazonaws.com/0/189711/5980af39-ee3b-098d-adfa-4771c439eab1.png)

#動作確認
[http://localhost:8080/security-sample/](http://localhost:8080/security-sample/)にアクセスし、
ユーザーIDに`test`、パスワードに`pass`を入力するとメニュー画面に遷移します。
ログイン画面
![ログイン.PNG](https://qiita-image-store.s3.amazonaws.com/0/189711/7a2d8c5e-71c9-0530-b9bf-00696fd5209c.png)
メニュー画面
![メニュー.PNG](https://qiita-image-store.s3.amazonaws.com/0/189711/f0727e8f-b8c1-67eb-b097-61fad1e5afb2.png)

#認証後の処理について
認証成功時、失敗時の処理については`SecurityConfig.java`の以下の部分に記載します。

```SecurityConfig.java
	/**
	 * HttpSecurityの設定
	 */
	@Override
	protected void configure(HttpSecurity http) throws Exception {

		//認可の設定
		http.authorizeRequests()
			//認証無しでアクセスできるURLを設定
			.antMatchers("/", "/index/**").permitAll()
			//上記以外は認証が必要にする設定
			.anyRequest().authenticated();

		//ログイン設定
		http.formLogin()
			//認証処理のパスを設定
			.loginProcessingUrl("/index/login")
			//ログインフォームのパスを設定
			.loginPage("/")
			.loginPage("/index/**")
			//認証成功時にリダイレクトするURLを設定
			.defaultSuccessUrl("/menu/")
			//認証失敗時にforwardするURLを設定
			.failureForwardUrl("/")
			//認証成功時にforwardするURLを設定
			//.successForwardUrl("/")
			//認証成功時に呼ばれるハンドラクラスを設定
			//.successHandler(successHandler)
			//認証失敗時にリダイレクトするURLを設定
			//.failureUrl("/menu/")
			//認証失敗時に呼ばれるハンドラクラスを設定
			//.failureHandler(failureHandler)
			//ユーザー名、パスワードのパラメータ名を設定
			.usernameParameter("userId").passwordParameter("password");

	}
```
コメントアウトをしていますが、認証成功時、失敗時それぞれで
リダイレクト、フォワード、ハンドラクラスへの処理委譲が行えます。

今回作成したプロジェクトは[ここ](https://github.com/tanabe38/spring-boot-security-sample)にあります