Chapter 21

ログイン(Spring Security)、トップページ

kazpgm
kazpgm
2022.01.02に更新

■ログイン(Spring Security)

・認証・認可情報の管理

認証はメールアドレスとパスワードを使用する。
認可はロール(USERおよびADMIN)で制御する。
ロールは、1ユーザーに2つ設定することも全く設定しないこともできる。
認証・認可情報はデータベースのuserテーブルに保存する。

・userテーブルエンティティ

メールアドレスは全ユーザーで一意となるようにユニークキーを設定。→1
パスワードはSpring Securityのパスワードエンコーダを使って暗号化した状態で保存する。→2
ロールはカンマ区切りの文字列で保存する。→3
有効フラグを設定する。→4
有効フラグ名を戻すメソッドを作成する。→5

com.kaz01u.demo.entity.User.java Userクラス
package com.kaz01u.demo.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import com.kaz01u.demo.utils.Elements;
import com.kaz01u.demo.utils.Functions;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Table(name = "user")
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", length = 60, nullable = false)
    private String name;

    @Column(name = "email", length = 120, nullable = false, unique = true) //←1
    private String email;

    @Column(name = "password", length = 120, nullable = false) //←2
    private String password;

    @Column(name = "roles", length = 120) //←3
    protected String roles;

    @Column(name = "enable_flag", nullable = false) //←4
    private Boolean enableFlag;

    /**
     * getEnableFlagNameメソッド
     * enable_flagの名前を戻す
     * 
     * @return enable_flagの名前
     */
    public String getEnableFlagName() {
        return Functions.nvl((Elements.ELEMENTS.get("ENABLE_FLG")).
	                            get(Boolean.toString(enableFlag)));//←5
    }
}

・userテーブルDDL

■demo-spring-kaz01u\sql\create_dbkz1_DDL.sql DDL
create database dbkz1 character set utf8;
USE `dbkz1`;

DROP TABLE IF EXISTS dbkz1.`user`;
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT       COMMENT 'ユーザーID',
  `email` varchar(120) NOT NULL                 COMMENT 'メールアドレス',
  `enable_flag` bit(1) NOT NULL DEFAULT 1       COMMENT '1:有効 0:無効',
  `name` varchar(60) NOT NULL                   COMMENT 'ユーザー名',
  `password` varchar(120) NOT NULL              COMMENT 'パスワード',
  `roles` varchar(120) DEFAULT NULL             COMMENT 'ロール',
  PRIMARY KEY (`id`),
  UNIQUE KEY (`email`) /* ←4 */
) ENGINE=InnoDB;

・リポジトリ

メールアドレスで検索するメソッドを追加する。→6
idで検索するメソッドを追加する。

com.kaz01u.demo.repository.UserRepository.java userリポジトリ
package com.kaz01u.demo.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.kaz01u.demo.entity.User;
public interface UserRepository extends JpaRepository<User, Long>, 
                                            RepositoryCustom {
    Optional<User> findByEmail(String email);))); //←6
    Optional<User> findById(Long id);
}

・セキュリティコンフィグレーション

com.kaz01u.demo.WebSecurityConfig.java

WebSecurityConfigurerAdapter抽象クラスを継承して作成する。→7
WebSecurityに、静的リソース(js、css、img)に対するアクセスはセキュリティを無視する設定を行っています。→8
HttpSecurityに
 ・authorizeRequests()でアクセス制御を行います。→9 
 ・permitAll()で全ユーザーアクセス許可。
 ・hasRole("USER")でユーザーロール(※1)がアクセス許可。
 ・hasRole("ADMIN")で管理者ロール(※1)がアクセス許可。
 ・それ以外は全て認証無しの場合アクセス不許可。
 ・formLogin()でログインフォームが使える。ログイン成功後のリダイレクト先を/(トップページ)に設定します。
 ・logout()でログアウトが使えるようにします。ログアウト成功後のリダイレクト先/(トップページ)に設定します。
 ・セッションの破棄とJSESSIONIDというクッキーを削除するようにします。
認証処理のURLへリクエストを送ると、Spring Securityが実際の認証処理を行います。
※1:Userテーブルのrolesのカラムに入っている"ROLE_USER"、"ROLE_ADMIN"の"ROLE_"部分を取り除いたもので認証される。

