SpringBoot解体新書でお勉強したことをまとめておきます。
環境
Eclipse 2020-6
SpringBoot 2.3.5
Thymeleaf
参考
SpringBoot解体新書
TERASOLUNA Global Framework Development Guideline
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フォルダ以下を書く↓
<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ディレクトリ内に置く。
@GetMapping("/login")
public String findAccount() {
//ログイン画面へ遷移
return "login";
}
POSTリクエスト
submitボタン押す
→@PostMapping("/login")がリクエストを受け取る
→@RequestParamで値を受け取る
→値をModelクラスに加える
→htmlでModelクラスの値を表示
<!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>
↓
@Controller
public class LoginController {
@PostMapping("/login")
public String executeSQL(
@RequestParam("accountEmail") String accountEmail, //@RequestPramで値受け取り
Model model) {
//modelに値をセット
model.addAttribute("accountEmailModel", accountEmail);
return "loginsuccess";
}
}
}
↓
<!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的なもの)を渡すこと。
バリデーションの流れ
0、Mavenに依存関係を追記。
1、モデルクラスに@Sizeとかつける。
2、コントローラクラスの引数に(@ModelAttribute @Vlidated)つける
3、messages.properties変える
4、htmlにthymeleafでエラーメッセージの表示コードを書く (spring解体新書p2286
0、Mavenに依存関係を追記。
<!-- Spring Boot Starter Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
1、モデルクラスのフィールドに@Sizeとかつける。
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;
}
}
2、コントローラクラスのメソッド引数に(@ModelAttribute @Vlidated バリデーションしたいモデルクラス)つける
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";
}
}
3、エラーメッセージを変更する。
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でエラーメッセージの表示コードを書く
<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クラスを付与する。
エラーメッセージを赤字表示。
<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">3~15文字</small> <span
class="text-danger" th:if="${#fields.hasErrors('testModel.name2')}"
th:errors="*{testModel.name2}"> name error </span>
</div>
<input type="submit">
</form>
</body>
例外処理の基本
〇流れ
1、共通エラーページを用意
2、エラーメッセージを変更するには@ControllerAdvice クラスを用意
1、共通エラーページを用意
templatesディレクトリに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>
2、エラーメッセージを変更するには@ControllerAdvice クラスを用意
・@ControllerAdvice
@ControllerAdviceクラスでアプリケーション全体の例外処理を実装する。
・@ExcetionHandler
@ExcetionHandlerを付けたメソッドを作るとExceptionクラス毎の例外処理を実装できる。
(NullPointerExceptionとか)
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_ で始める。
ログイン・ログアウト
1、Mavenに依存関係を追記。
2、セキュリティ設定クラスにアクセス可否などを書く。
3、htmlでログインエラーメッセージ表示。
1、Mavenに依存関係を追記。
<!-- 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>
2、セキュリティ設定クラスにアクセス可否などを書く。
セキュリティ設定用クラスには@EnableWebSecurityを付けます。
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 {
/* 注意1
* ログイン不要ページの設定
* 順番が大事。
* メソッドチェーンは上から順に設定される。
* 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") //ログイン失敗時の移動先
/*※注意2
* 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」固定とする。
* なお、ユーザを一意に取得できるクエリを記述すること。複数件数取得された場合には、1件目のレコードがユーザとして使われる。
*/
.usersByUsernameQuery("SELECT accountEmail, accountPassword, true FROM account WHERE accountEmail = ?")
/* 注意4
* ユーザーのロールを取得するSQL文を引数に入れる
* ユーザの権限を取得するクエリを設定する。取得するデータは、「ユーザID」、「権限ID」の順とする。
* 認可の機能を使用しない場合は、「権限ID」は任意の固定値でよい。
*/
.authoritiesByUsernameQuery("SELECT accountEmail, accountRole FROM account WHERE accountEmail = ?")
.passwordEncoder(passwordEncoder());
}
}
注意1
ログイン不要ページの設定。
順番が大事。
メソッドチェーンは上から順に設定される。
例えば、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(); //それ以外は直リンク禁止
注意2
usernameParameterとpasswordParameterの引数はhtmlのname属性と同じにする。
ここではlogin.htmlのinputタグのname。
.usernameParameter("accountEmail") //ログインページのユーザーID
.passwordParameter("accountPassword") //ログインページのパスワード
.defaultSuccessUrl("/index", true); //ログイン成功後の移動先
注意3
ユーザIDとパスワードを取得するSQL文を引数に入れる。
ここでは、accountEmailをユーザーIDとしている。
where句の後にand句を付け足すとエラー出る。
以下、TERASOLUNA Global Framework Development Guidelineから引用。
ユーザ情報を取得するクエリを設定する。
取得するデータは、「ユーザID」、「パスワード」、「有効フラグ」の順とする。
「有効フラグ」による認証判定を行わない場合には、「有効フラグ」のSELECT結果を「true」固定とする。
なお、ユーザを一意に取得できるクエリを記述すること。複数件数取得された場合には、1件目のレコードがユーザとして使われる。
.usersByUsernameQuery("SELECT accountEmail, accountPassword, true FROMaccount WHERE accountEmail = ?")
注意4
ユーザーのロールを取得するSQL文を引数に入れる。
以下、TERASOLUNA Global Framework Development Guidelineから引用。
ユーザの権限を取得するクエリを設定する。取得するデータは、「ユーザID」、「権限ID」の順とする。
認可の機能を使用しない場合は、「権限ID」は任意の固定値でよい。
.authoritiesByUsernameQuery("SELECT accountEmail, accountRole FROM account WHERE accountEmail = ?")
.passwordEncoder(passwordEncoder());
3、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>
4、ログアウトボタンの設置
<form method="post" th:action="@{/logout}">
<button type="submit">ログアウト</button>
</form>
サインアップ
・パスワードを暗号化
@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でアクセスした時のみ「消える管理者画面ボタン」を表示する。
<!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>
ログイン情報の取得
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String name = auth.getName();//ユーザーネームを取得