11.5. Spring Securityチュートリアル


11.5.1. はじめに

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

  • Spring Securityによる基本的な認証・認可

  • データベース上のアカウント情報を使用したログイン

  • 認証済みアカウントオブジェクトの取得方法


11.5.1.2. 対象読者


11.5.1.3. 検証環境


11.5.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.5.3. 環境構築

11.5.3.1. プロジェクトの作成

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

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

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

mvn archetype:generate -B^
 -DarchetypeGroupId=org.terasoluna.gfw.blank^
 -DarchetypeArtifactId=terasoluna-gfw-web-blank-jsp-mybatis3-archetype^
 -DarchetypeVersion=5.9.0.RELEASE^
 -DgroupId=com.example.security^
 -DartifactId=first-springsecurity^
 -Dversion=1.0.0-SNAPSHOT

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

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


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

11.5.4.1. ドメイン層の実装

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

  1. 入力されたusernameからユーザー情報を検索する。

  2. ユーザー情報が存在する場合、そのユーザー情報がもつパスワードと入力されたパスワードをハッシュ化したものを比較する。

  3. 比較結果が一致する場合、認証成功とみなす。

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

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

  1. Domain Object(Account)の作成

  2. AccountRepositoryの作成

  3. AccountSharedServiceの作成


11.5.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.5.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 java.util.Optional;

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

public interface AccountRepository {
    Optional<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.5.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 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;

import jakarta.inject.Inject;

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

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

}

項番

説明

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

11.5.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.5.4.1.5. データベースの初期化スクリプトの設定

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

ブランクプロジェクトには以下のようにDataSourceInitializer が設定済みであり、database + "-schema.sql にDDL文、database + "-dataload.sql にDML文を追加するだけでアプリケーション起動時にSQLを実行してデータベースを初期化することができる。なお、ブランクプロジェクトの設定ではfirst-springsecurity-infra.propertiesdatabase=H2 と定義されているため、H2-schema.sql 及びH2-dataload.sql が実行される。

src/main/java/com/example/security/config/app/FirstSpringSecurityEnvConfig.java

@Bean
public DataSourceInitializer dataSourceInitializer() {
    DataSourceInitializer bean = new DataSourceInitializer();
    bean.setDataSource(dataSource());

    ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
    databasePopulator.addScript(new ClassPathResource("/database/"
            + database + "-schema.sql"));
    databasePopulator.addScript(new ClassPathResource("/database/"
            + database + "-dataload.sql"));
    databasePopulator.setSqlScriptEncoding("UTF-8");
    databasePopulator.setIgnoreFailedDrops(true);
    bean.setDatabasePopulator(databasePopulator);
    return bean;
}

アカウント情報を保持するテーブルを作成するための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}9cccc80b1782715d013a4db1bd33306e53fc534b5052f9b5ff7f50062f3d6df8d4f3395639686016e5eb803639ca1d10', 'Taro', 'Yamada'); -- (1)
COMMIT;

項番

説明

(1)

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

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


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

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

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

security tutorial domain layer package explorer

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

11.5.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/java/com/example/security/config/web/SpringSecurityConfig.java

package com.example.security.config.web;

import java.util.LinkedHashMap;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.terasoluna.gfw.security.web.logging.UserIdMDCPutFilter;