com.kaz01u.demo.WebSecurityConfig.java WebSecurityConfigクラス
package com.kaz01u.demo;

import org.springframework.context.annotation.Bean;
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.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //←7

    // アカウント登録時のパスワードエンコードで利用するためDI管理する。
    @Bean
    PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //←9
        http.authorizeRequests().mvcMatchers("/", "/login", "/members/index", "/error/**").permitAll() // 全ユーザーアクセス許可 //
            .mvcMatchers("/members/user/**", "/images/user/**", "/upload/**", "/elements").hasRole("USER") // ユーザーロール(※1)がアクセス許可
            .mvcMatchers("/members/admin/**", "/images/admin/**", "/upload/**", "/elements").hasRole("ADMIN") // 管理者ロール(※1)がアクセス許可
            .anyRequest().authenticated() // それ以外は全て認証無しの場合アクセス不許可
            .and().formLogin().loginProcessingUrl("/login") // 認証処理のパス
            .loginPage("/login").permitAll() // ログイン画面のURL
            .failureUrl("/login?error=true") // 認証失敗時のURL
            .defaultSuccessUrl("/members/index") // 認証成功時の遷移先
            .and().logout() // SpringSecurityは「/logout」パスにリクエストを送るだけでこれが動く
            .invalidateHttpSession(true) // ログアウトしたらセッションを無効にする
            .deleteCookies("JSESSIONID") // ログアウトしたら cookieの JSESSIONID を削除
            .logoutSuccessUrl("/login") // ログアウト完了時のパス
        ;
        //※1:Userテーブルのrolesのカラムに入っている"ROLE_USER"、"ROLE_ADMIN"の"ROLE_"部分を取り除いたもので認証される。

    }
}

・認証・認可に使用するユーザー情報の取得

com.kaz01u.demo.auth.SimpleLoginUser.java

Spring Securityが認証・認可に使用するユーザー情報を、リファレンス実装であるorg.springframework.security.core.userdetails.Userを継承して作成する。
コンストラクタはデータベースから検索したUserエンティティのインスタンスを受け取り、そのインスタンスのメールアドレス、パスワード、有効フラグ、ロールを使ってスーパークラスのコンストラクタを呼び出します。→10

com.kaz01u.demo.auth.SimpleLoginUser.java ユーザー認証情報
package com.kaz01u.demo.auth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import com.kaz01u.demo.entity.User;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * ユーザー認証情報
 */
@Slf4j
public class SimpleLoginUser extends org.springframework.security.core.userdetails.User {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    /**
     * DBより検索したuserエンティティ
     * 認証・認可以外でアプリケーションから利用される
     */
    private User user;

    /**
     * データベースより検索したuserエンティティよりSpring Securityで使用するユーザー認証情報の
     * インスタンスを生成する
     *
     * @param user userエンティティ
     */
    public SimpleLoginUser(User user) { //←10
        super(user.getEmail(), user.getPassword(), user.getEnableFlag(), true, true,
                true, convertGrantedAuthorities(user.getRoles()));
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    /**
     * カンマ区切りのロールをSimpleGrantedAuthorityのコレクションへ変換する
     *
     * @param roles カンマ区切りのロール
     * @return SimpleGrantedAuthorityのコレクション
     */
    static Set<GrantedAuthority> convertGrantedAuthorities(String roles) {
        if (roles == null || roles.isEmpty()) {
            return Collections.emptySet();
        }
        Set<GrantedAuthority> authorities = Stream.of(roles.split(","))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toSet());
        return authorities;
    }
}

com.kaz01u.demo.auth.SimpleUserDetailsService.java

UserDetailsServiceインターフェースを実装したサービスクラスを作成し、userテーブルをメールアドレスで検索し、Spring Securityが認証・認可に使用するユーザー情報(SimpleLoginUser)を取得(生成)する。→11

