SpringBoot基礎メモ

SpringBoot解体新書でお勉強したことをまとめておきます。

#環境
Eclipse 2020-6
SpringBoot 2.3.5
Thymeleaf

#参考
[SpringBoot解体新書](https://www.amazon.co.jp/【後悔しないための入門書】Spring解体新書-Boot2で実際に作って学べる！Spring-Security、Spring-JDBC、Spring-MyBatisなど多数解説！-ebook/dp/B07H6XLXD7/ref=sr_1_2?__mk_ja_JP=カタカナ&dchild=1&keywords=spring+boot&qid=1604989281&sr=8-2)
[TERASOLUNA Global Framework Development Guideline](https://terasolunaorg.github.io/guideline/1.0.1.RELEASE/ja/Security/Authentication.html)

#SpringBootプロジェクトの新規作成
ファイル→新規→その他→Springスターター・プロジェクト→プロジェクト名入力→次へ
→project dependencies画面（ライブラリ選択）
→DevTool/Lombok/H2database/JDBC API/Thymeleaf/Spring Web/検証/MySQL Driver を選択
→完了

##ライブラリの説明
・DevTool
自動でアプリを再起動してくれる。
コードを修正して保存すると自動再起動。

・Lombok
setter getterを自動で実装。

・JDBC API
Springが用意したJDBCが使える。
通常のJDBCより簡単にSQLを実行可能。

・Thymeleaf（タイムリーフ）
SpringBoot標準で使われるテンプレートエンジン。
JSPの問題点であったHTMLの中にJavaのコードが入って可読性が悪くなる点を改善したもの。

・Spring Web
SpringMVC,SpringBootが使えるようになる。

・検証
バリデーションができるようになる。

・MySQL Driver
MySQLがつかえるようになる。


## SpringBootでCSSやjsファイルを読み込む
staticフォルダの中に入れないと読み込まない。
パス指定はstaticフォルダ以下を書く↓

```hello.html
<link th:href="@{/css/style.css}" rel="stylesheet"> 
<script th:src="@{/js/test.js}"></script>
```
※注意
SpringSecurityを入れている場合は別途設定が必要。
セキュリティ設定クラスをいじる。


# GETリクエスト
http://localhost:8080/loginにアクセス
→@GetMapping("/login)がリクエストを受け取る
→return "login";でlogin.htmlをレスポンスとして返す。
→login.htmlが画面に表示される。
※htmlファイルはtemplateディレクトリ内に置く。

```java:loginController.java
@GetMapping("/login") 
public String findAccount() { 
        //ログイン画面へ遷移 
	return "login"; 
}
```


# POSTリクエスト
submitボタン押す
→@PostMapping("/login")がリクエストを受け取る
→@RequestParamで値を受け取る
→値をModelクラスに加える
→htmlでModelクラスの値を表示

```html:login.html
<!DOCTYPE html> 
<html xmlns:th="http://www.thymeleaf.org"> 
<head> 
<meta charset="UTF-8"> 
<title>ログイン</title> 
</head> 
<body> 
<form method="post" action="/login" name="loginform"> 
<input type="text" name="accountEmail" th:value="${accountEmail}" required> 
<input type="submit" value="送信"> 
</form> 
</body> 
</html>
```
↓

```Logincontroller.java
@Controller
public class LoginController {
       @PostMapping("/login") 
	public String executeSQL( 
			@RequestParam("accountEmail") String accountEmail,  //@RequestPramで値受け取り
			Model model) {

        //modelに値をセット
		model.addAttribute("accountEmailModel", accountEmail);  
			return "loginsuccess"; 
		} 
	}
}
```
↓

```loginsuccess.html
<!DOCTYPE html> 
<html xmlns:th="http://www.thymeleaf.org"> 
<head> 
<meta charset="UTF-8"> 
<title>ログイン成功</title> 
</head> 
<body> 
ここにアカウントEメールが表示↓↓ 
<p th:text="${accountEmailModel}"></p> 
</body> 
</html>
```

#バリデーションの基本

・ポイント
@Validatedにモデルクラス（javabeans的なもの)を渡すこと。

バリデーションの流れ
０、Mavenに依存関係を追記。
１、モデルクラスに@Sizeとかつける。
２、コントローラクラスの引数に（@ModelAttribute @Vlidated）つける
３、messages.properties変える
４、htmlにthymeleafでエラーメッセージの表示コードを書く　（spring解体新書ｐ2286

０、Mavenに依存関係を追記。

```pom.xml
<!-- Spring Boot Starter Validation -->
<dependency> 
	<groupId>org.springframework.boot</groupId> 
	<artifactId>spring-boot-starter-validation</artifactId> 
</dependency>
```

１、モデルクラスのフィールドに@Sizeとかつける。

```AccountModel.java
import javax.validation.constraints.NotBlank; 
import javax.validation.constraints.Pattern; 
import javax.validation.constraints.Size; 
public class TestModel { 

	@NotBlank 
	@Size(min = 3, max = 15) 
	@Pattern(regexp = "^[ぁ-んー]*$") 
	private String name; 

	public String getName() { 
		return name; 
	} 
	public void setName(String name) { 
		this.name = name; 
	} 
}
```

２、コントローラクラスのメソッド引数に（@ModelAttribute @Vlidated バリデーションしたいモデルクラス）つける

```HelloController.java
import org.springframework.stereotype.Controller; 
import org.springframework.ui.Model; 
import org.springframework.validation.BindingResult; 
import org.springframework.validation.annotation.Validated; 
import org.springframework.web.bind.annotation.GetMapping; 
import org.springframework.web.bind.annotation.ModelAttribute; 
import org.springframework.web.bind.annotation.PostMapping; 
@Controller 
public class HelloController { 

	@GetMapping("/hello") 
	public String getHello( 
			@ModelAttribute TestModel tModel) { 
		return "hello"; 
	} 
        //「@Validated TestModel」でTestModelクラスをバリデーション指定。
	@PostMapping("/hello") 
	public String postHello( 
			@ModelAttribute @Validated TestModel tModel, 
			BindingResult result, 
			Model model) { 
		System.out.println("post"); 

//入力エラーのときはgetHelloを呼び出してhello.htmlに戻る。
		if (result.hasErrors()) { 
			return getHello(tModel); 
		} 
		return "success"; 
	} 
}
```

３、エラーメッセージを変更する。
messages.propertiesを変更。

```messages.properties
# ================================================= 
# バリデーションエラーメッセージ 
# ================================================= 
#名前 
name=名前 
NotBlank.name={0}を入力してください。 
Size.name={0}は{2}文字以上{1}文字以下で入力してください。 
Pattern.name={0}は全角ひらがな入力してください。漢字不可。
```
{0}には フィールド名＝XXX　のXXXが入る。ここだと「名前」
{1}{2}にはアノテーション名が昇順で入る。ここだとmin max なので{1}がmax、{2}がmin


4-1、htmlにthymeleafでエラーメッセージの表示コードを書く

```hello.html
<body> 
<form method="POST" action="/hello" name="nameform"> 
<input type="text"  name="name" th:value="${name}"> 
<input type="submit"> 

ここにエラーメッセージが表示↓
<span 
th:if="${#fields.hasErrors('testModel.name')}" 
th:errors="*{testModel.name}">
</span> 
</form> 
</body>
```

4-2、ブートストラップバージョン
入力エラー時にis-invalidクラスを付与する。
エラーメッセージを赤字表示。

```hello.html
<body> 
	<form method="POST" action="/hello" name="nameform"> 
		<div class="form-group"> 
			<label for="name">・名前(bootstrapバージョン)</label> <input 
				class="form-control" type="text" 
				th:classappend="${#fields.hasErrors('testModel.name2')} ? 'is-invalid'" 
				id="name" name="name2" th:value="${name2}" placeholder="Enter name"> 
			<small id="nameHelp" class="form-text text-muted">３～１５文字</small> <span 
				class="text-danger" th:if="${#fields.hasErrors('testModel.name2')}" 
				th:errors="*{testModel.name2}"> name error </span> 
		</div> 
		<input type="submit"> 
	</form> 
</body>
```

# 例外処理の基本
〇流れ
１、共通エラーページを用意
２、エラーメッセージを変更するには@ControllerAdvice クラスを用意

１、共通エラーページを用意
templatesディレクトリにerror.htmlを作る。
これだけでエラー出るとこのページが勝手に表示される。すごい。

```error.html
<!DOCTYPE html> 
<html xmlns:th="http://www.thymeleaf.org"> 
<head> 
<meta charset="UTF-8"> 
<title>Error</title> 
</head> 
<body> 
<h1 th:text="${status} + ':' + ${error}"></h1> 
<p th:text="${message}"></p> 
<form method="POST" th:action="@{/login}"> 
<button class="btn btn-link" type="submit">ログイン画面に戻る</button> 
</form> 
</body> 
</html>
```

２、エラーメッセージを変更するには@ControllerAdvice クラスを用意
・@ControllerAdvice
@ControllerAdviceクラスでアプリケーション全体の例外処理を実装する。

・@ExcetionHandler
@ExcetionHandlerを付けたメソッドを作るとExceptionクラス毎の例外処理を実装できる。
（NullPointerExceptionとか）

```GlobalControllAdvice.java
import org.springframework.http.HttpStatus; 
import org.springframework.stereotype.Component; 
import org.springframework.ui.Model; 
import org.springframework.web.bind.annotation.ControllerAdvice; 
import org.springframework.web.bind.annotation.ExceptionHandler; 
@ControllerAdvice 
@Component 
public class GlobalControllAdvice { 
	@ExceptionHandler(Exception.class) 
	public String ExceptionHandler(Exception e, Model model) { 
		model.addAttribute("error", "内部サーバーエラー"); 
		model.addAttribute("message", "サーバーエラーが発生しました。"); 
		model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR); 
		return "error"; 
	} 

@ExceptionHandler(NullPointerException.class) 
	public String NullPointerExceptionHandler(Exception e, Model model) { 
		model.addAttribute("error", "ぬるぽ"); 
		model.addAttribute("message", "■━⊂( ･∀･) 彡 ｶﾞｯ"); 
		model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR); 
		return "error"; 
	}
}
```

#セキュリティの基本(Spring Secrity)
〇ポイント
パスワードは暗号化必須。
デフォルトでCSRF対策が入っている。
ロール名はROLE_ADMINやROLE_GENERALのように、ROLE_ で始める。

##ログイン・ログアウト
１、Mavenに依存関係を追記。
２、セキュリティ設定クラスにアクセス可否などを書く。
３、htmlでログインエラーメッセージ表示。

１、Mavenに依存関係を追記。

```pom.xml
<!-- SpringSecurity-->
<dependency> 
	<groupId>org.springframework.boot</groupId> 
	<artifactId>spring-boot-starter-security</artifactId> 
</dependency> 

<!-- Thyemeleaf拡張--> 
<dependency> 
	<groupId>org.thymeleaf.extras</groupId> 
	<artifactId>thymeleaf-extras-springsecurity5</artifactId> 
</dependency>
```

２、セキュリティ設定クラスにアクセス可否などを書く。
セキュリティ設定用クラスには@EnableWebSecurityを付けます。

```SecurityConfig.java
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	private DataSource dataSource;

	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Override
	public void configure(WebSecurity web) throws Exception {
		//静的リソースを除外 。wejars配下とcss配下を除外。
		web.ignoring().antMatchers("/webjars/**", "/css/**");

	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {

		/* 注意１
		 * ログイン不要ページの設定
		 * 順番が大事。
		 * メソッドチェーンは上から順に設定される。
		 * anyRequest().authenticated().antMatchers("xxx").permitAll()
		 * にすると全リクエストに認証が必要になってしまう。
		 */
		http
				.authorizeRequests()
				.antMatchers("/webjars/**").permitAll() //webjarsへアクセス許可
				.antMatchers("/css/**").permitAll() //cssへアクセス許可
				.antMatchers("/login").permitAll() //ログインページは直リンクOK
				.antMatchers("/signup").permitAll() //ユーザー登録画面は直リンクOK
				.antMatchers("/index").permitAll() //ホーム画面は直リンクOK
				.antMatchers("/rest/**").permitAll() //RESTは直リンクOK
				.antMatchers("/adminpage").hasAuthority("ROLE_ADMIN") //アドミンページアクセスをアドミンに許可
				.anyRequest().authenticated(); //それ以外は直リンク禁止

		//ログイン処理
		http
				.formLogin()
				.loginProcessingUrl("/login") //ログイン処理のパス
				.loginPage("/login") //ログインページの指定
				.failureUrl("/login") //ログイン失敗時の移動先
				
				/*※注意２
				 * usernameParameterとpasswordParameterの引数は
				 * htmlのname属性と同じにする。
				 * ここではlogin.htmlのinputタグのname
				 */
				.usernameParameter("accountEmail") //ログインページのユーザーID
				.passwordParameter("accountPassword") //ログインページのパスワード
				.defaultSuccessUrl("/index", true); //ログイン成功後の移動先

		//ログアウト処理
		http
				.logout()
				.logoutRequestMatcher(new AntPathRequestMatcher("/logout")) //GETでログアウトする用。普通はPOSTなので不要。
				.logoutUrl("/logout")
				.logoutSuccessUrl("/login");

		//CSRF対策をOFFにする。デフォルトではON
		//http.csrf().disable();
	}

	// ログイン時のアカウント情報を、DBから取得する
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {

		auth.jdbcAuthentication()
				.dataSource(dataSource)
				
				/* 注意3
				 * ユーザーIDとパスワードを取得するSQL文を引数に入れる
				 * ここでは、accountEmailをユーザーIDとしている。
				 * where句の後にand句を付け足すとエラー出る。これでハマった・・・。
				 * 
				 * ユーザ情報を取得するクエリを設定する。
				 * 取得するデータは、「ユーザID」、「パスワード」、「有効フラグ」の順とする。
				 * 「有効フラグ」による認証判定を行わない場合には、「有効フラグ」のSELECT結果を「true」固定とする。
				 * なお、ユーザを一意に取得できるクエリを記述すること。複数件数取得された場合には、１件目のレコードがユーザとして使われる。
				 */
				.usersByUsernameQuery("SELECT accountEmail, accountPassword, true FROM account WHERE accountEmail = ?")

				/* 注意4
				 *  ユーザーのロールを取得するSQL文を引数に入れる
				 *  ユーザの権限を取得するクエリを設定する。取得するデータは、「ユーザID」、「権限ID」の順とする。
				 *  認可の機能を使用しない場合は、「権限ID」は任意の固定値でよい。
				 */
				.authoritiesByUsernameQuery("SELECT accountEmail, accountRole FROM account WHERE accountEmail = ?")
				.passwordEncoder(passwordEncoder());
	}
}
```

**注意１**
ログイン不要ページの設定。
順番が大事。
メソッドチェーンは上から順に設定される。
例えば、anyRequest().authenticated().antMatchers("xxx").permitAll()　にすると全リクエストに認証が必要になってしまう。

```java:
http
				.authorizeRequests()
				.antMatchers("/webjars/**").permitAll() //webjarsへアクセス許可
				.antMatchers("/css/**").permitAll() //cssへアクセス許可
				.antMatchers("/login").permitAll() //ログインページは直リンクOK
				.antMatchers("/signup").permitAll() //ユーザー登録画面は直リンクOK
				.antMatchers("/index").permitAll() //ホーム画面は直リンクOK
				//.antMatchers("/rest/**").permitAll() //RESTは直リンクOK
				.antMatchers("/adminpage").hasAuthority("ROLE_ADMIN") //アドミンページをアドミンユーザーに許可
				.anyRequest().authenticated(); //それ以外は直リンク禁止
```

**注意２**
usernameParameterとpasswordParameterの引数はhtmlのname属性と同じにする。
ここではlogin.htmlのinputタグのname。

```java
.usernameParameter("accountEmail") //ログインページのユーザーID 
.passwordParameter("accountPassword") //ログインページのパスワード 
.defaultSuccessUrl("/index", true); //ログイン成功後の移動先
```

**注意3**
ユーザIDとパスワードを取得するSQL文を引数に入れる。
ここでは、accountEmailをユーザーIDとしている。
where句の後にand句を付け足すとエラー出る。
以下、[TERASOLUNA Global Framework Development Guideline](https://terasolunaorg.github.io/guideline/1.0.1.RELEASE/ja/Security/Authentication.html)から引用。

>ユーザ情報を取得するクエリを設定する。
>取得するデータは、「ユーザID」、「パスワード」、「有効フラグ」の順とする。 
>「有効フラグ」による認証判定を行わない場合には、「有効フラグ」のSELECT結果を「true」固定とする。 
>なお、ユーザを一意に取得できるクエリを記述すること。複数件数取得された場合には、１件目のレコードがユーザとして使われる。

```java:
.usersByUsernameQuery("SELECT accountEmail, accountPassword, true FROMaccount WHERE accountEmail = ?")
```

**注意4**
ユーザーのロールを取得するSQL文を引数に入れる。
以下、[TERASOLUNA Global Framework Development Guideline](https://terasolunaorg.github.io/guideline/1.0.1.RELEASE/ja/Security/Authentication.html)から引用。

>ユーザの権限を取得するクエリを設定する。取得するデータは、「ユーザID」、「権限ID」の順とする。
>認可の機能を使用しない場合は、「権限ID」は任意の固定値でよい。

```java:
.authoritiesByUsernameQuery("SELECT accountEmail, accountRole FROM account WHERE accountEmail = ?")
.passwordEncoder(passwordEncoder());
```

３、htmlでログインエラーメッセージの表示

```login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<form method="post" th:action="@{/login}">
		<input type="email" name="accountEmail" th:value="${accountEmail}">
		<input type="password" name="accountPassword">
		<button type="submit">ログイン</button>
	</form>

<!-- ログインエラーメッセージ --> 
<p th:if="${session['SPRING_SECURITY_LAST_EXCEPTION']} != null" 
    th:text="${session['SPRING_SECURITY_LAST_EXCEPTION'].message}" 
    class="text-danger">ログインエラーメッセージ</p>
</body>
</html>
```

４、ログアウトボタンの設置

```html:
<form method="post" th:action="@{/logout}">
	<button type="submit">ログアウト</button> 
</form>
```

##サインアップ
・パスワードを暗号化

```SignupController.java
@Controller
public class SignupController {

	@Autowired
	AccountMapper accountMapper;

        //パスワードエンコーダー
	@Autowired
	PasswordEncoder passwordEncoder;

	@GetMapping("/signup")
	public String getSignupPage(@ModelAttribute AccountModel acModel, Model model) {
		return "signup";
	}

	@PostMapping("/signup")
	public String executeSignUp(
			@ModelAttribute @Validated AccountModel acModel,
			BindingResult result,
			Model model) {

		if (result.hasErrors()) {
			System.out.println(result);
			return getSignupPage(acModel, model);
		}

		//アカウント新規登録処理
		accountMapper.insertAccount(
				acModel.getAccountEmail(),
                                
                                // パスワードを暗号化
				passwordEncoder.encode(acModel.getAccountPassword()),
				acModel.getAccountName(),
				acModel.getAccountRole());

		//登録したアカウント情報を取得
		AccountModel am = accountMapper.getAccount(
				acModel.getAccountEmail(), acModel.getAccountPassword());

		//アカウント情報をModelクラスに格納
		model.addAttribute("accountModel", am);

		return "index";
	}
}



```

##画面表示の認可
ROLE_ADMINでアクセスした時のみ「消える管理者画面ボタン」を表示する。

```html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
		 xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>Home</title>
</head>
<body>
	<div>
		<form method="get" th:action="@{/adminpage}" sec:authorize="hasRole('ADMIN')">
			<button type="submit">消える管理者画面ボタン</button>
		</form>
	</div>
</body>
</html>
```

##ログイン情報の取得

```java:
      Authentication auth = SecurityContextHolder.getContext().getAuthentication();
      String name = auth.getName();//ユーザーネームを取得
```