Chapter 08

ログイン、ログアウトの実装

あしたば
あしたば
2022.08.21に更新

この章で学ぶこと

前回導入したSpringSecurityにはいくつかの認証を行う機能が付随しています。
その中でも、Webサービスにはつきものなログイン要件を叶えるためのDB認証に関して利用してみましょう。

個別のユーザという概念を実装すれば、そのユーザ固有のデータを保存することができるため、Webサービスとしてできることの幅が広がります。

が、同時にユーザの個人情報にも触れることになります。
特に、ID、パスワード、メールアドレスといったものは、一度流出すると、それを使って他のサービスに不正アクセスされる可能性もあるため厳重な取り扱いが求められることに留意しましょう。
(サービスを公開するときはプライバシーポリシーや利用規約に留意が必要です。この記事では触れません)

  • Spring Securityと認証、認可
  • JDBC認証のための準備
  • ユーザの登録導線を作る
  • ログインする
  • ログアウトする

Spring Securityと認証、認可

最初にはっきり言っておくと、Railsのdevice等と比べるとSpringSecurityのそれは遥かに実装が難しく、使うのも単純には行きません。
ぱっと情報を調べてもSpringSecurityによってログインを実現する事例も明らかに少ないですし、リファレンスも難解です。

しかしながら、SpringSecurityの実装からは学べるものが多くありますので、(Rails + deviceがなんとなくえーーっいで終わってしまうのに比べれば)気を強く持って進めましょう。

SpringSecurityには認証と認可2つの機能がありますので、まずはこの認証認可というセキュリティ用語を抑えておく必要があります。
クラスメソッドさんがよい記事を書いてくださっているので読んでみてください。

ここでは簡単な説明・引用に留めます。

認証

認証とは、その対象になるモノが何なのか証明することです。
現実世界では、上記の記事に書いてあるように、マイナンバーカードがその役割に近しいでしょう。

例えば、私にあなたのマイナンバーカードを提示して、そこに「山田太郎」と書いてあった場合、私は目の前にいる人が山田太郎さんであることを認めると思います。でも、それだけです。こんにちは、山田太郎さん。(にっこり)

あなたが山田太郎さんだったとして、それだけで車の運転が許されるわけでもなければ、税務手続きのサポートでお金を稼げるわけでもなければ、国会議事堂への立ち入りを許可されるわけでもありませんね。

認可

一方、認可とは 特定の条件に対して、アクセスの権限を与えること です。
この特定の条件ですが、 それが何(誰)であるかは気にしません。

駅で切符を買いました。あなたは電車に乗ることを許されます。それだけです。あなたが誰かは関係ありません。

認証に基づく認可

これ以上にわかりやすい説明が不可能なので完全に引用いたします。

認証に基づく認可のメタファは「運転免許証」でしょうか。免許証は、写真によりある人が誰であるかを証明し、その上で、その人に対して運転の認可をしています。

切符のように、誰かに渡しても受け取った人が運転を許されたりはしません。なぜならこの認可は、認証にもとづいているからです。

Spring Security

Spring Securityは「認証」と「認可」をそれぞれサポートしています。

で、今回メインになるのは数ある認証方法のなかでも「ユーザ名・パスワード認証」です。さらにその中の、「JDBC認証」を取り扱います。

これはユーザ名とパスワードの組み合わせを持って「認証」を行う方法で、そのユーザ名とパスワードはDBで管理するよ、ということです。
ちなみに、JDBC(Java Database Connectivity) とは、RDBMS(リレーショナル・データベース・マネジメントシステム、要は一般にDBと呼ばれるもの。)を扱うためのJavaの標準APIを指しています。なので、DB認証と言い換えてもよいです。

JDBC認証のための準備

内容が難しいながら、リファレンスが整っているので、読み解いていきましょう。

リファレンスの冒頭で、JDBC認証については下記のように説明されています。

Spring Security の JdbcDaoImplUserDetailsService を実装し、JDBC を使用して取得されるユーザー名 / パスワードベースの認証をサポートします。JdbcUserDetailsManagerJdbcDaoImpl を継承して、 UserDetailsManager インターフェースを介した UserDetails の管理を提供します。 UserDetails ベースの認証は、Spring Security が認証のために「ユーザー名・パスワード認証」に構成されている場合に使用されます。