com.kaz01u.demo.auth.SimpleUserDetailsService.java ユーザー認証情報取得
package com.kaz01u.demo.auth;
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 org.springframework.transaction.annotation.Transactional;
import com.kaz01u.demo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
@RequiredArgsConstructor
public class SimpleUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    /**
     * メールアドレスで検索したユーザーのuserエンティティを
     * SimpleLoginUserクラスのインスタンスへ変換する
     *
     * @param username 検索するユーザーのメールアドレス
     * @return メールアドレスで検索できたユーザーのユーザー情報
     * @throws UsernameNotFoundException メールアドレスでユーザーが検索できなかった場合にスローする。
     */
    @Transactional(readOnly = true)
    @Override
    public UserDetails loadUserByUsername(String username) 
                                      throws UsernameNotFoundException { //←11
        assert(username != null);
        log.debug("loadUserByUsername(email):[{}]", username);
        return userRepository.findByEmail(username)
                .map(SimpleLoginUser::new)
                .orElseThrow(() -> new UsernameNotFoundException("User not found by email:[" + username + "]"));
    }
}

・トップページ

コントローラ(com.kaz01u.demo.controller.IndexController.java)

管理者メニュー画面表示("/members/admin/index")→12
ユーザーメニュー画面表示("/members/user/index")→13
管理者又はユーザーメニュー画面表示("/members/index")→14
 ・ログインユーザーのロールにより、管理者メニュー、ユーザーメニュー、その他のメニューを切り分けている。
 補足:WebSecurityConfigクラスに認証成功時の遷移先としてこのURLが設定されているログイン画面表示("/login")。→15
 補足:WebSecurityConfigクラスに認証処理のパスとしてこのURLが設定されているトップページ表示処理("/")。→16

com.kaz01u.demo.controller.IndexController.java ログイン画面コントローラ
package com.kaz01u.demo.controller;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.kaz01u.demo.auth.SimpleLoginUser;
import lombok.extern.slf4j.Slf4j;

@Controller
//log出力用
@Slf4j
public class IndexController {

    /**
     * 管理者メニュー画面表示
     * 管理者メニューを表示する
     * 
     * @return "members/admin/index"
     */
    @PostMapping("/members/admin/index")
    public String adminIndex(@AuthenticationPrincipal 
                                      SimpleLoginUser loginUser) { //←12
        log.info("#user id:{}, name:{}", loginUser.getUser().getId(), loginUser.getUser().getName());
        return "members/admin/index";
    }

    /**
     * ユーザーメニュー画面表示
     * ユーザーメニューを表示する
     * 
     * @return "members/user/index"
     */
    @PostMapping("/members/user/index")
    public String userIndex(@AuthenticationPrincipal 
                                      SimpleLoginUser loginUser) { //←13
        log.info("#user id:{}, name:{}", loginUser.getUser().getId(), loginUser.getUser().getName());
        return "members/user/index";
    }

    /**
     * 管理者又はユーザーメニュー画面表示
     * 管理者又はユーザーメニューを表示する
     * 
     * @return "members/admin/index"、"members/user/index"
     */
    @GetMapping(value = "/members/index")
    public String user(@AuthenticationPrincipal 
                                      SimpleLoginUser loginUser) { //←14
        log.info("#user id:{}, name:{}, roles:{}", loginUser.getUser().getId(), loginUser.getUser().getName(), loginUser.getUser().getRoles());
        //管理者ロールが含まれているとき
        if (loginUser.getUser().getRoles().indexOf("ROLE_ADMIN") >= 0) {
            return "members/admin/index";
        //ユーザーロールが含まれているとき
        } else if (loginUser.getUser().getRoles().indexOf("ROLE_USER") >= 0) {
            return "members/user/index";
        }
        //ロールが空文字の時
        return "members/index";        
    }

    /**
     * ログイン画面表示
     * ログイン画面を表示する
     * 
     * @return "login"
     */
    @GetMapping("/login")
    public String login() { //←15
        return "login";
    }
 
  /**
   * トップページ表示処理
   * トップページを表示する処理
   *
   * @param signupForm サインアップフォームデータ
   * @param model モデル(ユーザーリスト)
   * @return "index"
   */
    @GetMapping(value = "/")
    public String index() { //←16
        return "login";
    }
}

テンプレート(ログイン画面表示("/login"))

■src\main\resources\templates\login.html ログイン画面表示("/login")
<!DOCTYPE html>
<html lang="ja" 
   xmlns:th="http://www.thymeleaf.org" 
   xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
 <meta http-equiv="x-ua-compatible" content="ie=edge">