/**
 * Bean definition to configure SpringSecurity.
 */
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {

    /**
     * Configure ignore security pattern.
     * @return Bean of configured {@link WebSecurityCustomizer}
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring().requestMatchers(
                new AntPathRequestMatcher("/resources/**"));
    }

    /**
     * Configure {@link SecurityFilterChain} bean.
     * @param http Builder class for setting up authentication and authorization
     * @return Bean of configured {@link SecurityFilterChain}
     * @throws Exception Exception that occurs when setting HttpSecurity
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // (1)
        http.formLogin(login -> login
                .loginPage("/login/loginForm")
                .loginProcessingUrl("/login")
                .failureUrl("/login/loginForm?error=true"));
        // (2)
        http.logout(logout -> logout
                .logoutSuccessUrl("/")
                .deleteCookies("JSESSIONID"));
        http.exceptionHandling(ex -> ex.accessDeniedHandler(
                accessDeniedHandler()));
        http.addFilterAfter(userIdMDCPutFilter(),
                AnonymousAuthenticationFilter.class);
        http.sessionManagement(Customizer.withDefaults());
        // (3)
        http.authorizeHttpRequests(authz -> authz
                .requestMatchers(new AntPathRequestMatcher("/login/**")).permitAll()
                .requestMatchers(new AntPathRequestMatcher("/**")).authenticated());

        return http.build();
    }

    /**
     * Configure {@link AuthenticationProvider} bean.
     * @param userDetailsService Bean defined within Application
     * @param passwordEncoder Bean defined by ApplicationContext#passwordEncoder
     * @return Bean of configured {@link AuthenticationProvider}
     */
    // (4)
    @Bean
    public AuthenticationProvider authProvider(
            UserDetailsService sampleUserDetailsService,
            @Qualifier("passwordEncoder") PasswordEncoder passwordEncoder) {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(sampleUserDetailsService);
        // (5)
        authProvider.setPasswordEncoder(passwordEncoder);
        return authProvider;
    }

    /**
     * Configure {@link AccessDeniedHandler} bean.
     * @return Bean of configured {@link AccessDeniedHandler}
     */
    @Bean("accessDeniedHandler")
    public AccessDeniedHandler accessDeniedHandler() {
        LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler> errorHandlers = new LinkedHashMap<>();

        // Invalid CSRF authenticator error handler
        AccessDeniedHandlerImpl invalidCsrfTokenErrorHandler = new AccessDeniedHandlerImpl();
        invalidCsrfTokenErrorHandler.setErrorPage(
                "/WEB-INF/views/common/error/invalidCsrfTokenError.jsp");
        errorHandlers.put(InvalidCsrfTokenException.class,
                invalidCsrfTokenErrorHandler);

        // Missing CSRF authenticator error handler
        AccessDeniedHandlerImpl missingCsrfTokenErrorHandler = new AccessDeniedHandlerImpl();
        missingCsrfTokenErrorHandler.setErrorPage(
                "/WEB-INF/views/common/error/missingCsrfTokenError.jsp");
        errorHandlers.put(MissingCsrfTokenException.class,
                missingCsrfTokenErrorHandler);

        // Default error handler
        AccessDeniedHandlerImpl defaultErrorHandler = new AccessDeniedHandlerImpl();
        defaultErrorHandler.setErrorPage(
                "/WEB-INF/views/common/error/accessDeniedError.jsp");

        return new DelegatingAccessDeniedHandler(errorHandlers, defaultErrorHandler);
    }

    /**
     * Configure {@link DefaultWebSecurityExpressionHandler} bean.
     * @return Bean of configured {@link DefaultWebSecurityExpressionHandler}
     */
    @Bean("webSecurityExpressionHandler")
    public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
        return new DefaultWebSecurityExpressionHandler();
    }

    /**
     * Configure {@link UserIdMDCPutFilter} bean.
     * @return Bean of configured {@link UserIdMDCPutFilter}
     */
    @Bean("userIdMDCPutFilter")
    public UserIdMDCPutFilter userIdMDCPutFilter() {
        return new UserIdMDCPutFilter();
    }
}

項番

説明

(1)

http.formLogin()でログインフォームに関する設定を行う。

http.formLogin()には、

  • loginPageにログインフォームを表示するためのURL

  • failureUrlに認証エラー時に遷移するページを表示するためのURL

を設定する。

(2)

http.logout()でログアウトに関する設定を行う。

http.logout()には、

  • logoutSuccessUrlにログアウト後に遷移するページを表示するためのURL(本チュートリアルではウェルカムページを表示するためのURL)

  • deleteCookiesにログアウト時に削除するCookie名(本チュートリアルではセッションIDのCookie名)

を設定する。

(3)

http.authorizeHttpRequests()を使用してURL毎の認可設定を行う。

http.authorizeHttpRequests()には、

  • ログインフォームを表示するためのURLには、全てのユーザーのアクセスを許可するpermitAll

  • 上記以外のURLには、認証済みユーザーのみアクセスを許可するauthenticated

を設定する。

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

(4)

認証処理を行う org.springframework.security.authentication.AuthenticationProviderの設定を行う。

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

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

(5)