理解のポイントを前提条件を含めて整理しましょう。

  • JdbcDaoImplUserDetailsServiceを実装している。
    • UserDetailsServiceはユーザ情報を検索する処理を担うインタフェース。JDBC認証の場合は、JdbcUserDetailsManagerがその実装を行っている。
  • JdbcUserDetailsManagerUserDetailsManagerを介してUserDetailsの管理を提供する。
    • UserDetailsManageのJavadocを確認してみましょう。提供されるメソッドはシンプルです。
      • changePassword: パスワード変更
      • createUser: ユーザの作成
      • deleteUser: ユーザの削除
      • updateUser: ユーザの更新
      • userExists: ユーザの存在確認
    • JdbcUserDetailsManagerは上記のIFを実現しているため、DBを使ってユーザの管理を行っている機能であることがわかります。
    • UserDetailsのインタフェースを確認すると、ユーザのデータに対して認証/認可に係るインタフェースを持っていることがわかります。

まとめると、「JdbcUserDetailsManager」がDBを使ってユーザの情報(UserDetails)をいい感じに管理してくれます、ということです。

デフォルトスキーマの用意

JDBC認証は、DBを使ってユーザの情報を管理しますので、SpringSecurityがDBにユーザや認証・認可に利用するテーブル定義(スキーマ)が存在します。

リファレンスによると、このデフォルトスキーマのDDL(Data Definition Language, データを定義するための言語)は下記に公開されているということなので、早速検証してみましょう。

org/springframework/security/core/userdetails/jdbc/users.ddl

application.ymlのschemanについて次のように書き換えます。

  sql:
    init:
      schema-locations:
        - classpath:h2/schema.sql
        - classpath:org/springframework/security/core/userdetails/jdbc/users.ddl

クラスの実装によりますが、このように配列型のデータを受け取れるようになっている項目も存在します。これを使えば楽にデータ定義を流し込めますね。

このままapplicationを起動し、http://localhost:8080/h2-consoleからデータベースを覗いてみましょう。

…と、エラーになってしまいますね。
これは、前回のSpringSecurityの設定によって、h2-consoleのページにもセキュリティが適用されてしまったため、CSRF等の対策が働いているからです。
h2-console自体、開発者向けの機能ですから、当然csrf対応などは存在していません。

SecurityConfigクラスで、h2-consoleへのSecurityを無効にしておきましょう。

@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers("/h2-console/**").permitAll()
        .and().csrf().ignoringAntMatchers("/h2-console/**")
        .and().headers().frameOptions().sameOrigin();
    return http.build();
  }
}

このように、書き換えます。
authorizeRequests().antMatchers("/h2-console/**").permitAll() は/h2-consoleから始まるパスは誰でもアクセスして良い。
csrf().ignoringAntMatchers("/h2-console/**")は、h2-consoleから始まるパスはCSRF対策しない。
headers().frameOptions().sameOrigin()は、同じドメインであればiframeを許可する。
という意味合いになっています。

ともあれ、これで再度DBへアクセスしてみましょう。次はアクセスすることができます。

我々が用意しているテーブルはUSER_COMMENTだけですが、他にテーブルが増えましたね。これで、SpringSecurityが要求するユーザ情報の入れ物を作ることができました。

h2コンソール用のユーザの準備

data.sqlによる初期化処理で、h2-consoleのためのADMINユーザを用意しておきましょう。
下記2行をdata.sqlに追加してください

INSERT INTO USERS (USERNAME, PASSWORD, ENABLED) VALUES
('admin', '{bcrypt}$2a$10$vC.r53zKYPwEXplBYH3mxuZP52r2u3udRcEg9yTUmwYE5yjmoUXyG', true);
INSERT INTO AUTHORITIES (USERNAME, AUTHORITY) VALUES ('admin', 'ROLE_ADMIN');
INSERT INTO AUTHORITIES (USERNAME, AUTHORITY) VALUES ('admin', 'ROLE_USER');

DatasourceとJdbcUserDetailsManager Beanの設定

リファレンスによると、掲題のものが必要となっています。

SpringBootを利用しているので、実はこのあたりに関してはいくらかの省略が行われます。
Datasourceはapplication.ymlのspring.datasourceの項目をspringbootが読み取ってくれますからまるごと不要です。

UserDetailsManagerのBean登録は必要ですのでDatabaseConfigクラスを作成しましょう。

内容は下記です。

