ログイン

Spring Security

Springでログイン機能を実装するには、「Spring Security」を使用します。
Spring Securityはセキュリティ対策機能の実装に役立つフレームワークで、認証と認可の機能も提供しています。

一般にWebアプリケーションにおいて「認証」「認可」という言葉をよく使用します。
頻出ワードですので、意味を押さえておきましょう。
認証(Authentication):アプリケーションを使用するユーザーを特定すること。ログインユーザーの正当性を確認するプロセス。
認可(Authorization):認証したユーザーが行った操作の可否を制御すること。認証後、アクセスを制御するプロセス。管理者と一般ユーザーで表示内容を変える等。

ログイン機能の実装

さっそくログイン機能を実装していきましょう。

Spring Securityの導入、デフォルトのログイン機能

プロジェクト直下のpom.xmlに下記を追記しましょう。
<dependencies>タグ内に記述してください。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

入力が完了したら、プロジェクトを更新します。
(Spring Bootアプリケーションを実行中の場合は終了させてから、
プロジェクト名を右クリック>Maven>プロジェクトの更新 をクリック)

Spring Bootアプリケーションを実行して、既存のページにアクセスすると、
リダイレクトされて画像のようなログイン画面に飛ばされるはずです。

こちらは以下の固定されたアカウントでのみログインできます。
ログインすると、これまでと同様にどのページにもアクセスできるようになります。
・ユーザー名:user
・パスワード:アプリケーション起動時にコンソール出力された文字列(文字列は起動ごとに異なる)


上記の通り、この時点では初期機能ですので
決まったアカウントでのみログインでき、ログイン画面もこの初期表示のみしか使用できず、
このプロジェクト内の全てのページがログイン認証が必要(ログインしないとアクセス許可されない)状態になっています。

続いて機能を充実させていきましょう。

DB接続して認証する機能の実装

テーブル、データの準備

用意したDBのテーブルのデータからログイン認証するように変更します。

テーブルに必要なカラムはid, name, passwordです。
ここでは下記のSQL文で作成してください。

CREATE TABLE login_users ( id INTEGER, username VARCHAR2(50 CHAR), password VARCHAR2(1000 BYTE), email VARCHAR2(100 CHAR),
PRIMARY KEY (ID) );

Spring Securityの認証機能では、安全の為、パスワードはDBに保存する際はハッシュ化して保存しておき、
アプリケーション内部で照合を行うようになっています。
そのため、テーブルのパスワード列に保存する値はハッシュ化されたものでなければなりません。

下記のサイトで生成できますので、パスワードにしたい任意の文字列を入力してハッシュ値を生成し、
生成したハッシュ値とともに任意の値をテーブルに保存しておいてください。
BCrypt ハッシュ値 計算

データ挿入SQL例
INSERT INTO login_users VALUES ( 1, "test", "$2a$10$B2uKvb4HeWU.nuTT/EXuweIV・・・", "sample@example.com" );

プロジェクトに作成するファイル一覧

  • エンティティ (User.java)
  • リポジトリ(UserRepository.java)
  • UserDetailsImpl.java …認証処理で必要となる資格情報(ユーザー名とパスワード)を保持する。
  • UserDetailsServiceImpl.java …資格情報とユーザーの状態をDBから取得する。
  • セキュリティ設定クラス (SecurityConfig.java) …Spring Securityの設定を定義するクラス。

それぞれの詳細なロジックについてはここでは割愛します。
参考サイト:
【Spring Security】ユーザー認証のイメージを掴む編

エンティティ、リポジトリの作成

エンティティ、リポジトリに関してはこれまでと比較して変わったところは特にありません。

【com/cmps/spring/entity/User.java】

package com.cmps.spring.entity;

import jakarta.persistence.*;
import lombok.Data;

@Entity
@Data
@Table(name = "login_users")
public class User {
	
	// ID
	@Id
	@Column
	private Integer id;

	// ユーザーネーム(英字)
	@Column(length = 100)
	private String username;

	// パスワード
	@Column(length = 1000)
	private String password;

	// Eメール
	@Column(length = 100)
	private String email;
}


