11.5. Spring Securityチュートリアル¶
11.5.1. はじめに¶
11.5.1.1. このチュートリアルで学ぶこと¶
Spring Securityによる基本的な認証・認可
データベース上のアカウント情報を使用したログイン
認証済みアカウントオブジェクトの取得方法
11.5.1.2. 対象読者¶
チュートリアル(Todoアプリケーション JSP編)またはチュートリアル(Todoアプリケーション Thymeleaf編)を実施ずみ (インフラストラクチャ層の実装としてMyBatis3を使用して実施していること)
Mavenの基本的な操作を理解している
11.5.1.3. 検証環境¶
11.5.2. 作成するアプリケーションの概要¶
ログインページでIDとパスワード指定して、アプリケーションにログインする事ができる。
ログイン処理で必要となるアカウント情報はデータベース上に格納する。
ウェルカムページとアカウント情報表示ページがあり、これらのページはログインしないと閲覧する事ができない。
アプリケーションからログアウトする事ができる。
アプリケーションの概要を以下の図で示す。
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
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
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
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
アプリケーションを動かすために必要な設定(設定ファイル)の解説については、「設定ファイルの解説」を参照されたい。
11.5.4. アプリケーションの作成¶
11.5.4.1. ドメイン層の実装¶
Spring Securityの認証処理は基本的に以下の流れになる。
入力された
username
からユーザー情報を検索する。ユーザー情報が存在する場合、そのユーザー情報がもつパスワードと入力されたパスワードをハッシュ化したものを比較する。
比較結果が一致する場合、認証成功とみなす。
ユーザー情報が見つからない場合やパスワードの比較結果が一致しない場合は認証失敗である。
ドメイン層ではユーザー名からAccountオブジェクトを取得する処理が必要となる。実装は、以下の順に進める。
Domain Object(
Account
)の作成AccountRepository
の作成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.4. 認証サービスの作成¶
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 オブジェクトを取得することができる。 |
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.properties
にdatabase=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;
}
ブランクプロジェクトには以下のように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
<jdbc:initialize-database data-source="dataSource"
ignore-failures="ALL">
<jdbc:script location="classpath:/database/${database}-schema.sql" encoding="UTF-8" />
<jdbc:script location="classpath:/database/${database}-dataload.sql" encoding="UTF-8" />
</jdbc:initialize-database>
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)
);
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)
|
ブランクプロジェクトの設定では、 本チュートリアルでは、 |
11.5.4.1.6. ドメイン層の作成後のパッケージエクスプローラー¶
ドメイン層に作成したファイルを確認する。
Package ExplorerのPackage PresentationはHierarchicalを使用している。
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();
}
}
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(
"/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();
}
}
項番 |
説明 |
---|---|
(1)
|
を設定する。 |
(2)
|
を設定する。 |
(3)
|
を設定する。 ただし、 |
(4)
|
認証処理を行う デフォルトでは、
|
(5)
|
本チュートリアルでは、 |
src/main/resources/META-INF/spring/spring-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
">
<sec:http pattern="/resources/**" request-matcher="ant" security="none"/>
<sec:http request-matcher="ant">
<!-- (1) -->
<sec:form-login login-page="/login/loginForm"
authentication-failure-url="/login/loginForm?error=true" />
<!-- (2) -->
<sec:logout logout-success-url="/" delete-cookies="JSESSIONID" />
<!-- (3) -->
<sec:intercept-url pattern="/login/**"
access="permitAll" />
<sec:intercept-url pattern="/**" access="isAuthenticated()" />
<sec:access-denied-handler ref="accessDeniedHandler"/>
<sec:custom-filter ref="userIdMDCPutFilter" after="ANONYMOUS_FILTER"/>
<sec:session-management />
</sec:http>
<sec:authentication-manager>
<!-- com.example.security.domain.service.userdetails.SampleUserDetailsService
is scanned by component scan with @Service -->
<!-- (4) -->
<sec:authentication-provider
user-service-ref="sampleUserDetailsService">
<!-- (5) -->
<sec:password-encoder ref="passwordEncoder" />
</sec:authentication-provider>
</sec:authentication-manager>
<!-- CSRF Protection -->
<bean id="accessDeniedHandler"
class="org.springframework.security.web.access.DelegatingAccessDeniedHandler">
<constructor-arg index="0">
<map>
<entry
key="org.springframework.security.web.csrf.InvalidCsrfTokenException">
<bean
class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage"
value="/WEB-INF/views/common/error/invalidCsrfTokenError.jsp" />
</bean>
</entry>
<entry
key="org.springframework.security.web.csrf.MissingCsrfTokenException">
<bean
class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage"
value="/WEB-INF/views/common/error/missingCsrfTokenError.jsp" />
</bean>
</entry>
</map>
</constructor-arg>
<constructor-arg index="1">
<bean
class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage"
value="/WEB-INF/views/common/error/accessDeniedError.jsp" />
</bean>
</constructor-arg>
</bean>
<bean id="webSecurityExpressionHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />
<!-- Put UserID into MDC -->
<bean id="userIdMDCPutFilter" class="org.terasoluna.gfw.security.web.logging.UserIdMDCPutFilter">
</bean>
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
">
<sec:http pattern="/resources/**" request-matcher="ant" security="none"/>
<sec:http request-matcher="ant">
<!-- (1) -->
<sec:form-login login-page="/login/loginForm"
authentication-failure-url="/login/loginForm?error=true" />
<!-- (2) -->
<sec:logout logout-success-url="/" delete-cookies="JSESSIONID" />
<!-- (3) -->
<sec:intercept-url pattern="/login/**"
access="permitAll" />
<sec:intercept-url pattern="/**" access="isAuthenticated()" />
<sec:access-denied-handler ref="accessDeniedHandler"/>
<sec:custom-filter ref="userIdMDCPutFilter" after="ANONYMOUS_FILTER"/>
<sec:session-management />
</sec:http>
<sec:authentication-manager>
<!-- com.example.security.domain.service.userdetails.SampleUserDetailsService
is scanned by component scan with @Service -->
<!-- (4) -->
<sec:authentication-provider
user-service-ref="sampleUserDetailsService">
<!-- (5) -->
<sec:password-encoder ref="passwordEncoder" />
</sec:authentication-provider>
</sec:authentication-manager>
<!-- CSRF Protection -->
<bean id="accessDeniedHandler"
class="org.springframework.security.web.access.DelegatingAccessDeniedHandler">
<constructor-arg index="0">
<map>
<entry
key="org.springframework.security.web.csrf.InvalidCsrfTokenException">
<bean
class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage"
value="/common/error/invalidCsrfTokenError" />
</bean>
</entry>
<entry
key="org.springframework.security.web.csrf.MissingCsrfTokenException">
<bean
class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage"
value="/common/error/missingCsrfTokenError" />
</bean>
</entry>
</map>
</constructor-arg>
<constructor-arg index="1">
<bean
class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage"
value="/common/error/accessDeniedError" />
</bean>
</constructor-arg>
</bean>
<bean id="webSecurityExpressionHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />
<!-- Put UserID into MDC -->
<bean id="userIdMDCPutFilter" class="org.terasoluna.gfw.security.web.logging.UserIdMDCPutFilter">
</bean>
</beans>
項番 |
説明 |
---|---|
(1)
|
を設定する。 |
(2)
|
を設定する。 |
(3)
|
を設定する。 ただし、 |
(4)
|
デフォルトでは、
|
(5)
|
本チュートリアルでは、 |
11.5.4.2.2. ログインページを返す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)
|
ログインページである、 |
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> </td>
<td><input name="submit" type="submit" value="Login" /></td>
</tr>
</table>
</form:form>
</div>
</body>
</html>
項番 |
説明 |
---|---|
(1)
|
認証が失敗した場合、 |
(2)
|
共通ライブラリから提供されている 認証が失敗した場合、Spring Securityのデフォルトの設定で使用される、 ここでは、認証エラー時にはリダイレクトするため、認証エラー時に発生した例外オブジェクトは、セッションスコープに格納される。 |
(3)
|
認証処理に必要なパラメータ(ユーザー名とパスワード)をPOSTメソッドで送信する。 |
(4)
|
ユーザー名を指定するテキストボックスを作成する。 Spring Securityのデフォルトのパラメータ名は |
(5)
|
パスワードを指定するテキストボックス(パスワード用のテキストボックス)を作成する。 Spring Securityのデフォルトのパラメータ名は |
src/main/webapp/WEB-INF/views/login/loginForm.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login Page</title>
<link rel="stylesheet" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
<div id="wrapper">
<h3>Login with Username and Password</h3>
<!--/* (1) */-->
<div th:if="${param.containsKey('error')}"
th:with="exception = ${SPRING_SECURITY_LAST_EXCEPTION} ?: ${session[SPRING_SECURITY_LAST_EXCEPTION]}"> <!--/* (2) */-->
<ul th:if="${exception != null}" class="alert alert-error">
<li th:text="${exception.message}"></li>
</ul>
</div>
<!--/* (3) */-->
<form th:action="@{/login}" method="post">
<table>
<tr>
<td><label for="username">User:</label></td>
<td><input type="text" id="username"
name="username" value="demo">(demo)</td> <!--/* (4) */-->
</tr>
<tr>
<td><label for="password">Password:</label></td>
<td><input type="password" id="password"
name="password" value="demo">(demo)</td> <!--/* (5) */-->
</tr>
<tr>
<td> </td>
<td><input name="submit" type="submit" value="Login"></td>
</tr>
</table>
</form>
</div>
</body>
</html>
項番 |
説明 |
---|---|
(1)
|
認証が失敗した場合、 |
(2)
|
認証が失敗した場合、Spring Securityのデフォルトの設定で使用される、 ここでは、認証エラー時にはリダイレクトするため、認証エラー時に発生した例外オブジェクトは、セッションスコープに格納される。 |
(3)
|
認証処理に必要なパラメータ(ユーザー名とパスワード)をPOSTメソッドで送信する。 |
(4)
|
ユーザー名を指定するテキストボックスを作成する。 Spring Securityのデフォルトのパラメータ名は |
(5)
|
パスワードを指定するテキストボックス(パスワード用のテキストボックス)を作成する。 Spring Securityのデフォルトのパラメータ名は |
セッションスコープに格納される認証エラーの例外オブジェクトを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)
|
|
Note
ブランクプロジェクトのデフォルト設定では、JSPからセッションスコープにアクセスできないようになっている。これは、安易にセッションが使用されないようにするためであるが、認証エラーの例外オブジェクトをJSPから取得する場合は、JSPからセッションスコープにアクセスできるようにする必要がある。
Thymeleafでは、include.jspの利用はない。
http://localhost:8080/first-springsecurity/
を入力し、ウェルカムページを表示しようとする。<sec:form-login>
タグのlogin-page
属性またはhttp.formLogin()
のloginPage
の設定値(http://localhost:8080/first-springsecurity/login/loginForm
)に遷移し、以下のような画面が表示される。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)
|
ログインユーザーの |
src/main/webapp/WEB-INF/views/welcome/home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>Home</title>
<link rel="stylesheet"
href="../../../resources/app/css/styles.css" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
<div id="wrapper">
<h1 id="title">Hello world!</h1>
<p th:text="|The time on the server is ${serverTime}.|">The time on the server is 2018/01/01 00:00:00 JST.</p>
<!--/* (1) */-->
<p th:object="${#authentication.principal.account}" th:text="|Welcome *{firstName} *{lastName} !! |"></p>
<ul>
<li><a th:href="@{/account}">view account</a></li>
</ul>
</div>
</body>
</html>
項番 |
説明 |
---|---|
(1)
|
Spring Security Dialectから提供されている ログインユーザーの |
ログインページのLoginボタンを押下し、ウェルカムページを表示する。
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)
|
|
src/main/webapp/WEB-INF/views/welcome/home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>Home</title>
<link rel="stylesheet"
href="../../../resources/app/css/styles.css" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
<div id="wrapper">
<h1 id="title">Hello world!</h1>
<p th:text="|The time on the server is ${serverTime}.|">The time on the server is 2018/01/01 00:00:00 JST.</p>
<p th:object="${#authentication.principal.account}" th:text="|Welcome *{firstName} *{lastName} !! |"></p>
<p>
<!--/* (1) */-->
<form th:action="@{/logout}" method="post">
<button type="submit">Logout</button>
</form>
</p>
<ul>
<li><a th:href="@{/account}">view account</a></li>
</ul>
</div>
</body>
</html>
項番 |
説明 |
---|---|
(1)
|
|
ウェルカムページにLogoutボタンが表示される。
ウェルカムページでLogoutボタンを押下すると、アプリケーションからログアウトする(ログインページが表示される)。
11.5.4.2.6. Controllerからログインユーザーのアカウント情報へアクセス¶
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 に格納する。 |
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>
src/main/webapp/WEB-INF/views/account/view.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>Home</title>
<link rel="stylesheet" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
<div id="wrapper">
<h1>Account Information</h1>
<table th:object="${account}">
<tr>
<th>Username</th>
<td th:text="*{username}"></td>
</tr>
<tr>
<th>First name</th>
<td th:text="*{firstName}"></td>
</tr>
<tr>
<th>Last name</th>
<td th:text="*{lastName}"></td>
</tr>
</table>
</div>
</body>
</html>
ウェルカムページのview accountリンクを押下して、ログインユーザーのアカウント情報表示ページを表示する。
11.5.4.2.7. アプリケーション層の作成後のパッケージエクスプローラー¶
アプリケーション層に作成したファイルを確認する。
Package ExplorerのPackage PresentationはHierarchicalを使用している。
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();
}
}
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(
"/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();
}
}
項番 |
説明 |
---|---|
(1)
|
ブランクプロジェクトのデフォルトの設定では、 |
(2) |
|
(3) |
|
(4)
|
ブランクプロジェクトのデフォルトの設定では、
が設定済みである。 |
(5)
|
Spring Securityの認証ユーザ名をロガーのMDCに格納するためのサーブレットフィルタを有効化する。 この設定を有効化すると、ログに認証ユーザ名が出力されるため、トレーサビリティを向上することができる。 |
(6)
|
使用方法については、「セッション管理機能の適用」を参照されたい。 |
spring-security.xml
には、Spring Securityに関する定義を行う。
作成したブランクプロジェクトのsrc/main/resources/META-INF/spring/spring-security.xml
は、以下のような設定となっている。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
">
<!-- (1) -->
<sec:http pattern="/resources/**" request-matcher="ant" security="none"/>
<sec:http request-matcher="ant">
<!-- (2) -->
<sec:form-login/>
<!-- (3) -->
<sec:logout/>
<!-- (4) -->
<sec:access-denied-handler ref="accessDeniedHandler"/>
<!-- (5) -->
<sec:custom-filter ref="userIdMDCPutFilter" after="ANONYMOUS_FILTER"/>
<!-- (6) -->
<sec:session-management />
<sec:intercept-url pattern="/**" access="permitAll" />
</sec:http>
<!-- (7) -->
<sec:authentication-manager />
<!-- (4) -->
<!-- CSRF Protection -->
<bean id="accessDeniedHandler"
class="org.springframework.security.web.access.DelegatingAccessDeniedHandler">
<constructor-arg index="0">
<map>
<entry
key="org.springframework.security.web.csrf.InvalidCsrfTokenException">
<bean
class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage"
value="/WEB-INF/views/common/error/invalidCsrfTokenError.jsp" />
</bean>
</entry>
<entry
key="org.springframework.security.web.csrf.MissingCsrfTokenException">
<bean
class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage"
value="/WEB-INF/views/common/error/missingCsrfTokenError.jsp" />
</bean>
</entry>
</map>
</constructor-arg>
<constructor-arg index="1">
<bean
class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage"
value="/WEB-INF/views/common/error/accessDeniedError.jsp" />
</bean>
</constructor-arg>
</bean>
<bean id="webSecurityExpressionHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />
<!-- (5) -->
<!-- Put UserID into MDC -->
<bean id="userIdMDCPutFilter" class="org.terasoluna.gfw.security.web.logging.UserIdMDCPutFilter">
</bean>
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
">
<!-- (1) -->
<sec:http pattern="/resources/**" request-matcher="ant" security="none"/>
<sec:http request-matcher="ant">
<!-- (2) -->
<sec:form-login/>
<!-- (3) -->
<sec:logout/>
<!-- (4) -->
<sec:access-denied-handler ref="accessDeniedHandler"/>
<!-- (5) -->
<sec:custom-filter ref="userIdMDCPutFilter" after="ANONYMOUS_FILTER"/>
<!-- (6) -->
<sec:session-management />
<sec:intercept-url pattern="/**" access="permitAll" />
</sec:http>
<!-- (7) -->
<sec:authentication-manager />
<!-- (4) -->
<!-- CSRF Protection -->
<bean id="accessDeniedHandler"
class="org.springframework.security.web.access.DelegatingAccessDeniedHandler">
<constructor-arg index="0">
<map>
<entry
key="org.springframework.security.web.csrf.InvalidCsrfTokenException">
<bean
class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage"
value="/common/error/invalidCsrfTokenError" />
</bean>
</entry>
<entry
key="org.springframework.security.web.csrf.MissingCsrfTokenException">
<bean
class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage"
value="/common/error/missingCsrfTokenError" />
</bean>
</entry>
</map>
</constructor-arg>
<constructor-arg index="1">
<bean
class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage"
value="/common/error/accessDeniedError" />
</bean>
</constructor-arg>
</bean>
<bean id="webSecurityExpressionHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />
<!-- (5) -->
<!-- Put UserID into MDC -->
<bean id="userIdMDCPutFilter" class="org.terasoluna.gfw.security.web.logging.UserIdMDCPutFilter">
</bean>
</beans>
項番 |
説明 |
---|---|
(1)
|
ブランクプロジェクトのデフォルトの設定では、静的リソース(js, css, imageファイルなど)にアクセスするためのURLを認証・認可の対象外にしている。 |
(2) |
|
(3) |
|
(4)
|
ブランクプロジェクトのデフォルトの設定では、
が設定済みである。 |
(5)
|
Spring Securityの認証ユーザ名をロガーのMDCに格納するためのサーブレットフィルタを有効化する。 この設定を有効化すると、ログに認証ユーザ名が出力されるため、トレーサビリティを向上することができる。 |
(6)
|
使用方法については、「セッション管理機能の適用」を参照されたい。 |
(7)
|
使用方法については、「DB認証の適用」を参照されたい。 |
11.5.6.1.2. spring-mvc¶
SpringMvcConfig.java
には、Spring SecurityとSpring MVCを連携するための設定を行う。
src/main/java/com/example/security/config/web/SpringMvcConfig.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<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);
}
}
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<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.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<IDialect> 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);
}
}
項番 |
説明 |
---|---|
(1)
|
|
(2)
|
CSRFトークン値をHTMLフォームに埋め込むための設定。
|
spring-mvc.xml
には、Spring SecurityとSpring MVCを連携するための設定を行う。
src/main/resources/META-INF/spring/spring-mvc.xml
は、以下のような設定となっている。<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
">
<context:property-placeholder
location="classpath*:/META-INF/spring/*.properties" />
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean
class="org.springframework.data.web.PageableHandlerMethodArgumentResolver" />
<!-- (1) -->
<bean
class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
</mvc:argument-resolvers>
</mvc:annotation-driven>
<mvc:default-servlet-handler />
<context:component-scan base-package="com.example.security.app" />
<mvc:resources mapping="/resources/**"
location="/resources/,classpath:META-INF/resources/"
cache-period="#{60 * 60}" />
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/resources/**" />
<bean
class="org.terasoluna.gfw.web.logging.TraceLoggingInterceptor" />
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/resources/**" />
<bean
class="org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor" />
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/resources/**" />
<bean class="org.terasoluna.gfw.web.codelist.CodeListInterceptor">
<property name="codeListIdPattern" value="CL_.+" />
</bean>
</mvc:interceptor>
<!-- REMOVE THIS LINE IF YOU USE JPA
<mvc:interceptor>
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/resources/**" />
<bean
class="org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor" />
</mvc:interceptor>
REMOVE THIS LINE IF YOU USE JPA -->
</mvc:interceptors>
<!-- Settings View Resolver. -->
<mvc:view-resolvers>
<mvc:jsp prefix="/WEB-INF/views/" />
</mvc:view-resolvers>
<bean id="requestDataValueProcessor"
class="org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessor">
<constructor-arg>
<util:list>
<!-- (2) -->
<bean
class="org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor" />
<bean
class="org.terasoluna.gfw.web.token.transaction.TransactionTokenRequestDataValueProcessor" />
</util:list>
</constructor-arg>
</bean>
<!-- Setting Exception Handling. -->
<!-- Exception Resolver. -->
<bean id="systemExceptionResolver"
class="org.terasoluna.gfw.web.exception.SystemExceptionResolver">
<property name="exceptionCodeResolver" ref="exceptionCodeResolver" />
<!-- Setting and Customization by project. -->
<property name="order" value="3" />
<property name="exceptionMappings">
<map>
<entry key="ResourceNotFoundException" value="common/error/resourceNotFoundError" />
<entry key="BusinessException" value="common/error/businessError" />
<entry key="InvalidTransactionTokenException" value="common/error/transactionTokenError" />
<entry key=".DataAccessException" value="common/error/dataAccessError" />
</map>
</property>
<property name="statusCodes">
<map>
<entry key="common/error/resourceNotFoundError" value="404" />
<entry key="common/error/businessError" value="409" />
<entry key="common/error/transactionTokenError" value="409" />
<entry key="common/error/dataAccessError" value="500" />
</map>
</property>
<property name="excludedExceptions">
<array>
</array>
</property>
<property name="defaultErrorView" value="common/error/systemError" />
<property name="defaultStatusCode" value="500" />
</bean>
<!-- Setting AOP. -->
<bean id="handlerExceptionResolverLoggingInterceptor"
class="org.terasoluna.gfw.web.exception.HandlerExceptionResolverLoggingInterceptor">
<property name="exceptionLogger" ref="exceptionLogger" />
</bean>
<aop:config>
<aop:advisor advice-ref="handlerExceptionResolverLoggingInterceptor"
pointcut="execution(* org.springframework.web.servlet.HandlerExceptionResolver.resolveException(..))" />
</aop:config>
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
">
<context:property-placeholder
location="classpath*:/META-INF/spring/*.properties" />
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean
class="org.springframework.data.web.PageableHandlerMethodArgumentResolver" />
<!-- (1) -->
<bean
class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
</mvc:argument-resolvers>
</mvc:annotation-driven>
<mvc:default-servlet-handler />
<context:component-scan base-package="com.example.security.app" />
<mvc:resources mapping="/resources/**"
location="/resources/,classpath:META-INF/resources/"
cache-period="#{60 * 60}" />
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/resources/**" />
<bean
class="org.terasoluna.gfw.web.logging.TraceLoggingInterceptor" />
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/resources/**" />
<bean
class="org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor" />
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/resources/**" />
<bean class="org.terasoluna.gfw.web.codelist.CodeListInterceptor">
<property name="codeListIdPattern" value="CL_.+" />
</bean>
</mvc:interceptor>
<!-- REMOVE THIS LINE IF YOU USE JPA
<mvc:interceptor>
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/resources/**" />
<bean
class="org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor" />
</mvc:interceptor>
REMOVE THIS LINE IF YOU USE JPA -->
</mvc:interceptors>
<!-- Settings View Resolver. -->
<mvc:view-resolvers>
<bean class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<property name="templateEngine" ref="templateEngine" />
<property name="characterEncoding" value="UTF-8" />
<property name="forceContentType" value="true" />
<property name="contentType" value="text/html;charset=UTF-8" />
</bean>
</mvc:view-resolvers>
<!-- TemplateResolver. -->
<bean id="templateResolver"
class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".html" />
<property name="templateMode" value="HTML" />
<property name="characterEncoding" value="UTF-8" />
</bean>
<!-- TemplateEngine. -->
<bean id="templateEngine" class="org.thymeleaf.spring6.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver" />
<property name="enableSpringELCompiler" value="true" />
<property name="additionalDialects">
<set>
<bean class="org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect" />
</set>
</property>
</bean>
<bean id="requestDataValueProcessor"
class="org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessor">
<constructor-arg>
<util:list>
<!-- (2) -->
<bean
class="org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor" />
<bean
class="org.terasoluna.gfw.web.token.transaction.TransactionTokenRequestDataValueProcessor" />
</util:list>
</constructor-arg>
</bean>
<!-- Setting Exception Handling. -->
<!-- Exception Resolver. -->
<bean id="systemExceptionResolver"
class="org.terasoluna.gfw.web.exception.SystemExceptionResolver">
<property name="exceptionCodeResolver" ref="exceptionCodeResolver" />
<!-- Setting and Customization by project. -->
<property name="order" value="3" />
<property name="exceptionMappings">
<map>
<entry key="ResourceNotFoundException" value="common/error/resourceNotFoundError" />
<entry key="BusinessException" value="common/error/businessError" />
<entry key="InvalidTransactionTokenException" value="common/error/transactionTokenError" />
<entry key=".DataAccessException" value="common/error/dataAccessError" />
</map>
</property>
<property name="statusCodes">
<map>
<entry key="common/error/resourceNotFoundError" value="404" />
<entry key="common/error/businessError" value="409" />
<entry key="common/error/transactionTokenError" value="409" />
<entry key="common/error/dataAccessError" value="500" />
</map>
</property>
<property name="excludedExceptions">
<array>
</array>
</property>
<property name="defaultErrorView" value="common/error/systemError" />
<property name="defaultStatusCode" value="500" />
</bean>
<!-- Setting AOP. -->
<bean id="handlerExceptionResolverLoggingInterceptor"
class="org.terasoluna.gfw.web.exception.HandlerExceptionResolverLoggingInterceptor">
<property name="exceptionLogger" ref="exceptionLogger" />
</bean>
<aop:config>
<aop:advisor advice-ref="handlerExceptionResolverLoggingInterceptor"
pointcut="execution(* org.springframework.web.servlet.HandlerExceptionResolver.resolveException(..))" />
</aop:config>
</beans>
項番 |
説明 |
---|---|
(1)
|
|
(2)
|
|
CompositeRequestDataValueProcessor
のコンストラクタにCsrfRequestDataValueProcessor
を指定する。