package chalkboard.me.bulletinboard.infrastructure.datasource.condig;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;

import javax.sql.DataSource;

@Configuration
@RequiredArgsConstructor
public class DatabaseConfig {
  private final DataSource dataSource;

  @Bean
  public UserDetailsManager userDetailsManager() {
    return new JdbcUserDetailsManager(dataSource);
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  }
}

@BeanDIコンテナに指定されたインスタンスを格納し、必要なときに取り出して使うようにすることができる、というものです。

ここでは、UserDetailsManagerのBeanとしてJdbcUserDetailsManagerのインスタンスを定義していますので、UserDetailsManagerくれ、というシーンが出てきたらJdbcUserDetailsManagerが利用されるということになります。

passwordEncoderはパスワードのエンコード形式をさしています。デフォルトでは、bcryptと呼ばれるハッシュアルゴリズムでエンコードされ、元がなんであったのかわからないように不可逆な変更が行われます。

ハッシュ化と暗号化は違いますよ
復号「化」もちょっとモニョリます

SecurityConfigの設定

SecurityConfigを書き換えて、ログインしないと使えない掲示板にしてみましょう。

package chalkboard.me.bulletinboard.config;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/h2-console/**").hasRole("ADMIN")
        .antMatchers("/board").hasRole("USER")
        .and().formLogin()
          .loginPage("/user").permitAll() // ログインページのカスタマイズ
          .defaultSuccessUrl("/board") // ログイン認証ページの要求, ログイン成功後デフォルト画面の設定
        .and().logout()
          .logoutUrl("/user/logout")
          .logoutSuccessUrl("/user")
        .and().csrf().ignoringAntMatchers("/h2-console/**")
        .and().headers().frameOptions().sameOrigin();
    return http.build();
  }

  @Bean
  public WebSecurityCustomizer webSecurityCustomizer() {
    return web -> web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
  }
}

DBにはROLE_ADMIN、とロールを登録しました。が、このコンフィグでhasRoleを使うときは、ROLE_の接頭辞は外す必要がありますので注意してください(内部ロジックが勝手にROLE_を付与するため)

        .antMatchers("/h2-console/**").hasRole("ADMIN")
        .antMatchers("/board").hasRole("USER")

hasRoleは、antMatchersで一致したURLへのアクセスに対して、そのRoleを持っていることを要求します。ロールはアカウントに紐付いていますので、ログイン必須になっています。

これで、ユーザにログインを強制するようになりました。
しかし、困ったことがあります。

ログインする導線もなければ登録する動線もありませんね。このままでは使い物になりません。

[TIPS]誰がUserDetailsManager/PasswordEncoderを使うのか

SpringSecurity5.7より以前では、掲題の機能を使うために AuthenticationManagerBuilder というクラスを使用していました。

SpringSecurity5.7からは、SpringSecurityがAuthenticationManagerを作成する際に、DIコンテナに存在しているUserDetailsManager, PasswordEncoderを使用するようになっています。

https://github.com/spring-projects/spring-security/issues/10822#issuecomment-1090160648

そのため、AuthenticationManagerについてユーザ側で考慮する必要はありません。

ユーザの登録導線を作る

ユーザIDとパスワードを受け取って、ユーザを作成する画面を作りましょう。

[TIPS]SpringSecurityのデバッグログ

SpringSecurityはデフォルトですと、ほとんどログをだしてくれません。
開発に詰まったときのヒントがありませんので、適宜下記の設定でデバッグログを有効にしてください。

application.ymlに追加します。

logging:
  level:
    org:
      springframework:
        security: DEBUG

登録画面づくり

手始めに、ユーザログイン、登録のためのFormクラスを作ります。

package chalkboard.me.bulletinboard.application.form;

import lombok.Data;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

@Data
public class UserForm {
  @Size(max=20)
  @Pattern(regexp = "^[a-zA-Z0-9]*$")
  @NotNull
  private String username;
  @Size(max=64)
  @Pattern(regexp = "^[a-zA-Z0-9]*$")
  @NotNull
  private String password;
}

全角文字の扱いが出てくると後々面倒なことになるので、ログインに使う名前とパスワードは半角英数字に制限してしまいます。(@Pattern(regexp = "^[a-zA-Z0-9]*$") がそれをします。この書き方を正規表現といいます。)

次に、アクセスを受けるControllerを作成しましょう。

package chalkboard.me.bulletinboard.presentation;

import chalkboard.me.bulletinboard.application.form.UserForm;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
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;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
  /**
   * 登録ページの表示
   * @return
   */
  @GetMapping("/signup")
  public ModelAndView signup(ModelAndView modelAndView) {
    modelAndView.setViewName("user/signup");
    modelAndView.addObject("userForm", new UserForm());

    return modelAndView;
  }

  /**
   * ユーザの登録処理
   * @param userForm
   * @param bindingResult
   * @return
   */
  @PostMapping("/signup")
  public ModelAndView register(
      @Validated @ModelAttribute UserForm userForm,
      BindingResult bindingResult
  ) {
    if(bindingResult.hasErrors()) {
      ModelAndView modelAndView = new ModelAndView("user/signup");
      modelAndView.addObject("userForm", userForm);
      return modelAndView;
    }

    //TODO: ユーザ作成処理

    return new ModelAndView("redirect:/board");
  }
}