【com/cmps/spring/repository/UserRepository.java】

package com.cmps.spring.repository;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.cmps.spring.entity.User;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {

	/**
	 * ユーザー名による検索
	 * @param username ユーザー名
	 * @return Optional<User>
	 */
	Optional<User> findByUsernameIs(String username);
}

UserDetailsの実装(UserDetailsImplの作成)

認証処理で必要となる資格情報(ユーザー名とパスワード)を保持するためのインターフェースを実装します。
ログイン(認証)時に、Spring Securityから使用されます。

【com/cmps/spring/service/impl/UserDetailsImpl.java】

package com.cmps.spring.service.impl;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.cmps.spring.entity.User;

public class UserDetailsImpl implements UserDetails {

	// Userオブジェクトをフィールド変数として保持
	private final User user;
	
	/**
	 * コンストラクタ
	 * @param User
	 */
	public UserDetailsImpl(User user) {
		this.user = user;
	}

	/**
	 * Userオブジェクトを返すgetterメソッド
	 * @return User
	 */
	public User getUser() {
		return user;
	}

	/**
	 * ロールの取得(今回は使わないのでnullをリターン)
	 */
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return null;
	}

	/**
	 * パスワードの取得
	 */
	@Override
	public String getPassword() {
		return this.user.getPassword();
	}

	/**
	 * ユーザー名の取得
	 * ログインユーザーの名前をGetする
	 */
	@Override
	public String getUsername() {
		return this.user.getUsername();
	}

	/**
	 * アカウントが有効期限でないか(使わないので常にtrue)
	 */
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	/**
	 * アカウントがロックされていないか(使わないので常にtrue)
	 */
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	/**
	 * 認証情報が有効期限切れでないか(使わないので常にtrue)
	 */
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	/**
	 * アカウントが有効であるかどうか(使わないので常にtrue)
	 */
	@Override
	public boolean isEnabled() {
		return true;
	}
}

主な役割はメンバ変数としてUserオブジェクトを保持していることです。
getAuthorities()以降のメソッドは、ユーザーアカウント情報に関連するメソッドで、インターフェースに定義されているためにオーバーライドが必要なため記述しています。
ここではDBのデータからログインできることを目的にしているので、上記については省略した記述としています。

UserDetailsServiceの実装(UserDetailsServiceImplの作成)


【com/cmps/spring/service/impl/UserDetailsServiceImpl.java】

package com.cmps.spring.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
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 com.cmps.spring.entity.User;
import com.cmps.spring.repository.UserRepository;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
	
	@Autowired
	private UserRepository userRepository;

	/**
	 * ユーザー名で検索しUserDetailsImpl(ユーザー情報)を返す
	 */
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = userRepository.findByUsernameIs(username).orElse(null);
		if (user == null) {
			throw new UsernameNotFoundException("not found :" + username);
		}
		return new UserDetailsImpl(user);
	}
}

UserDetailsService には loadUserByUsername() というメソッドが1つだけ存在します。
ログイン画面で入力されたユーザー名を引数として受け取り、その識別文字列に対応するユーザー情報を返却します。
対応するユーザー情報が存在しない場合は UsernameNotFoundException をスローします。
パスワードが合っているかの照合は、UserDetailsImplのデータをもとにSpring Securityの機能が確認してくれます。

SecurityConfigの作成

Spring Securityの設定を定義するためのクラスです。
パスワードのハッシュ化、アクセス制御、ログインページの設定、ログアウトの設定などを行うことができます。
【com/cmps/spring/config/SecurityConfig.java】

package com.cmps.spring.config;

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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	/**
	 * パスワードエンコーダー(パスワードのハッシュ化)を提供する
	 */
	@Bean
	PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Bean
	protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http.formLogin(login -> login		// ログインの設定を記述
				.defaultSuccessUrl("/hello") // ログイン成功後のリダイレクト先URLを指定
		).logout(logout -> logout			// ログアウトの設定を記述
				.logoutSuccessUrl("/login") // ログアウト成功後のリダイレクト先URLを指定
		).authorizeHttpRequests(authz -> authz							//URLごとの認可設定を記述
				.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // "/src/main/resources/css/**"などはログインなしでもアクセス可とする(設定しないとCSSが利かない)
				.requestMatchers("/hello", "/emp/all").authenticated()	// 認証が必要なURLとして設定
				.anyRequest().permitAll()								// その他は認可なくてもアクセス許可
		);

		return http.build();
	}
}

passwordEncoder()メソッドはパスワードエンコーダーについて設定しており、filterChain()メソッドは返り値のSecurityFilterChainに係る設定を担います。

・細かい説明は省き、具体的に「ここを変えるとこうできる」箇所を説明していきます。
大まかな作りを抜き出すと、以下のような構成になっています。

http.formLogin(login -> login					// ログインの設定を記述

		).logout(logout -> logout			// ログアウトの設定を記述

		).authorizeHttpRequests(authz -> authz		//URLごとの認可設定を記述

		);

オブジェクトhttpの、formLoginメソッドの中にlogin 、logout、(認証:ログイン・ログアウト)
authorizeHttpRequests()メソッドの中にauthz(認可)をラムダ式(->を使う形の記述法)で記述します。

・認証にあたるlogin、logoutについては、他にもログイン画面を自作して設定したり、ログイン失敗時のリダイレクト先URLなども設定できます。

・認可の部分は簡単に書き換えが可能です。主語、述語のような関係になっており、
 .requestMatchers(“/hello”, “/emp/all”).authenticated()
と書けば、「requestMatchers()に指定したURL」は「認証が必要(ログインしないとアクセスできない)」となり、
 .requestMatchers(“/hello”, “/emp/all”).permitAll()
と書けば、「requestMatchers()に指定したURL」は「認証が不要」となります。

☆ “/emp/*” という形でワイルドカードを使用すると、/emp/から始まるすべてのパスを含めることができます。

以上のファイルをすべて記述した後、アプリケーションを実行して以下の挙動になることを確認しましょう。
①認可に記述したURL(”/hello”, “/emp/all”)にアクセスしようとすると、ログインページに遷移する(未ログインだと閲覧できないこと)。
反対に、その他のURLは未ログインでもアクセスできる。
②ログインページで、DBに登録したユーザー名、パスワード(ハッシュ化前の文字列)を入力すると、ログインでき、”/hello”に遷移する。

Thymeleafでの認証情報の表示・表示内容の切り替え

ログインユーザーの情報を表示したり、認証に応じて表示を変える方法を見ていきます。

thymeleaf-extras-springsecurityの導入

認証情報や認可機能にアクセスするためのThymeleafのダイアレクトが格納されているモジュールを導入します。
プロジェクト直下のpom.xmlに下記を追記しましょう。
<dependencies>タグ内に記述してください。

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



入力が完了したら、プロジェクトを更新します。
(Spring Bootアプリケーションを実行中の場合は終了させてから、プロジェクト名を右クリック>Maven>プロジェクトの更新 をクリック)

Controller、Viewの作成

Controllerは基本的にViewで画面表示するだけの簡素な内容です。
前章の
【com/cmps/spring/controller/AuthController.java】

package com.cmps.spring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class AuthController {
	
	@GetMapping("/auth")
	public String showAuth(Model model) {
	    return "login/auth";
	}
}


【/src/main/resources/templates/login/auth.html】

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>認証情報の表示</title>
</head>
<body>
	<header>
		<h1>認証情報の表示</h1>
	</header>
	<div>
		<div sec:authorize="!isAuthenticated()">
			<p>◆ログインしていません◆</p>
		</div>
		<div sec:authorize="isAuthenticated()">
			<p>◆ログイン中です◆</p>
			<p><ログイン情報><br>
				ユーザー名:<span sec:authentication="principal.user.username"></span><br>
				メールアドレス:<span sec:authentication="principal.user.email"></span></p>
			<form method="post" th:action="@{/logout}">
				<input type="submit" value="ログアウトボタン">
			</form>
			<p th:text="|ようこそ、${#authentication.principal.user.username}さん。|"></p>
		</div>
</body>
</html>

ファイルを作成したら、”/auth”にアクセスして確認してください。認証に応じて(ログイン後と未ログイン)ブラウザへの出力内容が変わります。
なお、Springアプリケーション自体を再起動した場合は未ログイン状態となります。

Thymeleafの記述を見ると、sec:authorizesec:authentication#authentication といった記述が目に付くでしょう。

sec:authorize

認可機能です。
値に isAuthenticated() を記述すると、認証済みかどうかで条件分岐を行います。(th:ifのイメージ)
ログイン済みであれば表示、そうでない場合は非表示になります。
ここでは省きますが、ログイン状態のほかROLE(アカウント区分・役割)による出し分けも可能です。

sec:authentication

認証情報を出力するために使用できます。
値の principal は、認証情報を保持するUserDetailsにあたります。
principalからUserDetailsのメンバにアクセスできますので、principal.userでUserオブジェクト(=Entity)が取得できます。
あとはこれまで扱ったEntityと同様、欲しいフィールド名を指定するとth:textのようにspanタグ内に値が挿入されます。

#authentication

認証情報について、ユーティリティオブジェクトとしてもアクセス可能です。
principalにアクセスできますので、あとはsec:authenticationと同様に使用できます。

参考サイト:
Thymeleaf + Spring Security integration basics
9.2. 認証 — Macchinetta Server Framework (1.x) Development Guideline 1.5.1.RELEASE documentation

Controller側での認証情報の取得・使用方法

Controller内でログインユーザーの情報を取得する方法としては、org.springframework.security.core.annotation.AuthenticationPrincipal アノテーションを使用します。
このアノテーションを使用することでSpring Securityの機能と連携して、ログイン中のユーザー情報を取り出すことが出来ます。
ログイン中のユーザー情報は、認証情報を保持するUserDetailsImplクラスのオブジェクトとして取得します。

使用法としては、以下のようにControllerのアクションメソッドの引数として使用します。

public String sampleFunc(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails) {
    //・・・以下略
}

実践

前章のAuthControllerのメソッドに以下のように追記して試してみましょう。
【com/cmps/spring/controller/AuthController.java】

public class AuthController {
	
	@GetMapping("/auth")
	public String showAuth(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails) {

	    if (userDetails != null) {
                //ログイン中であれば出力
                System.out.println("ユーザー名: " + userDetails.getUsername());
            } else {
                System.out.println("未ログインです。");            
            }

	    return "login/auth";
	}
}

※ログインしていない状態ではUserDetailsImplはnullになりますので、nullの状態でgetterを使用するとエラーになってしまいます。
そのため、この例では条件分岐を入れています。

アノテーションを使う方法のほか、認証情報を取り出す専用のクラス(Component)を作成する方法もあります。
参考サイトではユーザー名を返すメソッドになっていますが、認証情報そのものを返すメソッドを作成すれば、上の例と同じように取得することも可能です。
参考サイト:Spring Security:ログインユーザー名をセッションから取得する方法を紹介!

補足

ここでは基礎的な使い方を紹介しました。
その他にも、自作したログインページを使用したり、ログイン後はログインページにアクセスできないようにすることなどもできます。
必要に応じて調べてみましょう。

参考サイト:
Spring Security 6.0 入門~はじめに
最新の6.0で学ぶ!初めてのひとのためのSpring Security | ドクセル

Spring Security バージョン6でのデータベース認証 – Qiita
Spring Security の仕組みを整理してみた – Qiita
【Spring Security】ユーザー認証のイメージを掴む編

※調べる際の注意
ここで扱った内容は、Spring Framework 6.0、Spring Security 3.0以降のSpring Security6.4以降です。(詳細なver.はpom.xmlの実効POMから確認できます。)
しかし、Spring Security5.5~6.0で記述や仕組みが大きく変わっています。
ver.により、参考にできる記述とできないものがありますので、調べる際はver.をよく確認するようにしましょう。

練習問題

問1:Security Configを編集し、任意のページに認証を実装してください。(ログインしないとアクセスできないページにする)

問2:問1のページに「ログイン中のユーザー名」を表示してください。

タイトルとURLをコピーしました