<title>あなたのPGMタイトル&nbsp;&nbsp;管理システム</title>
<link rel="stylesheet" href="/css/screen.css" type="text/css" media="screen,print" />
</head>
<body>
<div id="wrapper">
<div id="header">
<div style="font-size: large; color:#FFFFFF; text-align: center;"><strong>あなたのPGMタイトル 管理システム</strong></div>
<div id="r-navi">
<form class="text-center" th:action="@{/logout}" method="post">
<input type="image" src="/img/botton_logout.gif" alt="ログアウト" width="59" height="14" hspace="5" vspace="7" border="0">
</form>
</div>
<p class="clear"></p>
</div>

<div id="contents-top">

 <form th:action="@{/login}" method="POST" id="login">
<input type="text" name="dummy" style="display:none;" />
 <input type="hidden" name="mode" value="login_do">
<div align="center">
    <table width="379" border="1" cellspacing="0" cellpadding="7">
   <tr><td>
    <table width="448" border="0" cellspacing="0" cellpadding="5">
      <tr>
     <td colspan="2">※ユーザーID(メールアドレス)、パスワードを入力後、「ログイン」ボタンを押してください。</td>
      </tr>
      <tr>
     <td>
     <table width="100%" border="0" cellspacing="0" cellpadding="5">
      <tr>
        <td>ユーザーID(メールアドレス)</td>
        <td><input type="text" id="username" name="username" value="" /></td>
      </tr>
      <tr>
        <td>パスワード</td>
        <td><input type="password" id="password" name="password" /></td>
      </tr>
      <tr>
        <td colspan="2">
<p th:if="${param.error}"  class="font-s-red-form">ユーザーID(メールアドレス)かパスワードが正しくありません</p>
        </td>
      </tr>
       </table>
       <br>
     </td>
     <td>
     <table border="0" cellspacing="0" cellpadding="0">
      <tr>
        <td><p class="btn"><input type="image" src="/img/botton_login.gif" alt="LOGIN" width="78" height="45" border="0" /></p></td>  
      </tr>
</table>
       </td>
      </tr>
    </table>
   </td></tr>
    </table>
</div>
</form>
</div>
<div id="footer">
<p>Copyright (c) あなたのPGM著作権表示 All rights reserved.</p>
</div>
</div>
</body>
</html>

管理者メニュー画面表示("/members/admin/index")


Thymeleaf 用の属性は xmlns:th="http://www.thymeleaf.org" で名前空間を宣言して利用できます。
以下のような仕組みで、管理者メニュー画面テンプレート(index.html)のTymeleafの機能「 th:replace="~{/members/admin/template :: layout(~{::act}, ~{::main})}">」により、基本画面テンプレート(template.html)のlayoutフラグメントの内容が置換され表示されます。

Tymeleafの機能「 th:replace="~{/members/admin/template :: layout(~{::act}, ~{::main})}">」で、→17
基本画面テンプレートのact、mainは、管理者メニュー画面表示の内容「th:fragment="act" 」→18、「th:fragment="main" 」→19で置き換えられる。
 補足:メニュー表示なので「th:fragment="main" 」は空です。

更に、管理者メニュー画面表示のactは「<div id="act" th:replace="~
{/members/admin/actionStr :: action}"></div> 」により
リスト昇降順切替などのアクションテンプレート(actionStr.html)の
「th:fragment="action" 」の内容に置き換えられる。→20

更に、基本画面テンプレート(template.html)のth:replace="~{/members/admin/header :: header}"は、header.html、th:replace="~{/members/admin/side :: side}"はside.html、th:replace="~{/members/admin/footer :: footer}は、footer.htmlに置換される。

■src\main\resources\templates\members\admin\index.html 管理者メニュー画面表示("/members/admin/index")
<!DOCTYPE html>
<!--/* (1) */-->
<html xmlns:th="http://www.thymeleaf.org"<!-- ←Thymeleaf 用の属性は xmlns:th="http://www.thymeleaf.org" で名前空間を宣言して利用できる -->
    th:replace="~{/members/admin/template :: layout(~{::act}, ~{::main})}"> <!-- ←17 -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>header</title>
</head>
<body>
<div th:fragment="act" th:remove="tag">   <!-- ←18-->
<div th:with="actionStr = '/members/admin/index'">
 <div id="act" th:replace="~{/members/admin/actionStr :: action}">
  </div>   <!-- ←20-->
 </div>