Controllerを作ったら、次はそのための画面ですね。

<!DOCTYPE html>
<html
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        xmlns:th="http://www.w3.org/1999/xhtml"
        layout:decorate="~{layout}">
<head>
    <title>掲示板</title>
    <meta name="description" content="ユーザー登録">
</head>
<body>
<div layout:fragment="layoutContent">
    <h2>新規ユーザー登録</h2>
    <form method="POST" th:action="@{/user/signup}" th:object="${userForm}">
        <div>
            <input placeholder="ユーザネーム" th:field="*{username}">
            <span class="error-message" th:if="${#fields.hasErrors('username')}" th:errors="*{username}">Name Error</span>
        </div>
        <div>
            <input placeholder="パスワード" th:field="*{password}" type="password">
            <span class="error-message" th:if="${#fields.hasErrors('password')}" th:errors="*{password}">password Error</span>
        </div>
        <button>送信</button>
    </form>
</div>
</body>
</html>

これで一度アプリケーションを起動してみます。
ログイン画面はまだないので、http://localhost:8080/user/signupにアクセスしましょう。

はい、表示されました。
しかし、登録処理はまだTODOなので、データを送っても何も起こりません。

登録処理

ではリポジトリを作成していきましょう。

認証に係る処理やデータは、だいたいビジネスロジックに絡まない=domain層におく価値がない一方、ユースケースには深く関わるため、application層にRepositoryを定義します。

package chalkboard.me.bulletinboard.application.auth;

public interface UserAuthRepository {
  void createUser(String userName, String password);
}

実装はインフラ層です。

ImmutableSetというコレクションを利用したいので、pom.xmlに次を追加しましょう。

<dependency>
	<groupId>com.google.guava</groupId>
	<artifactId>guava</artifactId>
	<version>30.1-jre</version>
</dependency>
package chalkboard.me.bulletinboard.infrastructure.datasource;

import chalkboard.me.bulletinboard.application.auth.UserAuthRepository;
import com.google.common.collect.ImmutableSet;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.stereotype.Repository;

@RequiredArgsConstructor
@Repository
public class UserDatasource implements UserAuthRepository {
  private final UserDetailsManager manager;
  private final PasswordEncoder passwordEncoder;

  public void createUser(String userName, String password) {
    User user = new User(
        userName,
        passwordEncoder.encode(password),
        ImmutableSet.of(new SimpleGrantedAuthority("ROLE_USER"))
    );
    manager.createUser(user);
  }
}

UserDetailsManagerをそのまま使う形で実装しているので、Mapperを定義していません。

では、これを使用する、つまりユーザの登録や削除等を扱うユースケースを作成しましょう。

package chalkboard.me.bulletinboard.application.usecase;

import chalkboard.me.bulletinboard.application.auth.UserAuthRepository;
import chalkboard.me.bulletinboard.application.form.UserForm;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

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

@Service
@RequiredArgsConstructor
public class UserAuthUsecase {
  private final UserAuthRepository authRepository;

  public void userCreate(UserForm form, HttpServletRequest request) throws ServletException {
    authRepository.createUser(
        form.getUsername(),
        form.getPassword()
    );

    request.login(form.getUsername(), form.getPassword());
  }
}

HttpServletRequestを使うことで、ユーザが問題なく作れたら自動でログインするようにしています。

これをUserControllerから呼べるようにしてみます。

package chalkboard.me.bulletinboard.presentation;

import chalkboard.me.bulletinboard.application.form.UserForm;
import chalkboard.me.bulletinboard.application.usecase.UserAuthUsecase;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
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;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@Controller
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {

  private final UserAuthUsecase userAuthUsecase;

  /**
   * 登録ページの表示
   * @return
   */
  @GetMapping("/signup")
  public ModelAndView signup(ModelAndView modelAndView) {
    modelAndView.setViewName("user/signup");
    modelAndView.addObject("userForm", new UserForm());

    return modelAndView;
  }

  /**
   * ユーザの登録処理
   * @param userForm
   * @param bindingResult
   * @return
   */
  @PostMapping("/signup")
  public ModelAndView register(
      @Validated @ModelAttribute UserForm userForm,
      BindingResult bindingResult,
      HttpServletRequest request
  ) {
    if(bindingResult.hasErrors()) {
      ModelAndView modelAndView = new ModelAndView("user/signup");
      modelAndView.addObject("userForm", userForm);
      return modelAndView;
    }
    try {
      userAuthUsecase.userCreate(userForm, request);
    }catch (Exception e) {
      log.error("ユーザ作成 or ログイン失敗", e);
      return new ModelAndView("redirect:/user");
    }

    return new ModelAndView("redirect:/board");
  }
}

では、ユーザ作成処理を動かしてみましょう。アプリケーションを起動します。

http://localhost:8080/user/signup

にアクセスしましょう。

ユーザを作成すると、うまくいけば

掲示板にアクセスできます。
このとき、Cookieにはセッション情報があります。このセッション情報が、ログイン情報と紐付けられていますので、この状態はいわゆる「ログインしている」状態になります。

この状態では、新規ユーザをまた作成しようとすると、作成には成功するもののログイン処理には失敗します。これは、2重でログインしようとしたからです。
暫定的な対処としてはCookieを手動削除してしまいます。こうすると、セッションが再発行されますので、以前の認証情報が使われなくなり、ログアウトと同等の状態になります。

ログインする

さて、登録はできるようになったので、次はシンプルなログイン処理を実装しましょう。

UserControllerにメソッドを2つ足します。

  @GetMapping
  public ModelAndView loginPage(ModelAndView modelAndView) {
    modelAndView.setViewName("user/login");
    modelAndView.addObject("userForm", new UserForm());

    return modelAndView;
  }

これだけです。
というのも、SecurityConfigで、loginPageメソッドに/userを指定しています。
これによって、このパスに対してPOSTメソッドでUserDetailsインタフェースに適合するデータを投げつけると、自動的にログイン処理が行われ、defaultSuccessUrlで指定したページへリダイレクトされるというわけです。

ただ、Webページは自分でカスタマイズする前提にしてあるので、作成しましょう。

ほとんど登録と同じですが、th:actionは/userである必要がありますので注意しましょう。

<!DOCTYPE html>
<html
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        xmlns:th="http://www.w3.org/1999/xhtml"
        layout:decorate="~{layout}">
<head>
    <title>掲示板</title>
    <meta name="description" content="ログイン">
</head>
<body>
<div layout:fragment="layoutContent">
    <h2>ログイン</h2>
    <form method="POST" th:action="@{/user}" th:object="${userForm}">
        <div>
            <input placeholder="ユーザネーム" th:field="*{username}">
            <span class="error-message" th:if="${#fields.hasErrors('username')}" th:errors="*{username}">Name Error</span>
        </div>
        <div>
            <input placeholder="パスワード" th:field="*{password}" type="password">
            <span class="error-message" th:if="${#fields.hasErrors('password')}" th:errors="*{password}">password Error</span>
        </div>
        <button>送信</button>
    </form>
</div>
</body>
</html>

では検証する前に一度、手動でログアウトしましょう。
検証ツールを使ってCookieを削除します。

アプリケーションを実行し、
http://localhost:8080/board

にアクセスすると、認証情報がないためログイン画面に移動します。

ここで、ADMINアカウントであるadmin/adminでログインしてみましょう。

うまくいけば、掲示板にアクセスできます。

Adminなので、
http://localhost:8080/h2-console
にもアクセスできます。

ログアウトする

さて、登録もログインもできました。

しかし、このままでは一般のユーザはログアウトできないため、戸惑うかもしれませんからログアウトを実装しておきましょう。

ログアウトのリファレンスはシンプルなので、一度読んでみてください。
ログアウト時の追加処理や、削除対象のCookieなどをカスタマイズできます。

今回は簡単に、URLのカスタマイズをしてみましょう。

SecurityConfigクラスのconfigureに、ログアウトに係る処理、logout()からはじまるメソッドチェーンを追加しましょう。
これで、/user/logoutにアクセスすると、ログアウト処理が走るようになります。
(POSTでアクセスする必要があります)

  @Override
  public void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/h2-console/**").hasRole("ADMIN")
        .antMatchers("/board").hasRole("USER")
        .and().formLogin()
          .loginPage("/user").permitAll() // ログインページのカスタマイズ
          .defaultSuccessUrl("/board") // ログイン認証ページの要求, ログイン成功後デフォルト画面の設定
        .and().logout()
          .logoutUrl("/user/logout")
          .logoutSuccessUrl("/user")
        .and().csrf().ignoringAntMatchers("/h2-console/**")
        .and().headers().frameOptions().sameOrigin();
  }

ログアウトボタンの表示

さて、ログアウトのボタンを追加すればよいだけですが、少し考えてみてください。
単純にログアウトボタンを追加するだけですと、ログイン指定なくても表示されてしまいますね。

ですので、

  • ログインしていなければ「登録」と「ログイン」
  • ログインしていれば「ログアウト」

への導線をヘッダーに出すようにしてみます。

まず、SpringSecurityとThymeleafで連携をとるため、下記のモジュールをpom.xmlに追加しましょう。

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

こちらの大本の機能リファレンスはthymeleafのリファレンスと、ライブラリそのもののgithubにあります。

では、layout.htmlを書き換えましょう。

<!DOCTYPE html>
<html
        xmlns="http://www.w3.org/1999/xhtml"
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        xmlns:th="http://www.w3.org/1999/xhtml"
        xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
>
<head>
    <meta charset="UTF-8" />
    <meta name="description" content="common-meta">
    <title>レイアウト</title>

    <link th:href="@{/css/style.css}" rel="stylesheet" type="text/css">
</head>
<body>
    <header>
        <div>
            <h1 id="logo">けいじばん</h1>
        </div>
        <div class="account">
            <div th:if="${#authorization.expression('isAuthenticated()')}">
                <form th:action="@{/user/logout}" method="post">
                    <button class="logout-button">
                        ログアウト
                    </button>
                </form>
            </div>
            <div th:if="${#authorization.expression('!isAuthenticated()')}">
                <a href="/user">
                    <div>ログイン</div>
                </a>
                <a href="/user/signup">
                    <div>サインアップ</div>
                </a>
            </div>
        </div>
    </header>
    <div id="content">
        <main id="main-area" layout:fragment="layoutContent">
            <p>Default content</p>
        </main>
        <div id="side-area">
            サイドバー
        </div>
    </div>
    <footer>footer</footer>
</body>
</html>

最低限にCSSを設定しておきましょう。style.cssを変更します。
headerに要素を追加、新しくIDセレクタ#logoを加えます。

header{
    display: flex;
    background-color: #f0f8ff;
    padding: 0.5rem 1rem;
    justify-content: space-between; /*これが追加*/
}

#logo {
    font-size: 1.5rem;
    margin: 0.1rem 0.25rem;
}

これで表示してみましょう。

ログイン状態によって表示を切り替えることができました。
他にも、持っているロールによって分岐を作ったりすることも可能です。

TIPS

Userデータそのもののカスタマイズを行うことも出来ます。たとえば、ユーザのメールアドレスをUserテーブルに登録する、といったことを実現する方法です

下記の記事を紹介いたします。
https://b1tblog.com/2020/02/27/spring-security-5/

ちなみに、自分でアプリケーションを作るにしても、仕事で作るにしても、SQLを書く前に設計の段階をきちんと踏むことをおすすめします。
設計なくSQLを書き始めると、データ構造がぐちゃぐちゃになってしまいます。
ER図や、それらデータを扱うユースケース図などの書き方を調べてみてください。

https://qiita.com/nishina555/items/a79ece1b54faf7240fac

今回のPR

https://github.com/angelica-keiskei/spring-sample/pull/10

このPRはSpringBoot2.7対応の影響を受けています

https://github.com/ange-k/spring-sample/pull/14