setPasswordEncoderメソッドを使用して、ログインフォームで指定されたパスワードをハッシュ化するためのクラス(PasswordEncoder)の設定を行う。

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


11.5.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.5.4.2.3. ログインページの作成

ログインページにログインフォームを作成する。

src/main/webapp/WEB-INF/views/login/loginForm.jsp

<!DOCTYPE html>
<html>
<head>
<title>Login Page</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/resources/app/css/styles.css">
</head>
<body>
    <div id="wrapper">
        <h3>Login with Username and Password</h3>

        <!-- (1) -->
        <c:if test="${param.containsKey('error')}">
            <!-- (2) -->
            <t:messagesPanel messagesType="error"
                messagesAttributeName="SPRING_SECURITY_LAST_EXCEPTION" />
        </c:if>

        <!-- (3) -->
        <form:form action="${pageContext.request.contextPath}/login">
            <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:form>
    </div>
</body>
</html>

項番

説明

(1)

認証が失敗した場合、/login/loginForm?error=trueが呼び出され、ログインページを表示する。 そのため、認証エラー後の表示の時のみエラーメッセージが表示されるように<c:if>タグを使用する。

(2)

共通ライブラリから提供されている<t:messagesPanel>タグを使用してエラーメッセージを表示する。

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

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

(3)

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

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

(4)

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

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

(5)

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

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


セッションスコープに格納される認証エラーの例外オブジェクトをJSPから取得できるようにする。

src/main/webapp/WEB-INF/views/common/include.jsp

<%@ page session="true"%> <!-- (6) -->
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec"%>
<%@ taglib uri="http://terasoluna.org/tags" prefix="t"%>
<%@ taglib uri="http://terasoluna.org/functions" prefix="f"%>

項番

説明

(6)

pageディレクティブのsession属性をtrueにする。

Note

ブランクプロジェクトのデフォルト設定では、JSPからセッションスコープにアクセスできないようになっている。これは、安易にセッションが使用されないようにするためであるが、認証エラーの例外オブジェクトをJSPから取得する場合は、JSPからセッションスコープにアクセスできるようにする必要がある。

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

11.5.4.2.4. Viewファイルからログインユーザーのアカウント情報へアクセス

JSPからログインユーザーのアカウント情報にアクセスし、氏名を表示する。

src/main/webapp/WEB-INF/views/welcome/home.jsp

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Home</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/resources/app/css/styles.css">
</head>

<!-- (1) -->
<sec:authentication property="principal.account" var="account" />

<body>
    <div class="container">
        <div id="wrapper">
            <h1 id="title">Hello world!</h1>
            <p>The time on the server is ${serverTime}.</p>
            <!-- (2) -->
            <p>Welcome ${f:h(account.firstName)} ${f:h(account.lastName)} !!</p>
            <ul>
                <li><a href="${pageContext.request.contextPath}/account">view account</a></li>
            </ul>
        </div>
        <jsp:include page="../layout/footer.jsp" />
    </div>
</body>
</html>

項番

説明

(1)
<sec:authentication>タグを使用して、ログインユーザーのorg.springframework.security.core.Authenticationオブジェクトにアクセスする。

property属性を使用するとAuthenticationオブジェクトが保持する任意のプロパティにアクセスする事ができ、アクセスしたプロパティ値はvar属性を使用して任意のスコープに格納することできる。
デフォルトではpageスコープが設定され、このJSP内のみで参照可能となる。

チュートリアルでは、ログインユーザーのAccountオブジェクトをaccountという属性名でpageスコープに格納する。
(2)

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


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

../_images/security_tutorial_welcome_page.png

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

ログアウトするためのボタンを追加する。

src/main/webapp/WEB-INF/views/welcome/home.jsp

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Home</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/resources/app/css/styles.css">
</head>

<sec:authentication property="principal.account" var="account" />

<body>
    <div class="container">
        <div id="wrapper">
            <h1 id="title">Hello world!</h1>
            <p>The time on the server is ${serverTime}.</p>
            <p>Welcome ${f:h(account.firstName)} ${f:h(account.lastName)} !!</p>
            <p>
                <!-- (1) -->
                <form:form action="${pageContext.request.contextPath}/logout">
                    <button type="submit">Logout</button>
                </form:form>
            </p>
            <ul>
                <li><a href="${pageContext.request.contextPath}/account">view account</a></li>
            </ul>
        </div>
        <jsp:include page="../layout/footer.jsp" />
    </div>
</body>
</html>

項番

説明

(1)

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

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


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

../_images/security_tutorial_add_logout.png

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

../_images/security_tutorial_login_page.png

11.5.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.jsp

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Home</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/resources/app/css/styles.css">
</head>
<body>
    <div id="wrapper">
        <h1>Account Information</h1>
        <table>
            <tr>
                <th>Username</th>
                <td>${f:h(account.username)}</td>
            </tr>
            <tr>
                <th>First name</th>
                <td>${f:h(account.firstName)}</td>
            </tr>
            <tr>
                <th>Last name</th>
                <td>${f:h(account.lastName)}</td>
            </tr>
        </table>
    </div>
</body>
</html>

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

../_images/security_tutorial_account_information_page.png

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

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

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

security tutorial application layer package explorer

11.5.5. おわりに

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

  • Spring Securityによる基本的な認証・認可

  • 認証ユーザーオブジェクトのカスタマイズ方法

  • RepositoryおよびServiceクラスを用いた認証処理の設定

  • JSPまたはテンプレートHTMLでログイン済みアカウント情報にアクセスする方法

  • Controllerでログイン済みアカウント情報にアクセスする方法


11.5.6. Appendix

11.5.6.1. 設定ファイルの解説

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


11.5.6.1.1. spring-security

SpringSecurityConfig.javaには、Spring Securityに関する定義を行う。

作成したブランクプロジェクトのsrc/main/java/com/example/security/config/web/SpringSecurityConfig.javaは、以下のような設定となっている。

package com.example.security.config.web;

import java.util.LinkedHashMap;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.Customizer;
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;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.terasoluna.gfw.security.web.logging.UserIdMDCPutFilter;

/**
 * Bean definition to configure SpringSecurity.
 */
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {

    /**
     * Configure ignore security pattern.
     * @return Bean of configured {@link WebSecurityCustomizer}
     */
    // (1)
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring().requestMatchers(
                new AntPathRequestMatcher("/resources/**"));
    }

    /**
     * Configure {@link SecurityFilterChain} bean.
     * @param http Builder class for setting up authentication and authorization
     * @return Bean of configured {@link SecurityFilterChain}
     * @throws Exception Exception that occurs when setting HttpSecurity
     */
    // (1)
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // (2)
        http.formLogin(Customizer.withDefaults());
        // (3)
        http.logout(Customizer.withDefaults());
        http.exceptionHandling(ex -> ex.accessDeniedHandler(
                accessDeniedHandler()));
        // (5)
        http.addFilterAfter(userIdMDCPutFilter(),
                AnonymousAuthenticationFilter.class);
        // (6)
        http.sessionManagement(Customizer.withDefaults());
        http.authorizeHttpRequests(authz -> authz.requestMatchers(
                new AntPathRequestMatcher("/**")).permitAll());

        return http.build();
    }

    /**
     * Configure {@link AccessDeniedHandler} bean.
     * @return Bean of configured {@link AccessDeniedHandler}
     */
    // (4)
    @Bean("accessDeniedHandler")
    public AccessDeniedHandler accessDeniedHandler() {
        LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler> errorHandlers = new LinkedHashMap<>();

        // Invalid CSRF authenticator error handler
        AccessDeniedHandlerImpl invalidCsrfTokenErrorHandler = new AccessDeniedHandlerImpl();
        invalidCsrfTokenErrorHandler.setErrorPage(
                "/WEB-INF/views/common/error/invalidCsrfTokenError.jsp");
        errorHandlers.put(InvalidCsrfTokenException.class,
                invalidCsrfTokenErrorHandler);

        // Missing CSRF authenticator error handler
        AccessDeniedHandlerImpl missingCsrfTokenErrorHandler = new AccessDeniedHandlerImpl();
        missingCsrfTokenErrorHandler.setErrorPage(
                "/WEB-INF/views/common/error/missingCsrfTokenError.jsp");
        errorHandlers.put(MissingCsrfTokenException.class,
                missingCsrfTokenErrorHandler);

        // Default error handler
        AccessDeniedHandlerImpl defaultErrorHandler = new AccessDeniedHandlerImpl();
        defaultErrorHandler.setErrorPage(
                "/WEB-INF/views/common/error/accessDeniedError.jsp");

        return new DelegatingAccessDeniedHandler(errorHandlers, defaultErrorHandler);
    }

    /**
     * Configure {@link DefaultWebSecurityExpressionHandler} bean.
     * @return Bean of configured {@link DefaultWebSecurityExpressionHandler}
     */
    @Bean("webSecurityExpressionHandler")
    public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
        return new DefaultWebSecurityExpressionHandler();
    }

    /**
     * Configure {@link UserIdMDCPutFilter} bean.
     * @return Bean of configured {@link UserIdMDCPutFilter}
     */
    // (5)
    @Bean("userIdMDCPutFilter")
    public UserIdMDCPutFilter userIdMDCPutFilter() {
        return new UserIdMDCPutFilter();
    }
}