</div>
<div th:fragment="main" th:remove="tag">   <!-- ←19 -->
</div>
</body>
</html></html>
■src\main\resources\templates\members\admin\actionStr.html リスト昇降順切替などのアクションテンプレート
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1,![](https://storage.googleapis.com/zenn-user-upload/607d586e3fcd-20211220.png) shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>header</title>
</head>
<body>
<div th:fragment="action" th:remove="tag">   <!-- ←20-->
  <form name="frm2" method="post" th:action="@{${actionStr}}">
   <input type="text" name="dummy" style="display:none;" />
   <input type="hidden" name="mode" th:value="'list_up_dwn'"/>
   <input type="hidden" name="sortItemName" th:value="''">
   <input type="hidden" name="sortOrder" th:value="''">
  </form>
  <form name="frm4" method="post" th:action="@{${actionStr}}">
   <input type="hidden" name="mode" th:value="'list_back'"/>
   <input type="hidden" name="page" th:value="''">
  </form>
</div>
</body>
</html>
■src\main\resources\templates\members\admin\template.html 基本画面テンプレート
<!DOCTYPE html>
<html lang="ja" 
   xmlns:th="http://www.thymeleaf.org" 
   xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
   th:fragment="layout (act, main)">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
 <meta http-equiv="x-ua-compatible" content="ie=edge">
<!-- トークン値 -->
<meta name="_csrf" th:content="${_csrf.token}"/>
<!-- ヘッダー名 -->
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
 <title>あなたのPGMタイトル&nbsp;&nbsp;サイト管理システム</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/base/jquery-ui.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1/i18n/jquery.ui.datepicker-ja.min.js"></script>
<link rel="stylesheet" href="/css/screen.css" type="text/css" media="screen,print" />
<script type="text/javascript" src="/js/kaz.js"></script>
 <script type="text/JavaScript"><!-- 
        ・・・(省略)
  --></script>
</head>

<body>
<div id="wrapper">
 <script type="text/javascript" src="/js/kaz.js"></script>
 <div id="act" th:replace="${act}"></div>  <!-- ←18-->
 <div id="header" th:replace="~{/members/admin/header :: header}"></div>
 <div id="contents">
  <div id="side" th:replace="~{/members/admin/side :: side}"></div>
  <div id="main" th:replace="${main}"></div>   <!-- ←19 -->
  <p class="clear"></p>
 </div>
 <div id="footer" th:replace="~{/members/admin/footer :: footer}"></div>
</div>
<script type="text/javascript" src="/js/createDatepicker.js"></script>
</body>
</html>

ユーザーメニュー画面表示("/members/user/index")


ユーザー側のPGM自動作成を行わないので、このような画面表示になります

■src\main\resources\templates\members\user\index.html ユーザーメニュー画面表示("/members/user/index")
<!DOCTYPE html>
<html lang="ja"
   xmlns:th="http://www.thymeleaf.org"
   xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
 <meta http-equiv="x-ua-compatible" content="ie=edge">
 <title>ユーザー側メニュー</title>
</head>
<body>
<div id="app" class="container-fluid">
 <h1>ユーザー側メニュー<br/> ・自動作成@生成しないので、管理者側を参考にPGM作成してください</h1>
 <form class="text-center" th:action="@{/logout}" method="post">
 <input type="image" src="/img/botton_logout.gif" alt="ログアウト" width="59" height="14" hspace="5" vspace="7" border="0">
 </form>
</div>
</body>
</html>

管理者以外、ユーザー以外のメニュー画面表示("/members/index")


管理者以外、ユーザー以外のPGM自動作成を行わないので、このような画面表示になります。

■src\main\resources\templates\members\index.html 管理者以外、ユーザー以外のメニュー画面表示("/members/index")
<!DOCTYPE html>
<html lang="ja"
   xmlns:th="http://www.thymeleaf.org"
   xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
 <meta http-equiv="x-ua-compatible" content="ie=edge">
 <title>ログインすれば誰でもアクセスできるメニュー</title>
</head>
<body>
<div id="app" class="container-fluid">
 <h1>ログインすれば誰でもアクセスできるメニュー<br/> ・自動作成@生成しないので、管理者側を参考に作成してください</h1>
 <form class="text-center" th:action="@{/logout}" method="post">
 <input type="image" src="/img/botton_logout.gif" alt="ログアウト" width="59" height="14" hspace="5" vspace="7" border="0">
 </form>
</div>
</body>
</html>

・ユーザー情報管理


ユーザー情報の登録、更新、削除、CSVダウンロード、CSVアップロードができます。
ユーザー情報はPGMは固定なので、「javaSpringBootテーブル項目一覧などの定義.xlsm」に登録しません。
PGMは以下のようです。

controller.admin.UserController.java ユーザー情報管理コントローラー 
csv.UserCsv.java ユーザー情報CSV編集用 
entity.User.java ユーザー情報エンテティ 前述 
form.UserForm.java ユーザー情報フォーム 
form.UserSrchForm.java ユーザー情報検索フォーム 
form.SessionUserSrchForm.java セッションスコープ用ユーザー情報検索フォーム 
form.SessionUserSrchOrderForm.java セッションスコープ用ユーザー情報検索並び順フォーム 
repository.UserRepository.java ユーザー情報リポジトリ 前述 
service.UserService.java ユーザー情報サービス 
service.UserServiceImpl.java ユーザー情報サービスimpl 
upload.UserUpload.java ユーザー情報アップロード用 
画面用テンプレート 
 admin\user\userActionStr.html ユーザー情報用アクション文字列
 admin\user\userAmend.html ユーザー情報更新画面
 admin\user\userAmendRegister.html ユーザー情報登録更新画面
 admin\user\userDetail.html ユーザー情報表示画面
 admin\user\userList.html ユーザー情報検索一覧画面
 admin\user\userRegister.html ユーザー情報登録画面
 admin\user\userUpCsv.html ユーザー情報CSVアップロード画面

ここでは、com.kaz01u.demo.service.UserServiceImpl.java(ユーザー情報サービスimpl)の、重要な点のみ説明します。
・DB登録更新時にパスワードは「Spring SecurityのPasswordEncoder」でハッシュ化します。→21

■com.kaz01u.demo.service.UserServiceImpl.java ユーザー情報サービスimpl
package com.kaz01u.demo.service.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.validation.Valid;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import com.kaz01u.demo.entity.User;
import com.kaz01u.demo.exception.UploadComplexValidException;
import com.kaz01u.demo.form.UserForm;
import com.kaz01u.demo.repository.UserRepository;
import com.kaz01u.demo.service.UserService;
import com.kaz01u.demo.upload.UserUpload;
import com.kaz01u.demo.utils.Functions;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Validated
@Service
@Slf4j
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
 private final UserRepository userRepository;
 private final PasswordEncoder passwordEncoder;


 @Transactional(readOnly = true)
 @SuppressWarnings("unchecked")
 @Override
 public Page<User> getFindsUser(String countSql,String selectSql, List<Object> argsList, Pageable pageable) {
  return userRepository.finds(countSql, selectSql, argsList, pageable);
 }

 @Transactional(readOnly = true)
 @SuppressWarnings("unchecked")
 @Override
 public List<User> getFindsUser(String selectSql, List<Object> argsList) {
  return userRepository.finds(selectSql, argsList);
 }

 @Transactional(rollbackFor=Throwable.class)
 @Override
 public void register(String name, String email, String password, String[] roles, Boolean enableFlag) {
  if (userRepository.findByEmail(email).isPresent()) {
   throw new RuntimeException("invalid email");
  }
  String encodedPassword = passwordEncode(password);  // ←21
  String joinedRoles = Functions.joinStrs(roles);
  log.debug("name:{}, email:{}, roles:{}", name, email, joinedRoles);
  User user = new User(null, name, email, encodedPassword, 
    Functions.editStrs(joinedRoles), enableFlag);
  userRepository.save(user);
  userRepository.flush();
 }

 @Transactional(rollbackFor=Throwable.class)
 @Override
 public void register(@Valid List<UserUpload> userList) {
  
  // 行番号、項目のエラーメッセージ
  HashMap<Integer, ArrayList<String>> hashMap = new HashMap<>();
  // 項目昇順のエラーメッセージ
  ArrayList<String> list = new ArrayList<String>();
  int i = 0;
  boolean errFlg = false;
  //複合チェックを行う
  for (UserUpload userUpload: userList) {
   if (userUpload.isCheckEmailDupli(userRepository)) {
    errFlg = true;
    Functions.setErrorMsg(hashMap, Integer.valueOf(i+2), list, 
      "email:『メールアドレス』は、重複登録できません");
   }
   if (!userUpload.isCheckRoles()) {
    errFlg = true;
    Functions.setErrorMsg(hashMap, Integer.valueOf(i+2), list, 
      "roles:『ロール』を正しく入力してください");
   }
   if (!userUpload.isCheckeEableFlag()) {
    errFlg = true;
    Functions.setErrorMsg(hashMap, Integer.valueOf(i+2), list, 
      "enableFlag:『可否フラグ』を正しく入力してください");
   }
   i++;
  }
  //複合チェックエラー
  if (errFlg) {
   for (Map.Entry<Integer, ArrayList<String>> entry : hashMap.entrySet()) {
    // 項目昇順にする
    Collections.sort(entry.getValue());
   }
   //複合チェックエラー例外発生
   throw new UploadComplexValidException(hashMap);
  }
  //DB登録
  for (UserUpload userUpload: userList) {
   String encodedPassword = passwordEncode(userUpload.getPassword());  // ←21
   log.debug("name:{}, email:{}, roles:{}", userUpload.getName(), userUpload.getEmail(), userUpload.getRoles(), userUpload.getEnableFlag());
   User user1 = new User(null, userUpload.getName(), userUpload.getEmail(), 
     encodedPassword, Functions.editStrs(userUpload.getRoles()), userUpload.getEnableFlag().equals("true")?Boolean.TRUE:Boolean.FALSE);
   userRepository.save(user1);
  }
  userRepository.flush();
 }
 
 @Transactional(rollbackFor=Throwable.class)
 @Override
 public void delete(Long pk) {
  log.info("delete user:id={}", pk);
  User user = findByPk(pk);
  if (user == null) {
   log.error("user not found:pk={}", pk);
   throw new RuntimeException("invalid pk");
  }
  userRepository.delete(user);
  userRepository.flush();
 }

 @Transactional(readOnly = true)
 @Override
 public User findByPk(Long pk) {
  User user = userRepository.getOne(pk);
  return user;
 }

 @Transactional(readOnly = true)
 @Override
 public UserForm findByPkForUserForm(Long pk) {
  User user = findByPk(pk);
  UserForm userForm = null;
  if (user != null) {
   //null可の項目がnullのとき、空文字にする
   user.setRoles(Functions.nvl(user.getRoles()));
   String[] rolesArray = user.getRoles().split(",");
   userForm = new UserForm(user.getId(), user.getName(), user.getEmail(), 
     "","", rolesArray, user.getEnableFlag());
  } else {
   log.error("user not found:pk={}", pk);
   throw new RuntimeException("invalid pk");
  }
  return userForm;
 }

 @Transactional(rollbackFor=Throwable.class)
 @Override
 public User update(Long id, String name, String email, String password, String[] roles, Boolean enableFlag) {
  String encodedPassword = passwordEncode(password);  // ←21
  String joinedRoles = Functions.joinStrs(roles);
  log.debug("id:{}, name:{}, email:{}, roles:{}", id, name, email, joinedRoles);
  User user = new User(id, name, email, encodedPassword, Functions.editStrs(joinedRoles), enableFlag);
  Optional<User> findUser1 = userRepository.findById(user.getId());
  User userAftSave;
  //登録がある場合
  if (findUser1.isPresent()) {
   Optional<User> findUser2 = userRepository.findByEmail(user.getEmail());
   if (findUser2.isPresent()) {
    //Emailをほかのユーザが使っている場合
    if (findUser2.get().getId() != user.getId()) {
     log.error("invalid email={}", user.getEmail());
     throw new RuntimeException("invalid email");
    }
   }   
   userAftSave = userRepository.save(user);
   userRepository.flush();
  } else {
   log.error("user not found:pk={}", user.getId());
   throw new RuntimeException("invalid pk");
  }
  return userAftSave;
 } 

 /**
  *
  * @param rawPassword 平文のパスワード
  * @return 暗号化されたパスワード
  */
 private String passwordEncode(String rawPassword) {
  return passwordEncoder.encode(rawPassword);
 }
}