第3回/認証
をテンプレートにして作成
[
トップ
] [
新規
|
一覧
|
検索
|
最終更新
|
ヘルプ
]
開始行:
[[第3回]]
* 認証・認可 [#ldb0bb1e]
- テキストはパワーポイントを参照のこと
- 参考 [[SpringBoot/認証・認可]]
** ToDo管理アプリに認証・認可を実装する [#j079ac41]
*** やりたいこと [#e753a6d2]
+ ToDo管理に''メンバーごとのパスワード認証''をかけたい
+ これまで管理者が行っていたメンバー登録は,メンバー自身...
-- サインアップ: メンバーが自分自身でサービスに登録する...
-- サインイン: メンバーが登録した自身の認証情報でサービ...
+ 認可の権限は2種類
-- ''メンバー (MEMBER):'' ToDo画面 (/{mid}/todos) で自分...
--- 自身のTodDoを閲覧,登録,完了できる.全員のToDo (/{mi...
--- ただし,''他人のToDo画面は見ることができない''
-- ''管理者 (ADMIN):'' 管理者画面(/admin/register)でユ...
-- 登録済ユーザの一覧,新規登録,削除ができる
** 認証・認可部の実装 [#p48d9fed]
*** 方針 [#va9742c6]
- S1: Member, MemberFormにパスワードトロールを追加
- S2: UserDetailsの実装クラスUserDetailsImplを作る
- S3: MemverService にUserDetailsServiceを継承させ,loadU...
- S4: ToDoAppSecurityConfig を作る
*** 準備 [#zf1b5ff4]
- build.gradle の dependenciesの中に,下記を追記
implementation 'org.springframework.boot:spring-boot-sta...
implementation 'org.thymeleaf.extras:thymeleaf-extras-sp...
*** S1: Member, MemberFormにパスワードトロールを追加 [#rf...
- entity/Member.java
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Member {
@Id
String mid; //メンバーID
String name; //名前
String password; //パスワード(暗号化済)
String role; //ロール
}
- dto/MemberForm.java
@Data
public class MemberForm {
@Pattern(regexp ="[a-z0-9_\\-]{4,16}")
String mid; //メンバーID.英小文字,数字,ハイフン,...
@NotBlank
@Size(min = 1, max = 32)
String name; //名前.最大32文字
@NotBlank
@Size(min = 8)
String password; //パスワード
String role = "MEMBER"; //ロール.デフォルトは"MEMBER"
public Member toEntity() {
Member m = new Member(mid, name, password, role);
return m;
}
}
解説
- パスワード,ロールともに文字列型で定義
- ロールはデフォルトでMEMBERとしている
*** S2: UserDetailsの実装クラスUserDetailsImplを作る [#h2...
- dto/UserDetailsImpl.java
package jp.ac.kobe_u.cs.itspecialist.todoapp.dto;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.Simpl...
import org.springframework.security.core.userdetails.Use...
import jp.ac.kobe_u.cs.itspecialist.todoapp.entity.Member;
/**
* 認証に必要なUserDetailsの実装クラス.Memberをラップする
*/
public class UserDetailsImpl implements UserDetails {
Member member;
Collection<GrantedAuthority> authorities = new Array...
/**
* コンストラクタ
* @param member
*/
public UserDetailsImpl(Member member) {
this.member=member;
//メンバーのロールから権限を生成して追加
this.authorities.add(new SimpleGrantedAuthority(...
}
public Member getMember() {
return member;
}
@Override
public Collection<? extends GrantedAuthority> getAut...
return authorities;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getMid();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
*** S3: MemverService にUserDetailsServiceを継承させ,loa...
- service/MemberService.java
package jp.ac.kobe_u.cs.itspecialist.todoapp.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Auto...
import org.springframework.security.core.userdetails.Use...
import org.springframework.security.core.userdetails.Use...
import org.springframework.security.core.userdetails.Use...
import org.springframework.security.crypto.bcrypt.BCrypt...
import org.springframework.stereotype.Service;
import jp.ac.kobe_u.cs.itspecialist.todoapp.dto.MemberFo...
import jp.ac.kobe_u.cs.itspecialist.todoapp.dto.UserDeta...
import jp.ac.kobe_u.cs.itspecialist.todoapp.entity.Member;
import jp.ac.kobe_u.cs.itspecialist.todoapp.exception.To...
import jp.ac.kobe_u.cs.itspecialist.todoapp.repository.M...
/**
* メンバーのCRUDを行うサービス
*/
@Service
public class MemberService implements UserDetailsService{
@Autowired
MemberRepository mRepo;
@Autowired
BCryptPasswordEncoder encoder;
/**
* メンバーを作成する (C)
* @param form
* @return
*/
public Member createMember(MemberForm form) {
//IDの重複チェック
String mid = form.getMid();
if (mRepo.existsById(mid)) {
throw new ToDoAppException(ToDoAppException....
}
Member m = form.toEntity();
m.setPassword(encoder.encode(m.getPassword())); ...
return mRepo.save(m);
}
/**
* メンバーを取得する (R)
* @param mid
* @return
*/
public Member getMember(String mid) {
Member m = mRepo.findById(mid).orElseThrow(
() -> new ToDoAppException(ToDoAppExcept...
return m;
}
/**
* 全メンバーを取得する (R)
* @return
*/
public List<Member> getAllMembers() {
return mRepo.findAll();
}
public boolean existById(String mid) {
return mRepo.existsById(mid);
}
/**
* メンバーを削除する (D)
*/
public void deleteMember(String mid) {
Member m = getMember(mid);
mRepo.delete(m);
}
/* ------------------ ここから追加分 --------------...
/**
* Spring Securityのメソッド.ユーザIDを与えて,ユー...
*/
@Override
public UserDetails loadUserByUsername(String mid) th...
Member m = mRepo.findById(mid).orElseThrow(
() -> new UsernameNotFoundException(mid + ":...
);
return new UserDetailsImpl(m);
}
/**
* 管理者を登録するサービス.
*/
public Member registerAdmin(String adminPassword) {
Member m = new Member();
m.setMid("admin");
m.setName("System Administrator");
m.setPassword(encoder.encode(adminPassword));
m.setRole("ADMIN");
return mRepo.save(m);
}
}
*** S4: ToDoAppSecurityConfig を作る [#v8890f94]
*** ToDoアプリのセキュリティ設定方針 [#kf10be3b]
3つのconfigure()でやっている設定
+ 静的コンテンツ /img/**, /js/**, /css/** は認証なしでア...
+ HTTPアクセスについて
-- /sign_in は認証不要 (ログインの前)
-- /sign_up/** は認証不要 (新規アカウントの作成)
-- /admin/** はADMINロールが必要
-- それ以外はすべて認証が必要
+ ログインの設定
-- フォームログインで行う
-- ログイン画面は /sign_in
-- ログイン画面のポスト先は, /authenticate
-- ログインフォームのユーザIDは,mid
-- ログインフォームのパスワードは,password
-- ログイン成功したら,/sign_in_success に着地する
-- ログイン失敗したら,/sign_in?error に飛ぶ
+ ログアウト設定
-- ログアウトは /sign_out にPOST
-- ログアウト成功したら,/sign_in?sign_out に着地
-- クッキー,セッションともに消去
-- ログアウトは認証なしでアクセス可能
+ 認証方法の実装の指定
-- MemberServiceに委任
-- パスワードエンコーダは,ファイル内で生成したBeanを使う
*** 具体的な設定クラス [#cd0a8033]
- 新規にconfigurationという名前でフォルダを作成
- configuration/ToDoAppSecurityConfig.java
import org.springframework.beans.factory.annotation.Auto...
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configurat...
import org.springframework.security.config.annotation.au...
import org.springframework.security.config.annotation.we...
import org.springframework.security.config.annotation.we...
import org.springframework.security.config.annotation.we...
import org.springframework.security.config.annotation.we...
import org.springframework.security.crypto.bcrypt.BCrypt...
import jp.ac.kobe_u.cs.itspecialist.todoapp.service.Memb...
@Configuration
@EnableWebSecurity //(1) Spring Securityを使うための設定
public class ToDoAppSecurityConfig extends WebSecurityCo...
@Autowired
MemberService mService;
@Value("${admin.pass}")
String adminPass;
/**
* 静的リソースの認可設定
*/
@Override
public void configure(WebSecurity web) throws Except...
web.ignoring().antMatchers("/img/**", "/css/**",...
}
/**
* HTTPリクエストに対する認可,および,ログイン・ロ...
*/
@Override
protected void configure(HttpSecurity http) throws E...
//認可の設定
http.authorizeRequests()
.antMatchers("/sign_in").permitAll() //...
.antMatchers("/sign_up/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN") //...
.anyRequest().authenticated(); //...
//ログインの設定
http.formLogin() //...
.loginPage("/sign_in") // 2...
.loginProcessingUrl("/authenticate") // 3. ...
.usernameParameter("mid") // 4. ...
.passwordParameter("password") // 5. ...
.defaultSuccessUrl("/sign_in_success", true) ...
.failureUrl("/sign_in?error"); // 7...
//ログアウトの設定
http.logout() //1.
.logoutUrl("/sign_out") //2...
.logoutSuccessUrl("/sign_in?sign_out") /...
.deleteCookies("JSESSIONID") //4. ...
.invalidateHttpSession(true) //5. ...
.permitAll(); //6. ...
}
/**
* 認証の方法を設定
*/
@Override
protected void configure(AuthenticationManagerBuilde...
auth.userDetailsService(mService).passwordEncode...
//ついでに管理者を登録しておく
mService.registerAdmin(adminPass);
}
/**
* アプリで共通のパスワード生成器を作る.
* @Beanをつけているので任意の箇所でAutowired可能に...
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
BCryptPasswordEncoder bcpe = new BCryptPasswordE...
return bcpe;
}
}
- resources/application.properties に追記
admin.pass=s3cret
** 認証・認可に伴う更新 [#if9191e0]
*** サインアップ機能のためのコントローラと画面 [#mcf06920]
- controller/SignUpController.java
package jp.ac.kobe_u.cs.itspecialist.todoapp.controller;
import org.springframework.beans.factory.annotation.Auto...
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMa...
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validat...
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttr...
import org.springframework.web.bind.annotation.PostMappi...
import jp.ac.kobe_u.cs.itspecialist.todoapp.dto.MemberFo...
import jp.ac.kobe_u.cs.itspecialist.todoapp.entity.Member;
import jp.ac.kobe_u.cs.itspecialist.todoapp.service.Memb...
@Controller
@RequestMapping("/sign_up")
public class SignUpController {
@Autowired
MemberService mService;
/**
* 一般ユーザの登録ページ HTTP-GET /sign_up
*
* @param model
* @return
*/
@GetMapping("")
String showSignUpForm(@ModelAttribute MemberForm for...
model.addAttribute("MemberForm", form);
return "signup";
}
/**
* ユーザ登録確認ページを表示 HTTP-POST /sign_up/check
*
* @param form
* @param model
* @return
*/
@PostMapping("/check")
String checkMemberForm(@Validated @ModelAttribute(na...
Model model) {
// 入力チェックに引っかかった場合、ユーザー登録...
if (bindingResult.hasErrors()) {
// GETリクエスト用のメソッドを呼び出して、ユ...
return showSignUpForm(form, model);
}
model.addAttribute("MemberForm", form);
return "signup_check";
}
/**
* ユーザ登録処理 -> 完了ページ HTTP-POST /sign_up/r...
*
* @param form
* @param model
* @return
*/
@PostMapping("")
String createMember(@ModelAttribute(name = "MemberFo...
Member m = mService.createMember(form);
model.addAttribute("MemberForm", m);
return "signup_complete";
}
}
- resources/templates/signup.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>サインアップ</title>
</head>
<body>
<h1>サインアップ</h1>
<h2>メンバー新規登録</h2>
<p> メンバーIDと氏名を入力して,「確認する」を押して...
<ul>
<li>メンバーIDには,アルファベット小文字,数字,...
<li>氏名は最大32文字.半角・全角が使用できます....
<li>パスワードは8文字以上でつけてください.</li>
</ul>
<form role="form" th:action="@{/sign_up/check}" th:o...
<table>
<tr>
<td><label>メンバーID: </label></td>
<td><input type="text" required th:field...
<span th:if="${#fields.hasErrors('mi...
</td>
</tr>
<tr>
<td><label>氏名: </label></td>
<td><input type="text" required th:field...
<span th:if="${#fields.hasErrors('na...
</td>
</tr>
<tr>
<td><label>パスワード: </label></td>
<td><input type="password" required th:f...
<span th:if="${#fields.hasErrors('pa...
</td>
</tr>
</table>
<p><input type="submit" value="確認する" /></p>
</form>
</body>
</html>
- resources/templates/signup_check.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登録確認</title>
</head>
<body>
<h1>登録確認</h1>
<p>
以下の情報でメンバーを登録します.よろしければ登...
</p>
<form role="form" th:action="@{/sign_up}" th:object=...
<p>
<label>メンバーID: </label>
[[*{mid}]]
</p>
<p>
<label>氏名: </label>
[[*{name}]]
</p>
<p>
<label>パスワード: </label>
[[*{password}]]
</p>
<p>
<input type="button" value="戻る" onclick="h...
<input type="submit" value="登録する" />
</p>
<input type="hidden" th:field="*{mid}" />
<input type="hidden" th:field="*{name}" />
<input type="hidden" th:field="*{password}" />
</form>
</body>
</html>
- resources/templates/signup_complete.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登録完了</title>
</head>
<body>
<h1>登録完了</h1>
<p>
メンバー登録を完了しました.
</p>
<div th:object="${MemberForm}">
<p>
<label>メンバーID: </label>
[[*{mid}]]
</p>
<p>
<label>氏名: </label>
[[*{name}]]
</p>
<p>
<a th:href=@{/}>初めに戻る</a>
</p>
</div>
</body>
</html>
*** ToDpControllerの改訂方針 [#ica16b6d]
- ログインしていないと,/sign_in に飛ばされる
- ログインが成功すると,/sign_in_success にリダイレクトさ...
-- MEMBER: /{uid}/todos にリダイレクト
-- ADMIN: /admin/register にリダイレクト
- 各画面のログアウトも変更
-- /sign_out にPOSTリクエストしなければならない
*** ToDpControllerと関連画面の改訂 [#zaff7b09]
- controller/ToDoController.java
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Auto...
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.Securit...
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validat...
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttr...
import org.springframework.web.bind.annotation.PathVaria...
import org.springframework.web.bind.annotation.PostMappi...
import org.springframework.web.bind.annotation.RequestPa...
import jp.ac.kobe_u.cs.itspecialist.todoapp.dto.LoginForm;
import jp.ac.kobe_u.cs.itspecialist.todoapp.dto.ToDoForm;
import jp.ac.kobe_u.cs.itspecialist.todoapp.dto.UserDeta...
import jp.ac.kobe_u.cs.itspecialist.todoapp.entity.Member;
import jp.ac.kobe_u.cs.itspecialist.todoapp.entity.ToDo;
import jp.ac.kobe_u.cs.itspecialist.todoapp.exception.To...
import jp.ac.kobe_u.cs.itspecialist.todoapp.service.Memb...
import jp.ac.kobe_u.cs.itspecialist.todoapp.service.ToDo...
@Controller
public class ToDoController {
@Autowired
MemberService mService;
@Autowired
ToDoService tService;
/**
* トップページ
*/
@GetMapping("/sign_in")
String showIndex(@RequestParam Map<String, String> p...
//パラメータ処理.ログアウト時は?logout, 認証失...
if (params.containsKey("sign_out")) {
model.addAttribute("message", "サインアウトしました");
} else if (params.containsKey("error")) {
model.addAttribute("message", "サインインに失敗しまし...
}
//model.addAttribute("loginForm", loginForm);
return "signin";
}
/**
* ログイン処理.midの存在確認をして,ユーザページに...
*/
@GetMapping("/sign_in_success")
String login() {
Authentication auth = SecurityContextHolder.getC...
Member m = ((UserDetailsImpl) auth.getPrincipal...
if (m.getRole().equals("ADMIN")) {
return "redirect:/admin/register";
}
return "redirect:/" + m.getMid() + "/todos";
}
/**
* ユーザのToDoリストのページ
*/
@GetMapping("/{mid}/todos")
String showToDoList(@PathVariable String mid, @Model...
checkIdentity(mid);
Member m = mService.getMember(mid);
model.addAttribute("member", m);
model.addAttribute("ToDoForm", form);
List<ToDo> todos = tService.getToDoList(mid);
model.addAttribute("todos", todos);
List<ToDo> dones = tService.getDoneList(mid);
model.addAttribute("dones", dones);
return "list";
}
/**
* 全員のToDoリストのページ
*/
@GetMapping("/{mid}/todos/all")
String showAllToDoList(@PathVariable String mid, Mod...
checkIdentity(mid);
Member m = mService.getMember(mid);
model.addAttribute("member", m);
List<ToDo> todos = tService.getToDoList();
model.addAttribute("todos", todos);
List<ToDo> dones = tService.getDoneList();
model.addAttribute("dones", dones);
return "alllist";
}
/**
* ToDoの作成.作成処理後,ユーザページへリダイレクト
*/
@PostMapping("/{mid}/todos")
String createToDo(@PathVariable String mid, @Validat...
BindingResult bindingResult, Model model) {
checkIdentity(mid);
if (bindingResult.hasErrors()) {
return showToDoList(mid, form, model);
}
tService.createToDo(mid, form);
return "redirect:/" + mid + "/todos";
}
/**
* ToDoの完了.完了処理後,ユーザページへリダイレクト
*/
@GetMapping("/{mid}/todos/{seq}/done")
String doneToDo(@PathVariable String mid, @PathVaria...
checkIdentity(mid);
tService.done(mid, seq);
return "redirect:/" + mid + "/todos";
}
/**
* 認可チェック.与えられたmidがログイン中のmidに等...
*/
private void checkIdentity(String mid) {
Authentication auth = SecurityContextHolder.getC...
Member m = ((UserDetailsImpl) auth.getPrincipal...
if (!mid.equals(m.getMid())) {
throw new ToDoAppException(ToDoAppException....
m.getMid() + ": not authorized to access res...
}
}
}
- resources/templates/signin.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ToDoアプリケーション</title>
</head>
<body>
<h1>ToDoアプリケーション</h1>
<img th:src=@{/img/todo_people.png} width="640px">
<p>
サインインしてください.初めての方は,<a th:href...
</p>
<form role="form" th:action="@{/authenticate}" th:ob...
<p>
<label>メンバーID: </label>
<input type="text" th:field="*{mid}" />
<span th:if="${#fields.hasErrors('mid')}" th...
</p>
<p>
<label>パスワード: </label>
<input type="password" th:field="*{password}...
<span th:if="${#fields.hasErrors('password')...
</p>
<p>
<input type="submit" value="サインイン" />
</p>
<p>
<span th:if="${message}" th:text="${message}...
</p>
</form>
</body>
</html>
- resources/templates/list.html
-- サインアウトボタンはPOST
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ToDoList</title>
</head>
<body>
<h1> ようこそ [[${member.name}]] さん!</h1>
<p>
<a th:href="@{/{mid}/todos/all(mid=${member.mid}...
<form th:action="@{/sign_out}" method="post">
<input type="submit" value="サインアウト" />
</form>
</p>
<h2>ToDo</h2>
<table border="1">
<tr>
<th>#</th>
<th>タイトル</th>
<th>作成日時</th>
<th>コマンド</th>
</tr>
<tr th:each="todo: ${todos}">
<td>[[${todo.seq}]]</td>
<td>[[${todo.title}]]</td>
<td>[[${todo.createdAt}]]</td>
<td>
<a th:href="@{/{mid}/todos/{seq}/done(mi...
</td>
</tr>
<tr>
<td>
*
</td>
<td colspan="3">
<form role="form" th:action="@{/{mid}/to...
<input type="text" required th:field...
<input type="submit" value="新規作成...
<div th:if="${#fields.hasErrors('tit...
</form>
</td>
</tr>
</table>
<h2>Done</h2>
<table border="1">
<tr>
<th>#</th>
<th>タイトル</th>
<th>作成日時</th>
<th>完了日時</th>
</tr>
<tr th:each="done: ${dones}">
<td>[[${done.seq}]]</td>
<td>[[${done.title}]]</td>
<td>[[${done.createdAt}]]</td>
<td>[[${done.doneAt}]]</td>
</tr>
</table>
</body>
</html>
- resources/templates/alllist.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ToDoList</title>
</head>
<body>
<h1> みんなのToDoリスト</h1>
<p>
<a th:href="@{/{mid}/todos(mid=${member.mid})}">...
<form th:action="@{/sign_out}" method="post">
<input type="submit" value="サインアウト" />
</form>
</p>
<h2>ToDo</h2>
<table border="1">
<tr>
<th>#</th>
<th>タイトル</th>
<th>作成者</th>
<th>作成日時</th>
</tr>
<tr th:each="todo: ${todos}">
<td>[[${todo.seq}]]</td>
<td>[[${todo.title}]]</td>
<td>[[${todo.mid}]]</td>
<td>[[${todo.createdAt}]]</td>
</tr>
</table>
<h2>Done</h2>
<table border="1">
<tr>
<th>#</th>
<th>タイトル</th>
<th>作成者</th>
<th>完了日時</th>
</tr>
<tr th:each="done: ${dones}">
<td>[[${done.seq}]]</td>
<td>[[${done.title}]]</td>
<td>[[${done.mid}]]</td>
<td>[[${done.doneAt}]]</td>
</tr>
</table>
</body>
</html>
** 管理者画面 /admin/register [#p3793983]
同様に変更してみてください!
** 参考 [#z7b7ba69]
- [[SpringBoot/Form認証]]
終了行:
[[第3回]]
* 認証・認可 [#ldb0bb1e]
- テキストはパワーポイントを参照のこと
- 参考 [[SpringBoot/認証・認可]]
** ToDo管理アプリに認証・認可を実装する [#j079ac41]
*** やりたいこと [#e753a6d2]
+ ToDo管理に''メンバーごとのパスワード認証''をかけたい
+ これまで管理者が行っていたメンバー登録は,メンバー自身...
-- サインアップ: メンバーが自分自身でサービスに登録する...
-- サインイン: メンバーが登録した自身の認証情報でサービ...
+ 認可の権限は2種類
-- ''メンバー (MEMBER):'' ToDo画面 (/{mid}/todos) で自分...
--- 自身のTodDoを閲覧,登録,完了できる.全員のToDo (/{mi...
--- ただし,''他人のToDo画面は見ることができない''
-- ''管理者 (ADMIN):'' 管理者画面(/admin/register)でユ...
-- 登録済ユーザの一覧,新規登録,削除ができる
** 認証・認可部の実装 [#p48d9fed]
*** 方針 [#va9742c6]
- S1: Member, MemberFormにパスワードトロールを追加
- S2: UserDetailsの実装クラスUserDetailsImplを作る
- S3: MemverService にUserDetailsServiceを継承させ,loadU...
- S4: ToDoAppSecurityConfig を作る
*** 準備 [#zf1b5ff4]
- build.gradle の dependenciesの中に,下記を追記
implementation 'org.springframework.boot:spring-boot-sta...
implementation 'org.thymeleaf.extras:thymeleaf-extras-sp...
*** S1: Member, MemberFormにパスワードトロールを追加 [#rf...
- entity/Member.java
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Member {
@Id
String mid; //メンバーID
String name; //名前
String password; //パスワード(暗号化済)
String role; //ロール
}
- dto/MemberForm.java
@Data
public class MemberForm {
@Pattern(regexp ="[a-z0-9_\\-]{4,16}")
String mid; //メンバーID.英小文字,数字,ハイフン,...
@NotBlank
@Size(min = 1, max = 32)
String name; //名前.最大32文字
@NotBlank
@Size(min = 8)
String password; //パスワード
String role = "MEMBER"; //ロール.デフォルトは"MEMBER"
public Member toEntity() {
Member m = new Member(mid, name, password, role);
return m;
}
}
解説
- パスワード,ロールともに文字列型で定義
- ロールはデフォルトでMEMBERとしている
*** S2: UserDetailsの実装クラスUserDetailsImplを作る [#h2...
- dto/UserDetailsImpl.java
package jp.ac.kobe_u.cs.itspecialist.todoapp.dto;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.Simpl...
import org.springframework.security.core.userdetails.Use...
import jp.ac.kobe_u.cs.itspecialist.todoapp.entity.Member;
/**
* 認証に必要なUserDetailsの実装クラス.Memberをラップする
*/
public class UserDetailsImpl implements UserDetails {
Member member;
Collection<GrantedAuthority> authorities = new Array...
/**
* コンストラクタ
* @param member
*/
public UserDetailsImpl(Member member) {
this.member=member;
//メンバーのロールから権限を生成して追加
this.authorities.add(new SimpleGrantedAuthority(...
}
public Member getMember() {
return member;
}
@Override
public Collection<? extends GrantedAuthority> getAut...
return authorities;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getMid();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
*** S3: MemverService にUserDetailsServiceを継承させ,loa...
- service/MemberService.java
package jp.ac.kobe_u.cs.itspecialist.todoapp.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Auto...
import org.springframework.security.core.userdetails.Use...
import org.springframework.security.core.userdetails.Use...
import org.springframework.security.core.userdetails.Use...
import org.springframework.security.crypto.bcrypt.BCrypt...
import org.springframework.stereotype.Service;
import jp.ac.kobe_u.cs.itspecialist.todoapp.dto.MemberFo...
import jp.ac.kobe_u.cs.itspecialist.todoapp.dto.UserDeta...
import jp.ac.kobe_u.cs.itspecialist.todoapp.entity.Member;
import jp.ac.kobe_u.cs.itspecialist.todoapp.exception.To...
import jp.ac.kobe_u.cs.itspecialist.todoapp.repository.M...
/**
* メンバーのCRUDを行うサービス
*/
@Service
public class MemberService implements UserDetailsService{
@Autowired
MemberRepository mRepo;
@Autowired
BCryptPasswordEncoder encoder;
/**
* メンバーを作成する (C)
* @param form
* @return
*/
public Member createMember(MemberForm form) {
//IDの重複チェック
String mid = form.getMid();
if (mRepo.existsById(mid)) {
throw new ToDoAppException(ToDoAppException....
}
Member m = form.toEntity();
m.setPassword(encoder.encode(m.getPassword())); ...
return mRepo.save(m);
}
/**
* メンバーを取得する (R)
* @param mid
* @return
*/
public Member getMember(String mid) {
Member m = mRepo.findById(mid).orElseThrow(
() -> new ToDoAppException(ToDoAppExcept...
return m;
}
/**
* 全メンバーを取得する (R)
* @return
*/
public List<Member> getAllMembers() {
return mRepo.findAll();
}
public boolean existById(String mid) {
return mRepo.existsById(mid);
}
/**
* メンバーを削除する (D)
*/
public void deleteMember(String mid) {
Member m = getMember(mid);
mRepo.delete(m);
}
/* ------------------ ここから追加分 --------------...
/**
* Spring Securityのメソッド.ユーザIDを与えて,ユー...
*/
@Override
public UserDetails loadUserByUsername(String mid) th...
Member m = mRepo.findById(mid).orElseThrow(
() -> new UsernameNotFoundException(mid + ":...
);
return new UserDetailsImpl(m);
}
/**
* 管理者を登録するサービス.
*/
public Member registerAdmin(String adminPassword) {
Member m = new Member();
m.setMid("admin");
m.setName("System Administrator");
m.setPassword(encoder.encode(adminPassword));
m.setRole("ADMIN");
return mRepo.save(m);
}
}
*** S4: ToDoAppSecurityConfig を作る [#v8890f94]
*** ToDoアプリのセキュリティ設定方針 [#kf10be3b]
3つのconfigure()でやっている設定
+ 静的コンテンツ /img/**, /js/**, /css/** は認証なしでア...
+ HTTPアクセスについて
-- /sign_in は認証不要 (ログインの前)
-- /sign_up/** は認証不要 (新規アカウントの作成)
-- /admin/** はADMINロールが必要
-- それ以外はすべて認証が必要
+ ログインの設定
-- フォームログインで行う
-- ログイン画面は /sign_in
-- ログイン画面のポスト先は, /authenticate
-- ログインフォームのユーザIDは,mid
-- ログインフォームのパスワードは,password
-- ログイン成功したら,/sign_in_success に着地する
-- ログイン失敗したら,/sign_in?error に飛ぶ
+ ログアウト設定
-- ログアウトは /sign_out にPOST
-- ログアウト成功したら,/sign_in?sign_out に着地
-- クッキー,セッションともに消去
-- ログアウトは認証なしでアクセス可能
+ 認証方法の実装の指定
-- MemberServiceに委任
-- パスワードエンコーダは,ファイル内で生成したBeanを使う
*** 具体的な設定クラス [#cd0a8033]
- 新規にconfigurationという名前でフォルダを作成
- configuration/ToDoAppSecurityConfig.java
import org.springframework.beans.factory.annotation.Auto...
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configurat...
import org.springframework.security.config.annotation.au...
import org.springframework.security.config.annotation.we...
import org.springframework.security.config.annotation.we...
import org.springframework.security.config.annotation.we...
import org.springframework.security.config.annotation.we...
import org.springframework.security.crypto.bcrypt.BCrypt...
import jp.ac.kobe_u.cs.itspecialist.todoapp.service.Memb...
@Configuration
@EnableWebSecurity //(1) Spring Securityを使うための設定
public class ToDoAppSecurityConfig extends WebSecurityCo...
@Autowired
MemberService mService;
@Value("${admin.pass}")
String adminPass;
/**
* 静的リソースの認可設定
*/
@Override
public void configure(WebSecurity web) throws Except...
web.ignoring().antMatchers("/img/**", "/css/**",...
}
/**
* HTTPリクエストに対する認可,および,ログイン・ロ...
*/
@Override
protected void configure(HttpSecurity http) throws E...
//認可の設定
http.authorizeRequests()
.antMatchers("/sign_in").permitAll() //...
.antMatchers("/sign_up/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN") //...
.anyRequest().authenticated(); //...
//ログインの設定
http.formLogin() //...
.loginPage("/sign_in") // 2...
.loginProcessingUrl("/authenticate") // 3. ...
.usernameParameter("mid") // 4. ...
.passwordParameter("password") // 5. ...
.defaultSuccessUrl("/sign_in_success", true) ...
.failureUrl("/sign_in?error"); // 7...
//ログアウトの設定
http.logout() //1.
.logoutUrl("/sign_out") //2...
.logoutSuccessUrl("/sign_in?sign_out") /...
.deleteCookies("JSESSIONID") //4. ...
.invalidateHttpSession(true) //5. ...
.permitAll(); //6. ...
}
/**
* 認証の方法を設定
*/
@Override
protected void configure(AuthenticationManagerBuilde...
auth.userDetailsService(mService).passwordEncode...
//ついでに管理者を登録しておく
mService.registerAdmin(adminPass);
}
/**
* アプリで共通のパスワード生成器を作る.
* @Beanをつけているので任意の箇所でAutowired可能に...
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
BCryptPasswordEncoder bcpe = new BCryptPasswordE...
return bcpe;
}
}
- resources/application.properties に追記
admin.pass=s3cret
** 認証・認可に伴う更新 [#if9191e0]
*** サインアップ機能のためのコントローラと画面 [#mcf06920]
- controller/SignUpController.java
package jp.ac.kobe_u.cs.itspecialist.todoapp.controller;
import org.springframework.beans.factory.annotation.Auto...
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMa...
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validat...
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttr...
import org.springframework.web.bind.annotation.PostMappi...
import jp.ac.kobe_u.cs.itspecialist.todoapp.dto.MemberFo...
import jp.ac.kobe_u.cs.itspecialist.todoapp.entity.Member;
import jp.ac.kobe_u.cs.itspecialist.todoapp.service.Memb...
@Controller
@RequestMapping("/sign_up")
public class SignUpController {
@Autowired
MemberService mService;
/**
* 一般ユーザの登録ページ HTTP-GET /sign_up
*
* @param model
* @return
*/
@GetMapping("")
String showSignUpForm(@ModelAttribute MemberForm for...
model.addAttribute("MemberForm", form);
return "signup";
}
/**
* ユーザ登録確認ページを表示 HTTP-POST /sign_up/check
*
* @param form
* @param model
* @return
*/
@PostMapping("/check")
String checkMemberForm(@Validated @ModelAttribute(na...
Model model) {
// 入力チェックに引っかかった場合、ユーザー登録...
if (bindingResult.hasErrors()) {
// GETリクエスト用のメソッドを呼び出して、ユ...
return showSignUpForm(form, model);
}
model.addAttribute("MemberForm", form);
return "signup_check";
}
/**
* ユーザ登録処理 -> 完了ページ HTTP-POST /sign_up/r...
*
* @param form
* @param model
* @return
*/
@PostMapping("")
String createMember(@ModelAttribute(name = "MemberFo...
Member m = mService.createMember(form);
model.addAttribute("MemberForm", m);
return "signup_complete";
}
}
- resources/templates/signup.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>サインアップ</title>
</head>
<body>
<h1>サインアップ</h1>
<h2>メンバー新規登録</h2>
<p> メンバーIDと氏名を入力して,「確認する」を押して...
<ul>
<li>メンバーIDには,アルファベット小文字,数字,...
<li>氏名は最大32文字.半角・全角が使用できます....
<li>パスワードは8文字以上でつけてください.</li>
</ul>
<form role="form" th:action="@{/sign_up/check}" th:o...
<table>
<tr>
<td><label>メンバーID: </label></td>
<td><input type="text" required th:field...
<span th:if="${#fields.hasErrors('mi...
</td>
</tr>
<tr>
<td><label>氏名: </label></td>
<td><input type="text" required th:field...
<span th:if="${#fields.hasErrors('na...
</td>
</tr>
<tr>
<td><label>パスワード: </label></td>
<td><input type="password" required th:f...
<span th:if="${#fields.hasErrors('pa...
</td>
</tr>
</table>
<p><input type="submit" value="確認する" /></p>
</form>
</body>
</html>
- resources/templates/signup_check.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登録確認</title>
</head>
<body>
<h1>登録確認</h1>
<p>
以下の情報でメンバーを登録します.よろしければ登...
</p>
<form role="form" th:action="@{/sign_up}" th:object=...
<p>
<label>メンバーID: </label>
[[*{mid}]]
</p>
<p>
<label>氏名: </label>
[[*{name}]]
</p>
<p>
<label>パスワード: </label>
[[*{password}]]
</p>
<p>
<input type="button" value="戻る" onclick="h...
<input type="submit" value="登録する" />
</p>
<input type="hidden" th:field="*{mid}" />
<input type="hidden" th:field="*{name}" />
<input type="hidden" th:field="*{password}" />
</form>
</body>
</html>
- resources/templates/signup_complete.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登録完了</title>
</head>
<body>
<h1>登録完了</h1>
<p>
メンバー登録を完了しました.
</p>
<div th:object="${MemberForm}">
<p>
<label>メンバーID: </label>
[[*{mid}]]
</p>
<p>
<label>氏名: </label>
[[*{name}]]
</p>
<p>
<a th:href=@{/}>初めに戻る</a>
</p>
</div>
</body>
</html>
*** ToDpControllerの改訂方針 [#ica16b6d]
- ログインしていないと,/sign_in に飛ばされる
- ログインが成功すると,/sign_in_success にリダイレクトさ...
-- MEMBER: /{uid}/todos にリダイレクト
-- ADMIN: /admin/register にリダイレクト
- 各画面のログアウトも変更
-- /sign_out にPOSTリクエストしなければならない
*** ToDpControllerと関連画面の改訂 [#zaff7b09]
- controller/ToDoController.java
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Auto...
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.Securit...
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validat...
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttr...
import org.springframework.web.bind.annotation.PathVaria...
import org.springframework.web.bind.annotation.PostMappi...
import org.springframework.web.bind.annotation.RequestPa...
import jp.ac.kobe_u.cs.itspecialist.todoapp.dto.LoginForm;
import jp.ac.kobe_u.cs.itspecialist.todoapp.dto.ToDoForm;
import jp.ac.kobe_u.cs.itspecialist.todoapp.dto.UserDeta...
import jp.ac.kobe_u.cs.itspecialist.todoapp.entity.Member;
import jp.ac.kobe_u.cs.itspecialist.todoapp.entity.ToDo;
import jp.ac.kobe_u.cs.itspecialist.todoapp.exception.To...
import jp.ac.kobe_u.cs.itspecialist.todoapp.service.Memb...
import jp.ac.kobe_u.cs.itspecialist.todoapp.service.ToDo...
@Controller
public class ToDoController {
@Autowired
MemberService mService;
@Autowired
ToDoService tService;
/**
* トップページ
*/
@GetMapping("/sign_in")
String showIndex(@RequestParam Map<String, String> p...
//パラメータ処理.ログアウト時は?logout, 認証失...
if (params.containsKey("sign_out")) {
model.addAttribute("message", "サインアウトしました");
} else if (params.containsKey("error")) {
model.addAttribute("message", "サインインに失敗しまし...
}
//model.addAttribute("loginForm", loginForm);
return "signin";
}
/**
* ログイン処理.midの存在確認をして,ユーザページに...
*/
@GetMapping("/sign_in_success")
String login() {
Authentication auth = SecurityContextHolder.getC...
Member m = ((UserDetailsImpl) auth.getPrincipal...
if (m.getRole().equals("ADMIN")) {
return "redirect:/admin/register";
}
return "redirect:/" + m.getMid() + "/todos";
}
/**
* ユーザのToDoリストのページ
*/
@GetMapping("/{mid}/todos")
String showToDoList(@PathVariable String mid, @Model...
checkIdentity(mid);
Member m = mService.getMember(mid);
model.addAttribute("member", m);
model.addAttribute("ToDoForm", form);
List<ToDo> todos = tService.getToDoList(mid);
model.addAttribute("todos", todos);
List<ToDo> dones = tService.getDoneList(mid);
model.addAttribute("dones", dones);
return "list";
}
/**
* 全員のToDoリストのページ
*/
@GetMapping("/{mid}/todos/all")
String showAllToDoList(@PathVariable String mid, Mod...
checkIdentity(mid);
Member m = mService.getMember(mid);
model.addAttribute("member", m);
List<ToDo> todos = tService.getToDoList();
model.addAttribute("todos", todos);
List<ToDo> dones = tService.getDoneList();
model.addAttribute("dones", dones);
return "alllist";
}
/**
* ToDoの作成.作成処理後,ユーザページへリダイレクト
*/
@PostMapping("/{mid}/todos")
String createToDo(@PathVariable String mid, @Validat...
BindingResult bindingResult, Model model) {
checkIdentity(mid);
if (bindingResult.hasErrors()) {
return showToDoList(mid, form, model);
}
tService.createToDo(mid, form);
return "redirect:/" + mid + "/todos";
}
/**
* ToDoの完了.完了処理後,ユーザページへリダイレクト
*/
@GetMapping("/{mid}/todos/{seq}/done")
String doneToDo(@PathVariable String mid, @PathVaria...
checkIdentity(mid);
tService.done(mid, seq);
return "redirect:/" + mid + "/todos";
}
/**
* 認可チェック.与えられたmidがログイン中のmidに等...
*/
private void checkIdentity(String mid) {
Authentication auth = SecurityContextHolder.getC...
Member m = ((UserDetailsImpl) auth.getPrincipal...
if (!mid.equals(m.getMid())) {
throw new ToDoAppException(ToDoAppException....
m.getMid() + ": not authorized to access res...
}
}
}
- resources/templates/signin.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ToDoアプリケーション</title>
</head>
<body>
<h1>ToDoアプリケーション</h1>
<img th:src=@{/img/todo_people.png} width="640px">
<p>
サインインしてください.初めての方は,<a th:href...
</p>
<form role="form" th:action="@{/authenticate}" th:ob...
<p>
<label>メンバーID: </label>
<input type="text" th:field="*{mid}" />
<span th:if="${#fields.hasErrors('mid')}" th...
</p>
<p>
<label>パスワード: </label>
<input type="password" th:field="*{password}...
<span th:if="${#fields.hasErrors('password')...
</p>
<p>
<input type="submit" value="サインイン" />
</p>
<p>
<span th:if="${message}" th:text="${message}...
</p>
</form>
</body>
</html>
- resources/templates/list.html
-- サインアウトボタンはPOST
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ToDoList</title>
</head>
<body>
<h1> ようこそ [[${member.name}]] さん!</h1>
<p>
<a th:href="@{/{mid}/todos/all(mid=${member.mid}...
<form th:action="@{/sign_out}" method="post">
<input type="submit" value="サインアウト" />
</form>
</p>
<h2>ToDo</h2>
<table border="1">
<tr>
<th>#</th>
<th>タイトル</th>
<th>作成日時</th>
<th>コマンド</th>
</tr>
<tr th:each="todo: ${todos}">
<td>[[${todo.seq}]]</td>
<td>[[${todo.title}]]</td>
<td>[[${todo.createdAt}]]</td>
<td>
<a th:href="@{/{mid}/todos/{seq}/done(mi...
</td>
</tr>
<tr>
<td>
*
</td>
<td colspan="3">
<form role="form" th:action="@{/{mid}/to...
<input type="text" required th:field...
<input type="submit" value="新規作成...
<div th:if="${#fields.hasErrors('tit...
</form>
</td>
</tr>
</table>
<h2>Done</h2>
<table border="1">
<tr>
<th>#</th>
<th>タイトル</th>
<th>作成日時</th>
<th>完了日時</th>
</tr>
<tr th:each="done: ${dones}">
<td>[[${done.seq}]]</td>
<td>[[${done.title}]]</td>
<td>[[${done.createdAt}]]</td>
<td>[[${done.doneAt}]]</td>
</tr>
</table>
</body>
</html>
- resources/templates/alllist.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ToDoList</title>
</head>
<body>
<h1> みんなのToDoリスト</h1>
<p>
<a th:href="@{/{mid}/todos(mid=${member.mid})}">...
<form th:action="@{/sign_out}" method="post">
<input type="submit" value="サインアウト" />
</form>
</p>
<h2>ToDo</h2>
<table border="1">
<tr>
<th>#</th>
<th>タイトル</th>
<th>作成者</th>
<th>作成日時</th>
</tr>
<tr th:each="todo: ${todos}">
<td>[[${todo.seq}]]</td>
<td>[[${todo.title}]]</td>
<td>[[${todo.mid}]]</td>
<td>[[${todo.createdAt}]]</td>
</tr>
</table>
<h2>Done</h2>
<table border="1">
<tr>
<th>#</th>
<th>タイトル</th>
<th>作成者</th>
<th>完了日時</th>
</tr>
<tr th:each="done: ${dones}">
<td>[[${done.seq}]]</td>
<td>[[${done.title}]]</td>
<td>[[${done.mid}]]</td>
<td>[[${done.doneAt}]]</td>
</tr>
</table>
</body>
</html>
** 管理者画面 /admin/register [#p3793983]
同様に変更してみてください!
** 参考 [#z7b7ba69]
- [[SpringBoot/Form認証]]
ページ名: