11.4. Spring Securityチュートリアル


11.4.1. はじめに

11.4.1.1. このチュートリアルで学ぶこと

  • Spring Securityによる基本的な認証・認可
  • データベース上のアカウント情報を使用したログイン
  • 認証済みアカウントオブジェクトの取得方法

11.4.1.2. 対象読者


11.4.2. 作成するアプリケーションの概要

  • ログインページでIDとパスワード指定して、アプリケーションにログインする事ができる。
  • ログイン処理で必要となるアカウント情報はデータベース上に格納する。
  • ウェルカムページとアカウント情報表示ページがあり、これらのページはログインしないと閲覧する事ができない。
  • アプリケーションからログアウトする事ができる。

アプリケーションの概要を以下の図で示す。

../_images/security_tutorial_applicatioin_overview.png

URL一覧を以下に示す。

項番 プロセス名 HTTPメソッド URL 説明
1 ログインフォーム表示 GET /login/loginForm ログインフォームを表示する
2 ログイン POST /authentication ログインフォームから入力されたユーザー名、パスワードを使って認証する(Spring Securityが行う)
3 ウェルカムページ表示 GET / ウェルカムページを表示する
4 アカウント情報表示 GET /account ログインユーザーのアカウント情報を表示する
5 ログアウト POST /logout ログアウトする(Spring Securityが行う)

11.4.3. 環境構築

11.4.3.1. プロジェクトの作成

Mavenのアーキタイプを利用し、Macchinetta Server Framework (1.x)のブランクプロジェクトを作成する。

本チュートリアルでは、MyBatis3用のブランクプロジェクトを作成する。

なお、Spring Tool Suite(STS)へのインポート方法やアプリケーションサーバの起動方法など基本知識については、チュートリアル(Todoアプリケーション)で説明済みのため、本チュートリアルでは説明を割愛する。

mvn archetype:generate -B^
 -DarchetypeGroupId=com.github.macchinetta.blank^
 -DarchetypeArtifactId=macchinetta-web-blank-thymeleaf-archetype^
 -DarchetypeVersion=1.9.1.RELEASE^
 -DgroupId=com.example.security^
 -DartifactId=first-springsecurity^
 -Dversion=1.0.0-SNAPSHOT

チュートリアルを進める上で必要となる設定の多くは、作成したブランクプロジェクトに既に設定済みの状態である。
チュートリアルを実施するだけであれば、これらの設定の理解は必須ではないが、アプリケーションを動かすためにどのような設定が必要なのかを理解しておくことを推奨する。

アプリケーションを動かすために必要な設定(設定ファイル)の解説については、「設定ファイルの解説」を参照されたい。


11.4.4. アプリケーションの作成

11.4.4.1. ドメイン層の実装

Spring Securityの認証処理は基本的に以下の流れになる。

  1. 入力されたusernameからユーザー情報を検索する。
  2. ユーザー情報が存在する場合、そのユーザー情報がもつパスワードと入力されたパスワードをハッシュ化したものを比較する。
  3. 比較結果が一致する場合、認証成功とみなす。

ユーザー情報が見つからない場合やパスワードの比較結果が一致しない場合は認証失敗である。

ドメイン層ではユーザー名からAccountオブジェクトを取得する処理が必要となる。実装は、以下の順に進める。

  1. Domain Object(Account)の作成
  2. AccountRepositoryの作成
  3. AccountSharedServiceの作成

11.4.4.1.1. Domain Objectの作成

認証情報(ユーザー名とパスワード)を保持するAccountクラスを作成する。
src/main/java/com/example/security/domain/model/Account.java
package com.example.security.domain.model;

import java.io.Serializable;

public class Account implements Serializable {
    private static final long serialVersionUID = 1L;

    private String username;

    private String password;

    private String firstName;

    private String lastName;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return "Account [username=" + username + ", password=" + password
                + ", firstName=" + firstName + ", lastName=" + lastName + "]";
    }
}

11.4.4.1.2. AccountRepositoryの作成

Accountオブジェクトをデータベースから取得する処理を実装する。

AccountRepositoryインタフェースを作成する。
src/main/java/com/example/security/domain/repository/account/AccountRepository.java
package com.example.security.domain.repository.account;

import com.example.security.domain.model.Account;

public interface AccountRepository {
    Account findById(String username);
}

Accountを1件取得するためのSQLをMapperファイルに定義する。
src/main/resources/com/example/security/domain/repository/account/AccountRepository.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.security.domain.repository.account.AccountRepository">

    <resultMap id="accountResultMap" type="Account">
        <id property="username" column="username" />
        <result property="password" column="password" />
        <result property="firstName" column="first_name" />
        <result property="lastName" column="last_name" />
    </resultMap>

    <select id="findById" parameterType="String" resultMap="accountResultMap">
        SELECT
            username,
            password,
            first_name,
            last_name
        FROM
            account
        WHERE
            username = #{username}
    </select>
</mapper>

11.4.4.1.3. AccountSharedServiceの作成

ユーザー名からAccountオブジェクトを取得する業務処理を実装する。

この処理は、Spring Securityの認証サービスから利用するため、インタフェース名はAccountSharedService、クラス名はAccountSharedServiceImplとする。

Note

本ガイドラインでは、Serviceから別のServiceを呼び出す事を推奨していない。

ドメイン層の処理(Service)を共通化したい場合は、XxxServiceという名前ではなく、Serviceの処理を共通化するためのServiceであることを示すために、XxxSharedServiceという名前にすることを推奨している。

本チュートリアルで作成するアプリケーションでは共通化は必須ではないが、通常のアプリケーションであればアカウント情報を管理する業務のServiceと処理を共通化することが想定される。そのため、本チュートリアルではアカウント情報の取得処理をSharedServiceとして実装する。


AccountSharedServiceインタフェースを作成する。
src/main/java/com/example/security/domain/service/account/AccountSharedService.java
package com.example.security.domain.service.account;

import com.example.security.domain.model.Account;

public interface AccountSharedService {
    Account findOne(String username);
}

AccountSharedServiceImplクラスを作成する。
src/main/java/com/example/security/domain/service/account/AccountSharedServiceImpl.java
package com.example.security.domain.service.account;

import jakarta.inject.Inject;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.terasoluna.gfw.common.exception.ResourceNotFoundException;
import org.terasoluna.gfw.common.message.ResultMessage;
import org.terasoluna.gfw.common.message.ResultMessages;

import com.example.security.domain.model.Account;
import com.example.security.domain.repository.account.AccountRepository;

@Service
public class AccountSharedServiceImpl implements AccountSharedService {
    @Inject
    AccountRepository accountRepository;

    @Transactional(readOnly=true)
    @Override
    public Account findOne(String username) {
        // (1)
        Account account = accountRepository.findById(username);
        // (2)
        if (account == null) {
            ResultMessages messages = ResultMessages.error();
            messages.add(ResultMessage.fromText(
                    "The given account is not found! username=" + username));
            throw new ResourceNotFoundException(messages);
        }
        return account;
    }

}
項番 説明
(1)
ユーザー名に一致するAccountオブジェクトを1件取得する。
(2)
ユーザー名に一致するAccountが存在しない場合は、共通ライブラリから提供しているResourceNotFoundExceptionをスローする。

11.4.4.1.4. 認証サービスの作成

Spring Securityで使用する認証ユーザー情報を保持するクラスを作成する。
src/main/java/com/example/security/domain/service/userdetails/SampleUserDetails.java
package com.example.security.domain.service.userdetails;

import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;

import com.example.security.domain.model.Account;

public class SampleUserDetails extends User { // (1)
    private static final long serialVersionUID = 1L;

    private final Account account; // (2)

    public SampleUserDetails(Account account) {
        // (3)
        super(account.getUsername(), account.getPassword(), AuthorityUtils
                .createAuthorityList("ROLE_USER")); // (4)
        this.account = account;
    }

    public Account getAccount() { // (5)
        return account;
    }

}
項番 説明
(1)
org.springframework.security.core.userdetails.UserDetailsインタフェースを実装する。
ここではUserDetailsを実装したorg.springframework.security.core.userdetails.User クラスを継承し、本プロジェクト用のUserDetailsクラスを実装する。
(2)
Springの認証ユーザークラスに、本プロジェクトのアカウント情報を保持させる。
(3)
Userクラスのコンストラクタを呼び出す。第1引数はユーザー名、第2引数はパスワード、第3引数は権限リストである。
(4)
簡易実装として、ROLE_USERというロールのみ持つ権限を作成する。
(5)
アカウント情報のgetterを用意する。これにより、ログインユーザーのAccountオブジェクトを取得することができる。

Spring Securityで使用する認証ユーザー情報を取得するサービスを作成する。
src/main/java/com/example/security/domain/service/userdetails/SampleUserDetailsService.java
package com.example.security.domain.service.userdetails;

import jakarta.inject.Inject;

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 org.terasoluna.gfw.common.exception.ResourceNotFoundException;

import com.example.security.domain.model.Account;
import com.example.security.domain.service.account.AccountSharedService;

@Service
public class SampleUserDetailsService implements UserDetailsService { // (1)
    @Inject
    AccountSharedService accountSharedService; // (2)

    @Transactional(readOnly=true)
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            Account account = accountSharedService.findOne(username); // (3)
            return new SampleUserDetails(account); // (4)
        } catch (ResourceNotFoundException e) {
            throw new UsernameNotFoundException("user not found", e); // (5)
        }
    }

}
項番 説明
(1)
org.springframework.security.core.userdetails.UserDetailsServiceインタフェースを実装する。
(2)
AccountSharedServiceをインジェクションする。
(3)
usernameからAccountオブジェクトを取得する処理をAccountSharedServiceに委譲する。
(4)
取得したAccountオブジェクトを使用して、本プロジェクト用のUserDetailsオブジェクトを作成し、メソッドの返り値として返却する。
(5)
対象のユーザーが見つからない場合は、UsernameNotFoundExceptionがスローする。

11.4.4.1.5. データベースの初期化スクリプトの設定

本チュートリアルでは、アカウント情報を保持するデータベースとしてH2 Database(インメモリデータベース)を使用する。 そのため、アプリケーション起動時にSQLを実行してデータベースを初期化する必要がある。

ブランクプロジェクトには以下のようにjdbc:initialize-databaseが設定済みであり、${database}-schema.sqlにDDL文、${database}-dataload.sqlにDML文を追加するだけでアプリケーション起動時にSQLを実行してデータベースを初期化することができる。なお、ブランクプロジェクトの設定ではfirst-springsecurity-infra.propertiesdatabase=H2と定義されているため、H2-schema.sql及びH2-dataload.sqlが実行される。
src/main/resources/META-INF/spring/first-springsecurity-env.xml
<jdbc:initialize-database data-source="dataSource"
    ignore-failures="ALL">
    <jdbc:script location="classpath:/database/${database}-schema.sql" encoding="UTF-8" />
    <jdbc:script location="classpath:/database/${database}-dataload.sql" encoding="UTF-8" />
</jdbc:initialize-database>

アカウント情報を保持するテーブルを作成するためのDDL文を作成する。
src/main/resources/database/H2-schema.sql
CREATE TABLE account(
    username varchar(128),
    password varchar(124),
    first_name varchar(128),
    last_name varchar(128),
    constraint pk_tbl_account primary key (username)
);

デモユーザー(username=demo、password=demo)を登録するためのDML文を作成する。
src/main/resources/database/H2-dataload.sql
INSERT INTO account(username, password, first_name, last_name) VALUES('demo', '{pbkdf2@SpringSecurity_v5_8}9cccc80b1782715d013a4db1bd33306e53fc534b5052f9b5ff7f50062f3d6df8d4f3395639686016e5eb803639ca1d10', 'Taro', 'Yamada'); -- (1)
COMMIT;
項番 説明
(1)

ブランクプロジェクトの設定では、applicationContext.xmlにパスワードをハッシュ化するためのクラスとしてPbkdf2アルゴリズムでハッシュ化を行うorg.springframework.security.crypto.password.DelegatingPasswordEncoderが設定されている。

本チュートリアルでは、DelegatingPasswordEncoderを使用してパスワードのハッシュ化を行うため、パスワードにはdemoという文字列をPbkdf2アルゴリズムでハッシュ化した文字列を投入する。


11.4.4.1.6. ドメイン層の作成後のパッケージエクスプローラー

ドメイン層に作成したファイルを確認する。

Package ExplorerのPackage PresentationはHierarchicalを使用している。

security tutorial domain layer package explorer

11.4.4.2. アプリケーション層の実装

11.4.4.2.1. Spring Securityの設定

spring-security.xmlにSpring Securityによる認証・認可の設定を行う。

本チュートリアルで作成するアプリケーションで扱うURLのパターンを以下に示す。

URL
説明
/login/loginForm
ログインフォームを表示するためのURL
/login/loginForm?error=true
認証エラー時に遷移するページ(ログインページ)を表示するためのURL
/login
認証処理を行うためのURL
/logout
ログアウト処理を行うためのURL
/
ウェルカムページを表示するためのURL
/account
ログインユーザーのアカウント情報を表示するためのURL

ブランクプロジェクトから提供されている設定に加えて、以下の設定を追加する。
src/main/resources/META-INF/spring/spring-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:sec="http://www.springframework.org/schema/security"
    xsi:schemaLocation="
        http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
    ">

    <sec:http pattern="/resources/**" request-matcher="ant" security="none"/>
    <sec:http request-matcher="ant">

        <!-- (1) -->
        <sec:form-login login-page="/login/loginForm"
            authentication-failure-url="/login/loginForm?error=true" />
        <!-- (2) -->
        <sec:logout logout-success-url="/" delete-cookies="JSESSIONID" />
        <!-- (3) -->
        <sec:intercept-url pattern="/login/**"
            access="permitAll" />
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <sec:logout/>
        <sec:access-denied-handler ref="accessDeniedHandler"/>
        <sec:custom-filter ref="userIdMDCPutFilter" after="ANONYMOUS_FILTER"/>
        <sec:session-management />
    </sec:http>

    <sec:authentication-manager>
        <!-- com.example.security.domain.service.userdetails.SampleUserDetailsService
          is scanned by component scan with @Service -->
        <!-- (4) -->
        <sec:authentication-provider
            user-service-ref="sampleUserDetailsService">
            <!-- (5) -->
            <sec:password-encoder ref="passwordEncoder" />
        </sec:authentication-provider>
    </sec:authentication-manager>

    <!-- CSRF Protection -->
    <bean id="accessDeniedHandler"
        class="org.springframework.security.web.access.DelegatingAccessDeniedHandler">
        <constructor-arg index="0">
            <map>
                <entry
                    key="org.springframework.security.web.csrf.InvalidCsrfTokenException">
                    <bean
                        class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                        <property name="errorPage"
                            value="/common/error/invalidCsrfTokenError" />
                    </bean>
                </entry>
                <entry
                    key="org.springframework.security.web.csrf.MissingCsrfTokenException">
                    <bean
                        class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                        <property name="errorPage"
                            value="/common/error/missingCsrfTokenError" />
                    </bean>
                </entry>
            </map>
        </constructor-arg>
        <constructor-arg index="1">
            <bean
                class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                <property name="errorPage"
                    value="/common/error/accessDeniedError" />
            </bean>
        </constructor-arg>
    </bean>

    <bean id="webSecurityExpressionHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />

    <!-- Put UserID into MDC -->
    <bean id="userIdMDCPutFilter" class="org.terasoluna.gfw.security.web.logging.UserIdMDCPutFilter">
    </bean>

</beans>
項番 説明
(1)

<sec:form-login>タグでログインフォームに関する設定を行う。

<sec:form-login>タグには、

  • login-page属性にログインフォームを表示するためのURL
  • authentication-failure-url属性に認証エラー時に遷移するページを表示するためのURL

を設定する。

(2)

<sec:logout>タグでログアウトに関する設定を行う。

<sec:logout>タグには、

  • logout-success-url属性にログアウト後に遷移するページを表示するためのURL(本チュートリアルではウェルカムページを表示するためのURL)
  • delete-cookies属性にログアウト時に削除するCookie名(本チュートリアルではセッションIDのCookie名)

を設定する。

(3)

<sec:intercept-url>タグを使用してURL毎の認可設定を行う。

<sec:intercept-url>タグには、

  • ログインフォームを表示するためのURLには、全てのユーザーのアクセスを許可するpermitAll
  • 上記以外のURLには、認証済みユーザーのみアクセスを許可するisAuthenticated()

を設定する。

ただし、/resources/配下のURLについては、Spring Securityによる認証・認可処理を行わない設定(<sec:http pattern="/resources/**" security="none"/>)が行われているため、全てのユーザーがアクセスすることができる。

(4)

<sec:authentication-provider>タグを使用して、認証処理を行うorg.springframework.security.authentication.AuthenticationProviderの設定を行う。

デフォルトでは、UserDetailsServiceを使用してUserDetailsを取得し、そのUserDetailsが持つハッシュ化済みパスワードと、ログインフォームで指定されたパスワードを比較してユーザー認証を行うクラス(org.springframework.security.authentication.dao.DaoAuthenticationProvider)が使用される。

user-service-ref属性にUserDetailsServiceインタフェースを実装しているコンポーネントのbean名を指定する。本チュートリアルでは、ドメイン層に作成したSampleUserDetailsServiceクラスを設定する。

(5)

<sec:password-encoder>タグを使用して、ログインフォームで指定されたパスワードをハッシュ化するためのクラス(PasswordEncoder)の設定を行う。

本チュートリアルでは、applicationContext.xmlに定義されているorg.springframework.security.crypto.password.DelegatingPasswordEncoderを利用する。


11.4.4.2.2. ログインページを返すControllerの作成

ログインページを返すControllerを作成する。
src/main/java/com/example/security/app/login/LoginController.java
package com.example.security.app.login;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/login")
public class LoginController {

    @GetMapping("/loginForm") // (1)
    public String view() {
        return "login/loginForm";
    }
}
項番 説明
(1)
ログインページである、login/loginFormを返す。

11.4.4.2.3. ログインページの作成

ログインページにログインフォームを作成する。
src/main/webapp/WEB-INF/views/login/loginForm.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login Page</title>
<link rel="stylesheet" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
    <div id="wrapper">
        <h3>Login with Username and Password</h3>

        <!--/* (1) */-->
        <div th:if="${param.containsKey('error')}"
        th:with="exception = ${SPRING_SECURITY_LAST_EXCEPTION} ?: ${session[SPRING_SECURITY_LAST_EXCEPTION]}"> <!--/* (2) */-->
            <ul th:if="${exception != null}" class="alert alert-error">
                <li th:text="${exception.message}"></li>
            </ul>
        </div>

        <!--/* (3) */-->
        <form th:action="@{/login}" method="post">
            <table>
                <tr>
                    <td><label for="username">User:</label></td>
                    <td><input type="text" id="username"
                        name="username" value="demo">(demo)</td> <!--/* (4) */-->
                </tr>
                <tr>
                    <td><label for="password">Password:</label></td>
                    <td><input type="password" id="password"
                        name="password" value="demo">(demo)</td> <!--/* (5) */-->
                </tr>
                <tr>
                    <td>&nbsp;</td>
                    <td><input name="submit" type="submit" value="Login"></td>
                </tr>
            </table>
        </form>
    </div>
</body>
</html>
項番 説明
(1)
認証が失敗した場合、/login/loginForm?error=trueが呼び出され、ログインページを表示する。 そのため、認証エラー後の表示の時のみエラーメッセージが表示されるようにth:if属性を使用する。
(2)

エラーメッセージを表示する。

認証が失敗した場合、Spring Securityのデフォルトの設定で使用される、org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandlerでは、認証エラー時に発生した例外オブジェクトをSPRING_SECURITY_LAST_EXCEPTIONという属性名で、リダイレクト時はセッション、フォワード時はリクエスト属性に格納する。

ここでは、認証エラー時にはリダイレクトするため、認証エラー時に発生した例外オブジェクトは、セッションに格納される。

(3)

<form>タグのth:action属性に、認証処理用のURL(/login)を設定する。このURLはSpring Securityのデフォルトである。

認証処理に必要なパラメータ(ユーザー名とパスワード)をPOSTメソッドで送信する。

(4)

ユーザー名を指定するテキストボックスを作成する。

Spring Securityのデフォルトのパラメータ名はusernameである。

(5)

パスワードを指定するテキストボックス(パスワード用のテキストボックス)を作成する。

Spring Securityのデフォルトのパラメータ名はpasswordである。

ブラウザのアドレスバーに http://localhost:8080/first-springsecurity/ を入力し、ウェルカムページを表示しようとする。
未ログイン状態のため、<sec:form-login>タグのlogin-page属性の設定値( http://localhost:8080/first-springsecurity/login/loginForm )に遷移し、以下のような画面が表示される。
../_images/security_tutorial_login_page.png

11.4.4.2.4. ThymeleafのテンプレートHTMLからログインユーザーのアカウント情報へアクセス

本ガイドラインでは、HTMLで作成したプロトタイプにThymeleafのタグを付与してテンプレート化したものを、「テンプレートHTML」と呼ぶ。
テンプレートHTMLからログインユーザーのアカウント情報にアクセスし、氏名を表示する。
src/main/webapp/WEB-INF/views/welcome/home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>Home</title>
<link rel="stylesheet"
    href="../../../resources/app/css/styles.css" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
    <div id="wrapper">
        <h1 id="title">Hello world!</h1>
        <p th:text="|The time on the server is ${serverTime}.|">The time on the server is 2018/01/01 00:00:00 JST.</p>
        <!--/* (1) */-->
        <p th:object="${#authentication.principal.account}" th:text="|Welcome *{firstName} *{lastName} !! |"></p>
        <ul>
            <li><a th:href="@{/account}">view account</a></li>
        </ul>
    </div>
</body>
</html>
項番 説明
(1)

Spring Security Dialectから提供されている#authenticationを使用して、ログインユーザーのorg.springframework.security.core.Authenticationオブジェクトにアクセスする。

ログインユーザーのAccountオブジェクトにアクセスして、firstNamelastNameを表示する。


ログインページのLoginボタンを押下し、ウェルカムページを表示する。

../_images/security_tutorial_welcome_page.png

11.4.4.2.5. ログアウトボタンの追加

ログアウトするためのボタンを追加する。
src/main/webapp/WEB-INF/views/welcome/home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>Home</title>
<link rel="stylesheet"
    href="../../../resources/app/css/styles.css" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
    <div id="wrapper">
        <h1 id="title">Hello world!</h1>
        <p th:text="|The time on the server is ${serverTime}.|">The time on the server is 2018/01/01 00:00:00 JST.</p>
        <p th:object="${#authentication.principal.account}" th:text="|Welcome *{firstName} *{lastName} !! |"></p>
        <p>
            <!--/* (1) */-->
            <form th:action="@{/logout}" method="post">
                <button type="submit">Logout</button>
            </form>
        </p>
        <ul>
            <li><a th:href="@{/account}">view account</a></li>
        </ul>
    </div>
</body>
</html>
項番 説明
(1)

<form>タグを使用して、ログアウト用のフォームを追加する。

th:action 属性には、ログアウト処理用のURL( /logout)を指定して、Logoutボタンを追加する。このURLはSpring Securityのデフォルトである。


ウェルカムページにLogoutボタンが表示される。

../_images/security_tutorial_add_logout.png

ウェルカムページでLogoutボタンを押下すると、アプリケーションからログアウトする(ログインページが表示される)。

../_images/security_tutorial_login_page.png

11.4.4.2.6. Controllerからログインユーザーのアカウント情報へアクセス

Controllerからログインユーザーのアカウント情報にアクセスし、アカウント情報をViewに引き渡す。
src/main/java/com/example/security/app/account/AccountController.java
package com.example.security.app.account;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.security.domain.model.Account;
import com.example.security.domain.service.userdetails.SampleUserDetails;

@Controller
@RequestMapping("account")
public class AccountController {

    @GetMapping
    public String view(
            @AuthenticationPrincipal SampleUserDetails userDetails, // (1)
            Model model) {
        // (2)
        Account account = userDetails.getAccount();
        model.addAttribute(account);
        return "account/view";
    }
}
項番 説明
(1)
@AuthenticationPrincipalアノテーションを指定して、ログインユーザーのUserDetailsオブジェクトを受け取る。
(2)
SampleUserDetailsオブジェクトが保持しているAccountオブジェクトを取得し、Viewに引き渡すためにModelに格納する。

Controllerから引き渡されたアカウント情報にアクセスし、アカウント情報を表示する。
src/main/webapp/WEB-INF/views/account/view.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>Home</title>
<link rel="stylesheet" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
    <div id="wrapper">
        <h1>Account Information</h1>
        <table th:object="${account}">
            <tr>
                <th>Username</th>
                <td th:text="*{username}"></td>
            </tr>
            <tr>
                <th>First name</th>
                <td th:text="*{firstName}"></td>
            </tr>
            <tr>
                <th>Last name</th>
                <td th:text="*{lastName}"></td>
            </tr>
        </table>
    </div>
</body>
</html>

ウェルカムページのview accountリンクを押下して、ログインユーザーのアカウント情報表示ページを表示する。

../_images/security_tutorial_account_information_page.png

11.4.4.2.7. アプリケーション層の作成後のパッケージエクスプローラー

アプリケーション層に作成したファイルを確認する。

Package ExplorerのPackage PresentationはHierarchicalを使用している。

security tutorial application layer package explorer

11.4.5. おわりに

本チュートリアルでは以下の内容を学習した。

  • Spring Securityによる基本的な認証・認可
  • 認証ユーザーオブジェクトのカスタマイズ方法
  • RepositoryおよびServiceクラスを用いた認証処理の設定
  • ThymeleafのテンプレートHTMLからログイン済みアカウント情報にアクセスする方法
  • Controllerでログイン済みアカウント情報にアクセスする方法

11.4.6. Appendix

11.4.6.1. 設定ファイルの解説

Spring Securityを利用するためにどのような設定が必要なのかを理解するために、設定ファイルの解説を行う。

11.4.6.1.1. spring-security.xml

spring-security.xmlには、Spring Securityに関する定義を行う。

作成したブランクプロジェクトのsrc/main/resources/META-INF/spring/spring-security.xmlは、以下のような設定となっている。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:sec="http://www.springframework.org/schema/security"
    xsi:schemaLocation="
        http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
    ">

    <!-- (1) -->
    <sec:http pattern="/resources/**" request-matcher="ant" security="none"/>
    <sec:http request-matcher="ant">
        <!-- (2) -->
        <sec:form-login/>
        <!-- (3) -->
        <sec:logout/>
        <!-- (4) -->
        <sec:access-denied-handler ref="accessDeniedHandler"/>
        <!-- (5) -->
        <sec:custom-filter ref="userIdMDCPutFilter" after="ANONYMOUS_FILTER"/>
        <!-- (6) -->
        <sec:session-management />
        <sec:intercept-url pattern="/**" access="permitAll" />
    </sec:http>

    <!-- (7) -->
    <sec:authentication-manager />

    <!-- (4) -->
    <!-- CSRF Protection -->
    <bean id="accessDeniedHandler"
        class="org.springframework.security.web.access.DelegatingAccessDeniedHandler">
        <constructor-arg index="0">
            <map>
                <entry
                    key="org.springframework.security.web.csrf.InvalidCsrfTokenException">
                    <bean
                        class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                        <property name="errorPage"
                            value="/common/error/invalidCsrfTokenError" />
                    </bean>
                </entry>
                <entry
                    key="org.springframework.security.web.csrf.MissingCsrfTokenException">
                    <bean
                        class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                        <property name="errorPage"
                            value="/common/error/missingCsrfTokenError" />
                    </bean>
                </entry>
            </map>
        </constructor-arg>
        <constructor-arg index="1">
            <bean
                class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                <property name="errorPage"
                    value="/common/error/accessDeniedError" />
            </bean>
        </constructor-arg>
    </bean>

    <bean id="webSecurityExpressionHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />

    <!-- (5) -->
    <!-- Put UserID into MDC -->
    <bean id="userIdMDCPutFilter" class="org.terasoluna.gfw.security.web.logging.UserIdMDCPutFilter">
    </bean>

</beans>
項番 説明
(1)

<sec:http>タグを使用してHTTPアクセスに対して認証・認可を制御する。

ブランクプロジェクトのデフォルトの設定では、静的リソース(js, css, imageファイルなど)にアクセスするためのURLを認証・認可の対象外にしている。

(2)
<sec:form-login>タグを使用して、フォーム認証を使用したログインに関する動作を制御する。
使用方法については、「フォーム認証」 を参照されたい。
(3)
<sec:logout>タグ を使用して、ログアウトに関する動作を制御する。
使用方法については、「ログアウト」 を参照されたい。
(4)

<sec:access-denied-handler>タグを使用して、アクセスを拒否した後の動作を制御する。

ブランクプロジェクトのデフォルトの設定では、

  • 不正なCSRFトークンを検知した場合(InvalidCsrfTokenExceptionが発生した場合)の遷移先
  • トークンストアからCSRFトークンが取得できない場合(MissingCsrfTokenExceptionが発生した場合)の遷移先
  • 認可処理でアクセスが拒否された場合(上記以外のAccessDeniedExceptionが発生した場合)の遷移先

が設定済みである。

(5)
Spring Securityの認証ユーザ名をロガーのMDCに格納するためのサーブレットフィルタを有効化する。
この設定を有効化すると、ログに認証ユーザ名が出力されるため、トレーサビリティを向上することができる。
(6)

<sec:session-management>タグを使用して、Spring Securityのセッション管理方法を制御する。

使用方法については、「セッション管理機能の適用」を参照されたい。

(7)

<sec:authentication-manager>タグを使用して、認証処理を制御する。

使用方法については、「DB認証の適用」を参照されたい。


11.4.6.1.2. spring-mvc.xml

spring-mvc.xmlには、Spring SecurityとSpring MVCを連携するための設定を行う。

作成したブランクプロジェクトのsrc/main/resources/META-INF/spring/spring-mvc.xmlは、以下のような設定となっている。 Spring Securityと関係のない設定については、説明を割愛する。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:util="http://www.springframework.org/schema/util"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd
        http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
    ">

    <context:property-placeholder
        location="classpath*:/META-INF/spring/*.properties" />

    <mvc:annotation-driven>
        <mvc:argument-resolvers>
            <bean
                class="org.springframework.data.web.PageableHandlerMethodArgumentResolver" />
            <!-- (1) -->
            <bean
                class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
        </mvc:argument-resolvers>
    </mvc:annotation-driven>

    <mvc:default-servlet-handler />

    <context:component-scan base-package="com.example.security.app" />

    <mvc:resources mapping="/resources/**"
        location="/resources/,classpath:META-INF/resources/"
        cache-period="#{60 * 60}" />

    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**" />
            <mvc:exclude-mapping path="/resources/**" />
            <bean
                class="org.terasoluna.gfw.web.logging.TraceLoggingInterceptor" />
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/**" />
            <mvc:exclude-mapping path="/resources/**" />
            <bean
                class="org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor" />
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/**" />
            <mvc:exclude-mapping path="/resources/**" />
            <bean class="org.terasoluna.gfw.web.codelist.CodeListInterceptor">
                <property name="codeListIdPattern" value="CL_.+" />
            </bean>
        </mvc:interceptor>
    </mvc:interceptors>

    <!-- Settings View Resolver. -->
    <mvc:view-resolvers>
        <bean class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
            <property name="templateEngine" ref="templateEngine" />
            <property name="characterEncoding" value="UTF-8" />
            <property name="forceContentType" value="true" />
            <property name="contentType" value="text/html;charset=UTF-8" />
        </bean>
    </mvc:view-resolvers>

    <!-- TemplateResolver. -->
    <bean id="templateResolver"
        class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
        <property name="prefix" value="/WEB-INF/views/" />
        <property name="suffix" value=".html" />
        <property name="templateMode" value="HTML" />
        <property name="characterEncoding" value="UTF-8" />
    </bean>

    <!-- TemplateEngine. -->
    <bean id="templateEngine" class="org.thymeleaf.spring6.SpringTemplateEngine">
        <property name="templateResolver" ref="templateResolver" />
        <property name="enableSpringELCompiler" value="true" />
        <property name="additionalDialects">
            <set>
                <!-- (2) -->
                <bean class="org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect" />
            </set>
        </property>
    </bean>

    <bean id="requestDataValueProcessor"
        class="org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessor">
        <constructor-arg>
            <util:list>
                <!-- (3) -->
                <bean
                    class="org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor" />
                <bean
                    class="org.terasoluna.gfw.web.token.transaction.TransactionTokenRequestDataValueProcessor" />
            </util:list>
        </constructor-arg>
    </bean>

    <!-- Setting Exception Handling. -->
    <!-- Exception Resolver. -->
    <bean id="systemExceptionResolver"
        class="org.terasoluna.gfw.web.exception.SystemExceptionResolver">
        <property name="exceptionCodeResolver" ref="exceptionCodeResolver" />
        <!-- Setting and Customization by project. -->
        <property name="order" value="3" />
        <property name="exceptionMappings">
            <map>
                <entry key="ResourceNotFoundException" value="common/error/resourceNotFoundError" />
                <entry key="BusinessException" value="common/error/businessError" />
                <entry key="InvalidTransactionTokenException" value="common/error/transactionTokenError" />
                <entry key=".DataAccessException" value="common/error/dataAccessError" />
            </map>
        </property>
        <property name="statusCodes">
            <map>
                <entry key="common/error/resourceNotFoundError" value="404" />
                <entry key="common/error/businessError" value="409" />
                <entry key="common/error/transactionTokenError" value="409" />
                <entry key="common/error/dataAccessError" value="500" />
            </map>
        </property>
        <property name="excludedExceptions">
            <array>
            </array>
        </property>
        <property name="defaultErrorView" value="common/error/systemError" />
        <property name="defaultStatusCode" value="500" />
    </bean>
    <!-- Setting AOP. -->
    <bean id="handlerExceptionResolverLoggingInterceptor"
        class="org.terasoluna.gfw.web.exception.HandlerExceptionResolverLoggingInterceptor">
        <property name="exceptionLogger" ref="exceptionLogger" />
    </bean>
    <aop:config>
        <aop:advisor advice-ref="handlerExceptionResolverLoggingInterceptor"
            pointcut="execution(* org.springframework.web.servlet.HandlerExceptionResolver.resolveException(..))" />
    </aop:config>

</beans>
項番 説明
(1)

@AuthenticationPrincipalアノテーションを指定して、ログインユーザーのUserDetailsオブジェクトをControllerの引数として受け取れるようにするための設定。

<mvc:argument-resolvers>タグにAuthenticationPrincipalArgumentResolverを指定する。

(2)

テンプレートHTML内で、Spring Securityの認証・認可制御を可能にするための設定。

SpringTemplateEngineadditionalDialectsプロパティにSpringSecurityDialectを指定する。

(3)

CSRFトークン値をHTMLフォームに埋め込むための設定。

CompositeRequestDataValueProcessorのコンストラクタにCsrfRequestDataValueProcessorを指定する。