項番

説明

(1)

HttpSecurityを使用してHTTPアクセスに対して認証・認可を制御する。

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

(2)

http.formLogin()を使用して、フォーム認証を使用したログインに関する動作を制御する。 使用方法については、「フォーム認証」 を参照されたい。

(3)

http.logout()タグ を使用して、ログアウトに関する動作を制御する。 使用方法については、「ログアウト」を参照されたい。

(4)

AccessDeniedHandlerを使用して、アクセスを拒否した後の動作を制御する。

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

  • 不正なCSRFトークンを検知した場合(InvalidCsrfTokenExceptionが発生した場合)の遷移先

  • トークンストアからCSRFトークンが取得できない場合(MissingCsrfTokenExceptionが発生した場合)の遷移先

  • 認可処理でアクセスが拒否された場合(上記以外のAccessDeniedExceptionが発生した場合)の遷移先

が設定済みである。

(5)

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

(6)

http.sessionManagement()を使用して、Spring Securityのセッション管理方法を制御する。

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


11.5.6.1.2. spring-mvc

SpringMvcConfig.javaには、Spring SecurityとSpring MVCを連携するための設定を行う。

作成したブランクプロジェクトのsrc/main/java/com/example/security/config/web/SpringMvcConfig.javaは、以下のような設定となっている。
Spring Securityと関係のない設定については、説明を割愛する。
package com.example.security.config.web;

import java.util.List;
import java.util.Properties;
import java.util.regex.Pattern;

import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.io.Resource;
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
import org.springframework.http.HttpStatus;
/* REMOVE THIS LINE IF YOU USE JPA
import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor;
REMOVE THIS LINE IF YOU USE JPA */
import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
/* REMOVE THIS LINE IF YOU USE JPA
import org.springframework.web.context.request.WebRequestInterceptor;
REMOVE THIS LINE IF YOU USE JPA */
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.support.RequestDataValueProcessor;
import org.terasoluna.gfw.common.exception.ExceptionCodeResolver;
import org.terasoluna.gfw.common.exception.ExceptionLogger;
import org.terasoluna.gfw.web.codelist.CodeListInterceptor;
import org.terasoluna.gfw.web.exception.HandlerExceptionResolverLoggingInterceptor;
import org.terasoluna.gfw.web.exception.SystemExceptionResolver;
import org.terasoluna.gfw.web.logging.TraceLoggingInterceptor;
import org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessor;
import org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor;
import org.terasoluna.gfw.web.token.transaction.TransactionTokenRequestDataValueProcessor;

/**
 * Configure SpringMVC.
 */
@ComponentScan(basePackages = { "com.example.security.app" })
@EnableAspectJAutoProxy
@EnableWebMvc
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {

    /**
     * Configure {@link PropertySourcesPlaceholderConfigurer} bean.
     * @param properties Property files to be read
     * @return Bean of configured {@link PropertySourcesPlaceholderConfigurer}
     */
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(
            @Value("classpath*:/META-INF/spring/*.properties") Resource... properties) {
        PropertySourcesPlaceholderConfigurer bean = new PropertySourcesPlaceholderConfigurer();
        bean.setLocations(properties);
        return bean;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addArgumentResolvers(
            List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(pageableHandlerMethodArgumentResolver());
        argumentResolvers.add(authenticationPrincipalArgumentResolver()); // (1)
    }

    /**
     * Configure {@link PageableHandlerMethodArgumentResolver} bean.
     * @return Bean of configured {@link PageableHandlerMethodArgumentResolver}
     */
    @Bean
    public PageableHandlerMethodArgumentResolver pageableHandlerMethodArgumentResolver() {
        return new PageableHandlerMethodArgumentResolver();
    }

    /**
     * Configure {@link AuthenticationPrincipalArgumentResolver} bean.
     * @return Bean of configured {@link AuthenticationPrincipalArgumentResolver}
     */
    // (1)
    @Bean
    public AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver() {
        return new AuthenticationPrincipalArgumentResolver();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void configureDefaultServletHandling(
            DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addResourceHandlers(final ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**").addResourceLocations(
                "/resources/", "classpath:META-INF/resources/").setCachePeriod(
                        60 * 60);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        addInterceptor(registry, traceLoggingInterceptor());
        addInterceptor(registry, transactionTokenInterceptor());
        addInterceptor(registry, codeListInterceptor());
        /* REMOVE THIS LINE IF YOU USE JPA
        addWebRequestInterceptor(registry, openEntityManagerInViewInterceptor());
          REMOVE THIS LINE IF YOU USE JPA */
    }

    /**
     * Common processes used in #addInterceptors.
     * @param registry {@link InterceptorRegistry}
     * @param interceptor {@link HandlerInterceptor}
     */
    private void addInterceptor(InterceptorRegistry registry,
            HandlerInterceptor interceptor) {
        registry.addInterceptor(interceptor).addPathPatterns("/**")
                .excludePathPatterns("/resources/**");
    }

    /* REMOVE THIS LINE IF YOU USE JPA
    /**
     * Common processes used in #addInterceptors.
     * @param registry {@link InterceptorRegistry}
     * @param interceptor {@link WebRequestInterceptor}
     *REMOVE THIS COMMENT IF YOU USE JPA/
    private void addWebRequestInterceptor(InterceptorRegistry registry,
            WebRequestInterceptor interceptor) {
        registry.addWebRequestInterceptor(interceptor).addPathPatterns("/**")
                .excludePathPatterns("/resources/**");
    }
    REMOVE THIS LINE IF YOU USE JPA */

    /**
     * Configure {@link TraceLoggingInterceptor} bean.
     * @return Bean of configured {@link TraceLoggingInterceptor}
     */
    @Bean
    public TraceLoggingInterceptor traceLoggingInterceptor() {
        return new TraceLoggingInterceptor();
    }

    /**
     * Configure {@link TransactionTokenInterceptor} bean.
     * @return Bean of configured {@link TransactionTokenInterceptor}
     */
    @Bean
    public TransactionTokenInterceptor transactionTokenInterceptor() {
        return new TransactionTokenInterceptor();
    }

    /**
     * Configure {@link CodeListInterceptor} bean.
     * @return Bean of configured {@link CodeListInterceptor}
     */
    @Bean
    public CodeListInterceptor codeListInterceptor() {
        CodeListInterceptor codeListInterceptor = new CodeListInterceptor();
        codeListInterceptor.setCodeListIdPattern(Pattern.compile("CL_.+"));
        return codeListInterceptor;
    }

    /* REMOVE THIS LINE IF YOU USE JPA
    /**
     * Configure {@link OpenEntityManagerInViewInterceptor} bean.
     * @return Bean of configured {@link OpenEntityManagerInViewInterceptor}
     *REMOVE THIS COMMENT IF YOU USE JPA/
    @Bean
    public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
        return new OpenEntityManagerInViewInterceptor();
    }
    REMOVE THIS LINE IF YOU USE JPA */

    /**
     * {@inheritDoc}
     */
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.jsp("/WEB-INF/views/", ".jsp");
    }

    /**
     * Configure {@link RequestDataValueProcessor} bean.
     * @return Bean of configured {@link CompositeRequestDataValueProcessor}
     */
    @Bean("requestDataValueProcessor")
    public RequestDataValueProcessor requestDataValueProcessor() {
        return new CompositeRequestDataValueProcessor(csrfRequestDataValueProcessor(), transactionTokenRequestDataValueProcessor()); // (2)
    }

    /**
     * Configure {@link CsrfRequestDataValueProcessor} bean.
     * @return Bean of configured {@link CsrfRequestDataValueProcessor}
     */
    // (2)
    @Bean
    public CsrfRequestDataValueProcessor csrfRequestDataValueProcessor() {
        return new CsrfRequestDataValueProcessor();
    }

    /**
     * Configure {@link TransactionTokenRequestDataValueProcessor} bean.
     * @return Bean of configured {@link TransactionTokenRequestDataValueProcessor}
     */
    @Bean
    public TransactionTokenRequestDataValueProcessor transactionTokenRequestDataValueProcessor() {
        return new TransactionTokenRequestDataValueProcessor();
    }

    /**
     * Configure {@link SystemExceptionResolver} bean.
     * @param exceptionCodeResolver Bean defined by ApplicationContext#exceptionCodeResolver
     * @see com.example.security.config.app.ApplicationContext#exceptionCodeResolver()
     * @return Bean of configured {@link SystemExceptionResolver}
     */
    @Bean("systemExceptionResolver")
    public SystemExceptionResolver systemExceptionResolver(
            ExceptionCodeResolver exceptionCodeResolver) {
        SystemExceptionResolver bean = new SystemExceptionResolver();
        bean.setExceptionCodeResolver(exceptionCodeResolver);
        bean.setOrder(3);

        Properties exceptionMappings = new Properties();
        exceptionMappings.setProperty("ResourceNotFoundException",
                "common/error/resourceNotFoundError");
        exceptionMappings.setProperty("BusinessException",
                "common/error/businessError");
        exceptionMappings.setProperty("InvalidTransactionTokenException",
                "common/error/transactionTokenError");
        exceptionMappings.setProperty(".DataAccessException",
                "common/error/dataAccessError");
        bean.setExceptionMappings(exceptionMappings);

        Properties statusCodes = new Properties();
        statusCodes.setProperty("common/error/resourceNotFoundError", String
                .valueOf(HttpStatus.NOT_FOUND.value()));
        statusCodes.setProperty("common/error/businessError", String.valueOf(
                HttpStatus.CONFLICT.value()));
        statusCodes.setProperty("common/error/transactionTokenError", String
                .valueOf(HttpStatus.CONFLICT.value()));
        statusCodes.setProperty("common/error/dataAccessError", String.valueOf(
                HttpStatus.INTERNAL_SERVER_ERROR.value()));
        bean.setStatusCodes(statusCodes);

        bean.setDefaultErrorView("common/error/systemError");
        bean.setDefaultStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
        return bean;
    }

    /**
     * Configure messages logging AOP.
     * @param exceptionLogger Bean defined by ApplicationContext#exceptionLogger
     * @see com.example.security.config.app.ApplicationContext#exceptionLogger()
     * @return Bean of configured {@link HandlerExceptionResolverLoggingInterceptor}
     */
    @Bean("handlerExceptionResolverLoggingInterceptor")
    public HandlerExceptionResolverLoggingInterceptor handlerExceptionResolverLoggingInterceptor(
            ExceptionLogger exceptionLogger) {
        HandlerExceptionResolverLoggingInterceptor bean = new HandlerExceptionResolverLoggingInterceptor();
        bean.setExceptionLogger(exceptionLogger);
        return bean;
    }

    /**
     * Configure messages logging AOP advisor.
     * @param handlerExceptionResolverLoggingInterceptor Bean defined by #handlerExceptionResolverLoggingInterceptor
     * @see #handlerExceptionResolverLoggingInterceptor(ExceptionLogger)
     * @return Advisor configured for PointCut
     */
    @Bean
    public Advisor handlerExceptionResolverLoggingInterceptorAdvisor(
            HandlerExceptionResolverLoggingInterceptor handlerExceptionResolverLoggingInterceptor) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(
                "execution(* org.springframework.web.servlet.HandlerExceptionResolver.resolveException(..))");
        return new DefaultPointcutAdvisor(pointcut, handlerExceptionResolverLoggingInterceptor);
    }
}

項番

説明

(1)

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

WebMvcConfigurationSupportaddArgumentResolversメソッドで AuthenticationPrincipalArgumentResolverを指定する。

(2)

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

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

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