Spring Securityチュートリアル ================================================================================ .. only:: html .. contents:: 目次 :depth: 3 :local: | はじめに -------------------------------------------------------------------------------- このチュートリアルで学ぶこと ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * Spring Securityによる基本的な認証・認可 * データベース上のアカウント情報を使用したログイン * 認証済みアカウントオブジェクトの取得方法 | 対象読者 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * :doc:`./TutorialTodo`\ または\ :doc:`./TutorialTodoThymeleaf`\ を実施ずみ (インフラストラクチャ層の実装としてMyBatis3を使用して実施していること) * Mavenの基本的な操作を理解している | 検証環境 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * \ :doc:`./TutorialTodo`\ または\ :doc:`./TutorialTodoThymeleaf`\ と同様。 | 作成するアプリケーションの概要 -------------------------------------------------------------------------------- * ログインページでIDとパスワード指定して、アプリケーションにログインする事ができる。 * ログイン処理で必要となるアカウント情報はデータベース上に格納する。 * ウェルカムページとアカウント情報表示ページがあり、これらのページはログインしないと閲覧する事ができない。 * アプリケーションからログアウトする事ができる。 アプリケーションの概要を以下の図で示す。 .. figure:: ./images_TutorialSecurity/security_tutorial_applicatioin_overview.png :width: 90% URL一覧を以下に示す。 .. tabularcolumns:: |p{0.10\linewidth}|p{0.20\linewidth}|p{0.15\linewidth}|p{0.15\linewidth}|p{0.40\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 20 15 15 40 * - 項番 - プロセス名 - HTTPメソッド - URL - 説明 * - 1 - ログインフォーム表示 - GET - /login/loginForm - ログインフォームを表示する * - 2 - ログイン - POST - /authentication - ログインフォームから入力されたユーザー名、パスワードを使って認証する(Spring Securityが行う) * - 3 - ウェルカムページ表示 - GET - / - ウェルカムページを表示する * - 4 - アカウント情報表示 - GET - /account - ログインユーザーのアカウント情報を表示する * - 5 - ログアウト - POST - /logout - ログアウトする(Spring Securityが行う) | 環境構築 -------------------------------------------------------------------------------- プロジェクトの作成 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Mavenのアーキタイプを利用し、\ `TERASOLUNA Server Framework for Java (5.x)のブランクプロジェクト `_\ を作成する。 本チュートリアルでは、MyBatis3用のブランクプロジェクトを作成する。 なお、Spring Tool Suite(STS)へのインポート方法やアプリケーションサーバの起動方法など基本知識については、\ :doc:`./TutorialTodo`\ または\ :doc:`./TutorialTodoThymeleaf`\ で説明済みのため、本チュートリアルでは説明を割愛する。 .. tabs:: .. group-tab:: Java Config .. tabs:: .. group-tab:: JSP .. code-block:: console 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 .. group-tab:: Thymeleaf .. code-block:: console mvn archetype:generate -B^ -DarchetypeGroupId=org.terasoluna.gfw.blank^ -DarchetypeArtifactId=terasoluna-gfw-web-blank-thymeleaf-mybatis3-archetype^ -DarchetypeVersion=5.9.0.RELEASE^ -DgroupId=com.example.security^ -DartifactId=first-springsecurity^ -Dversion=1.0.0-SNAPSHOT .. group-tab:: XML Config .. tabs:: .. group-tab:: JSP .. code-block:: console mvn archetype:generate -B^ -DarchetypeGroupId=org.terasoluna.gfw.blank^ -DarchetypeArtifactId=terasoluna-gfw-web-blank-xmlconfig-jsp-mybatis3-archetype^ -DarchetypeVersion=5.9.0.RELEASE^ -DgroupId=com.example.security^ -DartifactId=first-springsecurity^ -Dversion=1.0.0-SNAPSHOT .. group-tab:: Thymeleaf .. code-block:: console mvn archetype:generate -B^ -DarchetypeGroupId=org.terasoluna.gfw.blank^ -DarchetypeArtifactId=terasoluna-gfw-web-blank-xmlconfig-thymeleaf-mybatis3-archetype^ -DarchetypeVersion=5.9.0.RELEASE^ -DgroupId=com.example.security^ -DartifactId=first-springsecurity^ -Dversion=1.0.0-SNAPSHOT | | チュートリアルを進める上で必要となる設定の多くは、作成したブランクプロジェクトに既に設定済みの状態である。 | チュートリアルを実施するだけであれば、これらの設定の理解は必須ではないが、アプリケーションを動かすためにどのような設定が必要なのかを理解しておくことを推奨する。 アプリケーションを動かすために必要な設定(設定ファイル)の解説については、「\ :ref:`SecurityTutorialAppendixConfigurationFiles`\ 」を参照されたい。 | アプリケーションの作成 -------------------------------------------------------------------------------- ドメイン層の実装 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Spring Securityの認証処理は基本的に以下の流れになる。 #. 入力された\ ``username``\ からユーザー情報を検索する。 #. ユーザー情報が存在する場合、そのユーザー情報がもつパスワードと入力されたパスワードをハッシュ化したものを比較する。 #. 比較結果が一致する場合、認証成功とみなす。 ユーザー情報が見つからない場合やパスワードの比較結果が一致しない場合は認証失敗である。 ドメイン層ではユーザー名からAccountオブジェクトを取得する処理が必要となる。実装は、以下の順に進める。 #. Domain Object(\ ``Account``\ )の作成 #. \ ``AccountRepository``\ の作成 #. \ ``AccountSharedService``\ の作成 | Domain Objectの作成 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" | 認証情報(ユーザー名とパスワード)を保持する\ ``Account``\ クラスを作成する。 | \ ``src/main/java/com/example/security/domain/model/Account.java``\ .. code-block:: 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 + "]"; } } | AccountRepositoryの作成 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" \ ``Account``\ オブジェクトをデータベースから取得する処理を実装する。 | \ ``AccountRepository``\ インタフェースを作成する。 | \ ``src/main/java/com/example/security/domain/repository/account/AccountRepository.java``\ .. code-block:: java package com.example.security.domain.repository.account; import java.util.Optional; import com.example.security.domain.model.Account; public interface AccountRepository { Optional findById(String username); } | | \ ``Account``\ を1件取得するためのSQLをMapperファイルに定義する。 | \ ``src/main/resources/com/example/security/domain/repository/account/AccountRepository.xml``\ .. code-block:: xml | 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``\ .. code-block:: 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``\ .. code-block:: 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); }); } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | ユーザー名に一致する\ ``Account``\ オブジェクトを1件取得する。 | 対象の\ ``Account``\ が存在しない場合は、共通ライブラリから提供している\ ``ResourceNotFoundException``\ をスローする。 | .. _Tutorial_CreateAuthService: 認証サービスの作成 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" | Spring Securityで使用する認証ユーザー情報を保持するクラスを作成する。 | \ ``src/main/java/com/example/security/domain/service/userdetails/SampleUserDetails.java``\ .. code-block:: 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; } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (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``\ .. code-block:: 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) } } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``org.springframework.security.core.userdetails.UserDetailsService``\ インタフェースを実装する。 * - | (2) - | \ ``AccountSharedService``\ をインジェクションする。 * - | (3) - | \ ``username``\ から\ ``Account``\ オブジェクトを取得する処理を\ ``AccountSharedService``\ に委譲する。 * - | (4) - | 取得した\ ``Account``\ オブジェクトを使用して、本プロジェクト用の\ ``UserDetails``\ オブジェクトを作成し、メソッドの返り値として返却する。 * - | (5) - | 対象のユーザーが見つからない場合は、\ ``UsernameNotFoundException``\ がスローする。 | データベースの初期化スクリプトの設定 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 本チュートリアルでは、アカウント情報を保持するデータベースとしてH2 Database(インメモリデータベース)を使用する。 そのため、アプリケーション起動時にSQLを実行してデータベースを初期化する必要がある。 .. tabs:: .. group-tab:: Java Config ブランクプロジェクトには以下のように\ ``DataSourceInitializer`` \ が設定済みであり、\ ``database + "-schema.sql`` \ にDDL文、\ ``database + "-dataload.sql`` \ にDML文を追加するだけでアプリケーション起動時にSQLを実行してデータベースを初期化することができる。なお、ブランクプロジェクトの設定では\ ``first-springsecurity-infra.properties`` \ に\ ``database=H2`` \ と定義されているため、\ ``H2-schema.sql`` \ 及び\ ``H2-dataload.sql`` \ が実行される。 \ ``src/main/java/com/example/security/config/app/FirstSpringSecurityEnvConfig.java``\ .. code-block:: 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; } .. group-tab:: XML Config ブランクプロジェクトには以下のように\ ``jdbc:initialize-database`` \ が設定済みであり、\ ``${database}-schema.sql`` \ にDDL文、\ ``${database}-dataload.sql`` \ にDML文を追加するだけでアプリケーション起動時にSQLを実行してデータベースを初期化することができる。なお、ブランクプロジェクトの設定では\ ``first-springsecurity-infra.properties`` \ に\ ``database=H2`` \ と定義されているため、\ ``H2-schema.sql`` \ 及び\ ``H2-dataload.sql`` \ が実行される。 \ ``src/main/resources/META-INF/spring/first-springsecurity-env.xml``\ .. code-block:: xml | | アカウント情報を保持するテーブルを作成するためのDDL文を作成する。 | \ ``src/main/resources/database/H2-schema.sql``\ .. code-block:: 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``\ .. code-block:: sql INSERT INTO account(username, password, first_name, last_name) VALUES('demo', '{pbkdf2}9cccc80b1782715d013a4db1bd33306e53fc534b5052f9b5ff7f50062f3d6df8d4f3395639686016e5eb803639ca1d10', 'Taro', 'Yamada'); -- (1) COMMIT; .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - ブランクプロジェクトの設定では、\ ``applicationContext.xml``\ または\ ``ApplicationContextConfig.java``\ にパスワードをハッシュ化するためのクラスとしてPbkdf2アルゴリズムでハッシュ化を行う\ ``org.springframework.security.crypto.password.DelegatingPasswordEncoder``\ が設定されている。 本チュートリアルでは、\ ``DelegatingPasswordEncoder``\ を使用してパスワードのハッシュ化を行うため、パスワードには\ ``demo``\ という文字列をPbkdf2アルゴリズムでハッシュ化した文字列を投入する。 | ドメイン層の作成後のパッケージエクスプローラー """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" ドメイン層に作成したファイルを確認する。 Package ExplorerのPackage PresentationはHierarchicalを使用している。 .. tabs:: .. group-tab:: Java Config .. figure:: ./images_TutorialSecurity/security_tutorial-domain-layer-package-explorer_JavaConfig.png :alt: security tutorial domain layer package explorer .. group-tab:: XML Config .. figure:: ./images_TutorialSecurity/security_tutorial-domain-layer-package-explorer_XMLConfig.png :alt: security tutorial domain layer package explorer | アプリケーション層の実装 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Spring Securityの設定 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" \ ``spring-security.xml``\ にSpring Securityによる認証・認可の設定を行う。 本チュートリアルで作成するアプリケーションで扱うURLのパターンを以下に示す。 .. tabularcolumns:: |p{0.30\linewidth}|p{0.70\linewidth}| .. list-table:: :header-rows: 1 :widths: 30 70 * - | URL - | 説明 * - | /login/loginForm - | ログインフォームを表示するためのURL * - | /login/loginForm?error=true - | 認証エラー時に遷移するページ(ログインページ)を表示するためのURL * - | /login - | 認証処理を行うためのURL * - | /logout - | ログアウト処理を行うためのURL * - | / - | ウェルカムページを表示するためのURL * - | /account - | ログインユーザーのアカウント情報を表示するためのURL | .. _Tutorial_setting-spring-security: | ブランクプロジェクトから提供されている設定に加えて、以下の設定を追加する。 .. tabs:: .. group-tab:: Java Config \ ``src/main/java/com/example/security/config/web/SpringSecurityConfig.java``\ .. tabs:: .. group-tab:: JSP .. code-block:: 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, 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(); } } .. group-tab:: Thymeleaf .. code-block:: 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, AccessDeniedHandler> errorHandlers = new LinkedHashMap<>(); // Invalid CSRF authenticator error handler AccessDeniedHandlerImpl invalidCsrfTokenErrorHandler = new AccessDeniedHandlerImpl(); invalidCsrfTokenErrorHandler.setErrorPage( "/common/error/invalidCsrfTokenError"); errorHandlers.put(InvalidCsrfTokenException.class, invalidCsrfTokenErrorHandler); // Missing CSRF authenticator error handler AccessDeniedHandlerImpl missingCsrfTokenErrorHandler = new AccessDeniedHandlerImpl(); missingCsrfTokenErrorHandler.setErrorPage( "/common/error/missingCsrfTokenError"); errorHandlers.put(MissingCsrfTokenException.class, missingCsrfTokenErrorHandler); // Default error handler AccessDeniedHandlerImpl defaultErrorHandler = new AccessDeniedHandlerImpl(); defaultErrorHandler.setErrorPage("/common/error/accessDeniedError"); 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(); } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (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``\ を利用する。 .. group-tab:: XML Config \ ``src/main/resources/META-INF/spring/spring-security.xml``\ .. tabs:: .. group-tab:: JSP .. code-block:: xml .. group-tab:: Thymeleaf .. code-block:: xml .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - \ ````\ タグでログインフォームに関する設定を行う。 \ ````\ タグには、 * \ ``login-page``\ 属性にログインフォームを表示するためのURL * \ ``authentication-failure-url``\ 属性に認証エラー時に遷移するページを表示するためのURL を設定する。 * - | (2) - \ ````\ タグでログアウトに関する設定を行う。 \ ````\ タグには、 * \ ``logout-success-url``\ 属性にログアウト後に遷移するページを表示するためのURL(本チュートリアルではウェルカムページを表示するためのURL) * \ ``delete-cookies``\ 属性にログアウト時に削除するCookie名(本チュートリアルではセッションIDのCookie名) を設定する。 * - | (3) - \ ````\ タグを使用してURL毎の認可設定を行う。 \ ````\ タグには、 * ログインフォームを表示するためのURLには、全てのユーザーのアクセスを許可する\ ``permitAll``\ * 上記以外のURLには、認証済みユーザーのみアクセスを許可する\ ``isAuthenticated()``\ を設定する。 ただし、\ ``/resources/``\ 配下のURLについては、Spring Securityによる認証・認可処理を行わない設定(\ ````\ )が行われているため、全てのユーザーがアクセスすることができる。 * - | (4) - \ ````\ タグを使用して、認証処理を行う\ ``org.springframework.security.authentication.AuthenticationProvider``\ の設定を行う。 デフォルトでは、\ ``UserDetailsService``\ を使用して\ ``UserDetails``\ を取得し、その\ ``UserDetails``\ が持つハッシュ化済みパスワードと、ログインフォームで指定されたパスワードを比較してユーザー認証を行うクラス(\ ``org.springframework.security.authentication.dao.DaoAuthenticationProvider``\ )が使用される。 \ ``user-service-ref``\ 属性に\ ``UserDetailsService``\ インタフェースを実装しているコンポーネントのbean名を指定する。本チュートリアルでは、ドメイン層に作成した\ ``SampleUserDetailsService``\ クラスを設定する。 * - | (5) - \ ````\ タグを使用して、ログインフォームで指定されたパスワードをハッシュ化するためのクラス(PasswordEncoder)の設定を行う。 本チュートリアルでは、\ ``applicationContext.xml``\ に定義されている\ \ ``org.springframework.security.crypto.password.DelegatingPasswordEncoder``\ を利用する。\ | ログインページを返すControllerの作成 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" | ログインページを返すControllerを作成する。 | \ ``src/main/java/com/example/security/app/login/LoginController.java``\ .. code-block:: 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"; } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - ログインページである、\ ``login/loginForm``\ を返す。 | ログインページの作成 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" | ログインページにログインフォームを作成する。 .. tabs:: .. group-tab:: JSP \ ``src/main/webapp/WEB-INF/views/login/loginForm.jsp``\ .. code-block:: jsp Login Page

Login with Username and Password

(demo)
(demo)
 
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - 認証が失敗した場合、\ ``/login/loginForm?error=true``\ が呼び出され、ログインページを表示する。 そのため、認証エラー後の表示の時のみエラーメッセージが表示されるように\ ````\ タグを使用する。 * - | (2) - 共通ライブラリから提供されている\ ````\ タグを使用してエラーメッセージを表示する。 認証が失敗した場合、Spring Securityのデフォルトの設定で使用される、\ ``org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler``\ では、認証エラー時に発生した例外オブジェクトを\ ``SPRING_SECURITY_LAST_EXCEPTION``\ という属性名で、リダイレクト時はセッション、フォワード時はリクエストスコープに格納する。 ここでは、認証エラー時にはリダイレクトするため、認証エラー時に発生した例外オブジェクトは、セッションスコープに格納される。 * - | (3) - \ ````\ タグの\ ``action``\ 属性に、認証処理用のURL(\ ``/login``\ )を設定する。このURLはSpring Securityのデフォルトである。 認証処理に必要なパラメータ(ユーザー名とパスワード)をPOSTメソッドで送信する。 * - | (4) - ユーザー名を指定するテキストボックスを作成する。 Spring Securityのデフォルトのパラメータ名は\ ``username``\ である。 * - | (5) - パスワードを指定するテキストボックス(パスワード用のテキストボックス)を作成する。 Spring Securityのデフォルトのパラメータ名は\ ``password``\ である。 .. group-tab:: Thymeleaf \ ``src/main/webapp/WEB-INF/views/login/loginForm.html``\ .. code-block:: html Login Page

Login with Username and Password

(demo)
(demo)
 
.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - 認証が失敗した場合、\ ``/login/loginForm?error=true``\ が呼び出され、ログインページを表示する。 そのため、認証エラー後の表示の時のみエラーメッセージが表示されるように\ ````\ タグを使用する。 * - | (2) - \ ``th:text``\ を使用してエラーメッセージを表示する。 認証が失敗した場合、Spring Securityのデフォルトの設定で使用される、\ ``org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler``\ では、認証エラー時に発生した例外オブジェクトを\ ``SPRING_SECURITY_LAST_EXCEPTION``\ という属性名で、リダイレクト時はセッション、フォワード時はリクエストスコープに格納する。 ここでは、認証エラー時にはリダイレクトするため、認証エラー時に発生した例外オブジェクトは、セッションスコープに格納される。 * - | (3) - \ ``
``\ タグの\ ``th:action``\ 属性に、認証処理用のURL(\ ``/login``\ )を設定する。このURLはSpring Securityのデフォルトである。 認証処理に必要なパラメータ(ユーザー名とパスワード)をPOSTメソッドで送信する。 * - | (4) - ユーザー名を指定するテキストボックスを作成する。 Spring Securityのデフォルトのパラメータ名は\ ``username``\ である。 * - | (5) - パスワードを指定するテキストボックス(パスワード用のテキストボックス)を作成する。 Spring Securityのデフォルトのパラメータ名は\ ``password``\ である。 | .. tabs:: .. group-tab:: JSP セッションスコープに格納される認証エラーの例外オブジェクトをJSPから取得できるようにする。 \ ``src/main/webapp/WEB-INF/views/common/include.jsp``\ .. code-block:: jsp <%@ page session="true"%> <%@ 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"%> .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (6) - \ ``page``\ ディレクティブの\ ``session``\ 属性を\ ``true``\ にする。 .. note:: ブランクプロジェクトのデフォルト設定では、JSPからセッションスコープにアクセスできないようになっている。これは、安易にセッションが使用されないようにするためであるが、認証エラーの例外オブジェクトをJSPから取得する場合は、JSPからセッションスコープにアクセスできるようにする必要がある。 .. group-tab:: Thymeleaf Thymeleafでは、include.jspの利用はない。 | ブラウザのアドレスバーに\ ``http://localhost:8080/first-springsecurity/``\ を入力し、ウェルカムページを表示しようとする。 | 未ログイン状態のため、\ ````\ タグの\ ``login-page``\ 属性または\ ``http.formLogin()``\ の\ ``loginPage``\ の設定値(\ ``http://localhost:8080/first-springsecurity/login/loginForm``\ )に遷移し、以下のような画面が表示される。 .. figure:: ./images_TutorialSecurity/security_tutorial_login_page.png :width: 80% | Viewファイルからログインユーザーのアカウント情報へアクセス """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" .. tabs:: .. group-tab:: JSP JSPからログインユーザーのアカウント情報にアクセスし、氏名を表示する。 \ ``src/main/webapp/WEB-INF/views/welcome/home.jsp``\ .. code-block:: jsp Home

Hello world!

The time on the server is ${serverTime}.

Welcome ${f:h(account.firstName)} ${f:h(account.lastName)} !!

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ````\ タグを使用して、ログインユーザーの\ ``org.springframework.security.core.Authentication``\ オブジェクトにアクセスする。 | | \ ``property``\ 属性を使用すると\ ``Authentication``\ オブジェクトが保持する任意のプロパティにアクセスする事ができ、アクセスしたプロパティ値は\ ``var``\ 属性を使用して任意のスコープに格納することできる。 | デフォルトではpageスコープが設定され、このJSP内のみで参照可能となる。 | | チュートリアルでは、ログインユーザーの\ ``Account``\ オブジェクトを\ ``account``\ という属性名でpageスコープに格納する。 * - | (2) - ログインユーザーの\ ``Account``\ オブジェクトにアクセスして、\ ``firstName``\ と\ ``lastName``\ を表示する。 .. group-tab:: Thymeleaf | 本ガイドラインでは、HTMLで作成したプロトタイプにThymeleafのタグを付与してテンプレート化したものを、「テンプレートHTML」と呼ぶ。 | テンプレートHTMLからログインユーザーのアカウント情報にアクセスし、氏名を表示する。 \ ``src/main/webapp/WEB-INF/views/welcome/home.html``\ .. code-block:: html Home

Hello world!

The time on the server is 2018/01/01 00:00:00 JST.

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - Spring Security Dialectから提供されている\ ``#authentication``\ を使用して、ログインユーザーの\ ``org.springframework.security.core.Authentication``\ オブジェクトにアクセスする。 ログインユーザーの\ ``Account``\ オブジェクトにアクセスして、\ ``firstName``\ と\ ``lastName``\ を表示する。 | ログインページのLoginボタンを押下し、ウェルカムページを表示する。 .. figure:: ./images_TutorialSecurity/security_tutorial_welcome_page.png :width: 70% | ログアウトボタンの追加 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" | ログアウトするためのボタンを追加する。 .. tabs:: .. group-tab:: JSP \ ``src/main/webapp/WEB-INF/views/welcome/home.jsp``\ .. code-block:: jsp Home

Hello world!

The time on the server is ${serverTime}.

Welcome ${f:h(account.firstName)} ${f:h(account.lastName)} !!

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - \ ````\ タグを使用して、ログアウト用のフォームを追加する。 \ ``action``\ 属性には、ログアウト処理用のURL(\ ``/logout``\ )を指定して、Logoutボタンを追加する。このURLはSpring Securityのデフォルトである。 .. group-tab:: Thymeleaf \ ``src/main/webapp/WEB-INF/views/welcome/home.html``\ .. code-block:: html Home

Hello world!

The time on the server is 2018/01/01 00:00:00 JST.

.. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - \ ``
``\ タグを使用して、ログアウト用のフォームを追加する。 \ ``th:action``\ 属性には、ログアウト処理用のURL( \ ``/logout``\ )を指定して、Logoutボタンを追加する。このURLはSpring Securityのデフォルトである。 | ウェルカムページにLogoutボタンが表示される。 .. figure:: ./images_TutorialSecurity/security_tutorial_add_logout.png :width: 70% ウェルカムページでLogoutボタンを押下すると、アプリケーションからログアウトする(ログインページが表示される)。 .. figure:: ./images_TutorialSecurity/security_tutorial_login_page.png :width: 80% | Controllerからログインユーザーのアカウント情報へアクセス """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" | Controllerからログインユーザーのアカウント情報にアクセスし、アカウント情報をViewに引き渡す。 | \ ``src/main/java/com/example/security/app/account/AccountController.java``\ .. code-block:: 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"; } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - | \ ``@AuthenticationPrincipal``\ アノテーションを指定して、ログインユーザーの\ ``UserDetails``\ オブジェクトを受け取る。 * - | (2) - | \ ``SampleUserDetails``\ オブジェクトが保持している\ ``Account``\ オブジェクトを取得し、Viewに引き渡すために\ ``Model``\ に格納する。 | | Controllerから引き渡されたアカウント情報にアクセスし、アカウント情報を表示する。 .. tabs:: .. group-tab:: JSP \ ``src/main/webapp/WEB-INF/views/account/view.jsp``\ .. code-block:: jsp Home

Account Information

Username ${f:h(account.username)}
First name ${f:h(account.firstName)}
Last name ${f:h(account.lastName)}
.. group-tab:: Thymeleaf \ ``src/main/webapp/WEB-INF/views/account/view.html``\ .. code-block:: html Home

Account Information

Username
First name
Last name
| ウェルカムページのview accountリンクを押下して、ログインユーザーのアカウント情報表示ページを表示する。 .. figure:: ./images_TutorialSecurity/security_tutorial_account_information_page.png :width: 80% | アプリケーション層の作成後のパッケージエクスプローラー """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" アプリケーション層に作成したファイルを確認する。 Package ExplorerのPackage PresentationはHierarchicalを使用している。 .. tabs:: .. group-tab:: JSP .. tabs:: .. group-tab:: Java Config .. figure:: ./images_TutorialSecurity/security_tutorial-application-layer-package-explorer_javaConfig_jsp.png :alt: security tutorial application layer package explorer .. group-tab:: XML Config .. figure:: ./images_TutorialSecurity/security_tutorial-application-layer-package-explorer_XMLConfig_jsp.png :alt: security tutorial application layer package explorer .. group-tab:: Thymeleaf .. tabs:: .. group-tab:: Java Config .. figure:: ./images_TutorialSecurity/security_tutorial-application-layer-package-explorer_javaConfig_thymeleaf.png :alt: security tutorial application layer package explorer .. group-tab:: XML Config .. figure:: ./images_TutorialSecurity/security_tutorial-application-layer-package-explorer_XMLConfig_thymeleaf.png :alt: security tutorial application layer package explorer | おわりに -------------------------------------------------------------------------------- 本チュートリアルでは以下の内容を学習した。 * Spring Securityによる基本的な認証・認可 * 認証ユーザーオブジェクトのカスタマイズ方法 * RepositoryおよびServiceクラスを用いた認証処理の設定 * JSPまたはテンプレートHTMLでログイン済みアカウント情報にアクセスする方法 * Controllerでログイン済みアカウント情報にアクセスする方法 | Appendix -------------------------------------------------------------------------------- .. _SecurityTutorialAppendixConfigurationFiles: 設定ファイルの解説 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Spring Securityを利用するためにどのような設定が必要なのかを理解するために、設定ファイルの解説を行う。 | spring-security """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" .. tabs:: .. group-tab:: Java Config \ ``SpringSecurityConfig.java``\ には、Spring Securityに関する定義を行う。 作成したブランクプロジェクトの\ ``src/main/java/com/example/security/config/web/SpringSecurityConfig.java``\ は、以下のような設定となっている。 .. tabs:: .. group-tab:: JSP .. code-block:: 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, 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(); } } .. group-tab:: Thymeleaf .. code-block:: 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, AccessDeniedHandler> errorHandlers = new LinkedHashMap<>(); // Invalid CSRF authenticator error handler AccessDeniedHandlerImpl invalidCsrfTokenErrorHandler = new AccessDeniedHandlerImpl(); invalidCsrfTokenErrorHandler.setErrorPage( "/common/error/invalidCsrfTokenError"); errorHandlers.put(InvalidCsrfTokenException.class, invalidCsrfTokenErrorHandler); // Missing CSRF authenticator error handler AccessDeniedHandlerImpl missingCsrfTokenErrorHandler = new AccessDeniedHandlerImpl(); missingCsrfTokenErrorHandler.setErrorPage( "/common/error/missingCsrfTokenError"); errorHandlers.put(MissingCsrfTokenException.class, missingCsrfTokenErrorHandler); // Default error handler AccessDeniedHandlerImpl defaultErrorHandler = new AccessDeniedHandlerImpl(); defaultErrorHandler.setErrorPage("/common/error/accessDeniedError"); 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(); } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - \ ``HttpSecurity``\ を使用してHTTPアクセスに対して認証・認可を制御する。 ブランクプロジェクトのデフォルトの設定では、\ ``WebSecurityCustomizer``\ で静的リソース(js, css, imageファイルなど)にアクセスするためのURLを認証・認可の対象外にしている。 * - \ (2) - \ ``http.formLogin()``\ を使用して、フォーム認証を使用したログインに関する動作を制御する。 \ 使用方法については、「\ :ref:`form-login`\ 」 を参照されたい。 * - \ (3) - \ ``http.logout()``\ タグ を使用して、ログアウトに関する動作を制御する。 \ 使用方法については、「\ :ref:`SpringSecurityAuthenticationLogout`\ 」を参照されたい。 * - | (4) - \ ``AccessDeniedHandler``\ を使用して、アクセスを拒否した後の動作を制御する。 ブランクプロジェクトのデフォルトの設定では、 * 不正なCSRFトークンを検知した場合(\ ``InvalidCsrfTokenException``\ が発生した場合)の遷移先 * トークンストアからCSRFトークンが取得できない場合(\ ``MissingCsrfTokenException``\ が発生した場合)の遷移先 * 認可処理でアクセスが拒否された場合(上記以外の\ ``AccessDeniedException``\ が発生した場合)の遷移先 が設定済みである。 * - | (5) - Spring Securityの認証ユーザ名をロガーのMDCに格納するためのサーブレットフィルタを有効化する。 この設定を有効化すると、ログに認証ユーザ名が出力されるため、トレーサビリティを向上することができる。 * - | (6) - \ ``http.sessionManagement()``\ を使用して、Spring Securityのセッション管理方法を制御する。 使用方法については、「\ :ref:`SpringSecuritySessionManagementSetup`\ 」を参照されたい。 .. group-tab:: XML Config \ ``spring-security.xml``\ には、Spring Securityに関する定義を行う。 作成したブランクプロジェクトの\ ``src/main/resources/META-INF/spring/spring-security.xml``\ は、以下のような設定となっている。 .. tabs:: .. group-tab:: JSP .. code-block:: xml .. group-tab:: Thymeleaf .. code-block:: xml .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - \ ````\ タグを使用してHTTPアクセスに対して認証・認可を制御する。 ブランクプロジェクトのデフォルトの設定では、静的リソース(js, css, imageファイルなど)にアクセスするためのURLを認証・認可の対象外にしている。 * - \ (2) - \ ````\ タグを使用して、フォーム認証を使用したログインに関する動作を制御する。 \ 使用方法については、「\ :ref:`form-login`\ 」 を参照されたい。 * - \ (3) - \ ````\ タグ を使用して、ログアウトに関する動作を制御する。 \ 使用方法については、「\ :ref:`SpringSecurityAuthenticationLogout`\ 」 を参照されたい。 * - | (4) - \ ````\ タグを使用して、アクセスを拒否した後の動作を制御する。 ブランクプロジェクトのデフォルトの設定では、 * 不正なCSRFトークンを検知した場合(\ ``InvalidCsrfTokenException``\ が発生した場合)の遷移先 * トークンストアからCSRFトークンが取得できない場合(\ ``MissingCsrfTokenException``\ が発生した場合)の遷移先 * 認可処理でアクセスが拒否された場合(上記以外の\ ``AccessDeniedException``\ が発生した場合)の遷移先 が設定済みである。 * - | (5) - Spring Securityの認証ユーザ名をロガーのMDCに格納するためのサーブレットフィルタを有効化する。 この設定を有効化すると、ログに認証ユーザ名が出力されるため、トレーサビリティを向上することができる。 * - | (6) - \ ````\ タグを使用して、Spring Securityのセッション管理方法を制御する。 使用方法については、「\ :ref:`SpringSecuritySessionManagementSetup`\ 」を参照されたい。 * - | (7) - \ ````\ タグを使用して、認証処理を制御する。 使用方法については、「\ :ref:`AuthenticationProviderConfiguration`\ 」を参照されたい。 | spring-mvc """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" .. tabs:: .. group-tab:: Java Config \ ``SpringMvcConfig.java``\ には、Spring SecurityとSpring MVCを連携するための設定を行う。 | 作成したブランクプロジェクトの\ ``src/main/java/com/example/security/config/web/SpringMvcConfig.java``\ は、以下のような設定となっている。 | Spring Securityと関係のない設定については、説明を割愛する。 .. tabs:: .. group-tab:: JSP .. code-block:: java 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 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); } } .. group-tab:: Thymeleaf .. code-block:: java package com.example.security.config.web; import java.util.HashSet; import java.util.List; import java.util.Properties; import java.util.Set; 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; import org.thymeleaf.dialect.IDialect; import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect; import org.thymeleaf.spring6.SpringTemplateEngine; import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver; import org.thymeleaf.spring6.view.ThymeleafViewResolver; import org.thymeleaf.templateresolver.ITemplateResolver; /** * 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 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.viewResolver(thymeleafViewResolver()); } /** * Configure Thymeleaf bean. * @return Bean of configured ThymeleafViewResolver */ @Bean public ThymeleafViewResolver thymeleafViewResolver() { ThymeleafViewResolver bean = new ThymeleafViewResolver(); bean.setTemplateEngine(templateEngine()); bean.setCharacterEncoding("UTF-8"); bean.setForceContentType(true); bean.setContentType("text/html;charset=UTF-8"); return bean; } /** * Configure ITemplateResolver Bean. * @return Bean of configured SpringResourceTemplateResolver */ @Bean("templateResolver") public ITemplateResolver templateResolver() { SpringResourceTemplateResolver bean = new SpringResourceTemplateResolver(); bean.setPrefix("/WEB-INF/views/"); bean.setSuffix(".html"); bean.setTemplateMode("HTML"); bean.setCharacterEncoding("UTF-8"); return bean; } /** * Configure SpringTemplateEngine Bean. * @return Bean of configured SpringTemplateEngine */ @Bean("templateEngine") public SpringTemplateEngine templateEngine() { SpringTemplateEngine bean = new SpringTemplateEngine(); bean.setTemplateResolver(templateResolver()); bean.setEnableSpringELCompiler(true); Set set = new HashSet<>(); set.add(new SpringSecurityDialect()); bean.setAdditionalDialects(set); return bean; } /** * 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); } } .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - \ ``@AuthenticationPrincipal``\ アノテーションを指定して、ログインユーザーの\ ``UserDetails``\ オブジェクトをControllerの引数として受け取れるようにするための設定。 \ ``WebMvcConfigurationSupport``\ の\ ``addArgumentResolvers``\ メソッドで \ ``AuthenticationPrincipalArgumentResolver``\ を指定する。 * - | (2) - CSRFトークン値をHTMLフォームに埋め込むための設定。 \ ``CompositeRequestDataValueProcessor``\ のコンストラクタに\ ``CsrfRequestDataValueProcessor``\ を指定する。 .. group-tab:: XML Config \ ``spring-mvc.xml``\ には、Spring SecurityとSpring MVCを連携するための設定を行う。 | 作成したブランクプロジェクトの\ ``src/main/resources/META-INF/spring/spring-mvc.xml``\ は、以下のような設定となっている。 | Spring Securityと関係のない設定については、説明を割愛する。 .. tabs:: .. group-tab:: JSP .. code-block:: xml .. group-tab:: Thymeleaf .. code-block:: xml .. tabularcolumns:: |p{0.10\linewidth}|p{0.90\linewidth}| .. list-table:: :header-rows: 1 :widths: 10 90 * - 項番 - 説明 * - | (1) - \ ``@AuthenticationPrincipal``\ アノテーションを指定して、ログインユーザーの\ ``UserDetails``\ オブジェクトをControllerの引数として受け取れるようにするための設定。 \ ````\ タグに\ ``AuthenticationPrincipalArgumentResolver``\ を指定する。 * - | (2) - \ ````\ タグ(JSPタグライブラリ)を使用して、CSRFトークン値をHTMLフォームに埋め込むための設定。 \ ``CompositeRequestDataValueProcessor``\ のコンストラクタに\ ``CsrfRequestDataValueProcessor``\ を指定する。 \ ``CompositeRequestDataValueProcessor``\ のコンストラクタに\ ``CsrfRequestDataValueProcessor``\ を指定する。 .. raw:: latex \newpage