自己紹介
- opengl-8080
- 主に Qiita で技術メモを書いたり
- 関西の SIer 勤務
今日お話しすること
簡単な Hello World を通じて、 Spring Security の仕組みの基礎的な部分を説明
- どのようなクラスが、どのように連携しあっているのか
- 設定ファイルがどのように関係しているのか
背景
- 個人的に Spring Security の勉強を開始
- ちょっと Hello World を書こうとしたが手こずる
- この設定はなんで必要?
- ・・・と書くとなぜ~~~が有効に?
- この設定って最小限の Hello World で必要?
抽象化された設定
- Spring Security の設定は高度に抽象化されている
- 設定が簡潔になる一方で、裏で何が行われているかが分かりづらい
- 仕組みの理解や、カスタマイズがしづらくなる
※個人の所感です
対象者
Hello World を通じて Spring Security の仕組みを学ぶことで、
- Spring Security をこれから勉強する人の参考に(なれるように)
- 仕組みを知らずに Spring Security を使ってた人の参考に(なれるように)
Hello World アプリ(前提)
Spring Boot は使わない
- Spring Security の基礎を学ぶのが目的
- 素の Spring Security だけを使う
Java Config は使わない
- xml で設定を記述する
- 結局 Java Config もやってることは同じ
Servlet 3.0 の機能は使わない
- Servlet 3.0 の機能を利用すれば web.xml なしで設定が可能
- 仕組みの基礎を学ぶのが目的なので、こちらもやはり使わないようにする
Hello World アプリ(環境構築)
ソース
https://github.com/opengl-8080/spring-security-hello-world
動作確認環境
- Java 1.8.0_121
- Tomcat 8.0.35
war ファイルの入手
-
こちら から
spring-security.warをダウンロード - ソースからビルドする場合は、ソースをダウンロードしてプロジェクトのルートで
gradlew warを実行。
動作確認
- Tomcat に
spring-security.warをデプロイ -
http://localhost:8080/spring-securityにアクセス
Hello World アプリ(動作)
http://localhost:8080/spring-security にアクセス
User, Password に foo と入力してログイン
403 エラーになる
次は bar でログイン
index ページが表示される。
logout ボタンをクリックすればログアウトができる。
Hello World アプリ(実装内容)
ファイル構成
|-build.gradle : Gradle のビルドファイル
|
`-src/main/webapp/
|
|-index.jsp : インデックスページ
|
`-WEB-INF/
|
|-applicationContext.xml : Spring 設定ファイル
|
`-web.xml : Servlet 設定ファイル
- 全4ファイル
- 1つはビルドファイルなので、実際デプロイされるのは3ファイル
- Java ソース一切なしの超シンプル構成
build.gradle
apply plugin: 'war'
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
compileJava.options.encoding = 'UTF-8'
repositories {
mavenCentral()
}
dependencies {
compile 'org.springframework.security:spring-security-web:4.2.1.RELEASE'
compile 'org.springframework.security:spring-security-config:4.2.1.RELEASE'
}
war.baseName = 'spring-security'
task wrapper(type: Wrapper) {
gradleVersion = '3.2.1'
}
index.jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Hello Spring Security!!
</title>
</head>
<body>
<h1>
Hello Spring Security!!
</h1>
<form action="logout" method="post">
<input type="submit"
value="logout" />
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}" />
</form>
</body>
</html>
- ログイン後に表示されるトップページ
- メッセージとログアウトボタンがあるだけ
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<filter>
<filter-name>
springSecurityFilterChain
</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
<filter-mapping>
<filter-name>
springSecurityFilterChain
</filter-name>
<url-pattern>
/*
</url-pattern>
</filter-mapping>
</web-app>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:sec="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<sec:http>
<sec:intercept-url
pattern="/login"
access="permitAll" />
<sec:intercept-url
pattern="/**"
access="isAuthenticated() and hasAuthority('BAR')" />
<sec:form-login />
<sec:logout />
</sec:http>
<sec:authentication-manager>
<sec:authentication-provider>
<sec:user-service>
<sec:user name="foo"
password="foo"
authorities="" />
<sec:user name="bar"
password="bar"
authorities="BAR" />
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
</beans>
Hello World を読み解く
- サーバー起動~リクエスト受付
- Spring Security の入り口
- ログイン処理
- アクセス制御
- ログアウト処理
Hello World を読み解く
- サーバー起動~リクエスト受付
- Spring Security の入り口
- ログイン処理
- アクセス制御
- ログアウト処理
Spring コンテナの初期化
web.xml
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
-
<listener>- Servlet の機能で、アプリケーション起動時に実行する処理を定義できる
-
ContextLoaderListener- Spring コンテナを初期化するクラス
- デフォルトで
XmlWebApplicationContextが使用される - 設定ファイルとして
WEB-INF/applicationContext.xmlが使用される
DelegatingFilterProxy の登録
web.xml
<filter>
<filter-name>
springSecurityFilterChain
</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
<filter-mapping>
<filter-name>
springSecurityFilterChain
</filter-name>
<url-pattern>
/*
</url-pattern>
</filter-mapping>
-
DelegatingFilterProxyという Filter をspringSecurityFilterChainという名前で登録- Filter は Servlet の提供する機能
- リクエストの前後に任意の処理を挟むことができる
-
url-patternに/*を指定して、全てのリクエストを処理
DelegatingFilterProxy の役割
-
DelegatingFilterProxyは、 Spring コンテナから次の条件に一致する Bean を検索する- Bean の名前が自身の Filter 名(
springSecurityFilterChain)と一致する -
javax.servlet.Filterインターフェースを実装している
- Bean の名前が自身の Filter 名(
- 取得した Bean に処理を委譲する
-
DelegatingFilterProxyが処理を委譲したクラスは Spring コンテナから取得した Bean なので、 Spring コンテナの機能を利用することができる- DI
- AOP
- etc...
-
DelegatingFilterProxyの役割は、 Servlet コンテナと Spring コンテナの橋渡し
Spring Security の設定
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:sec="http://www.springframework.org/schema/security"
...>
<sec:http>
<sec:intercept-url
pattern="/login"
access="permitAll" />
<sec:intercept-url
pattern="/**"
access="isAuthenticated() and hasAuthority('BAR')" />
<sec:form-login />
<sec:logout />
</sec:http>
...
-
applicationContext.xmlは Spring の設定ファイル-
<bean>タグを使って Bean を定義する - Spring Security 専用の設定ファイルというわけではない
-
- Spring Security の設定を簡潔に記述できるようにするため、専用のタグが用意されている
- リファレンスでは namespace と呼んでいる
-
xmlns:secで読み込んでいる
<http> が肝
applicationContext.xml
<sec:http>
...
</sec:http>
- この
<http>が Spring Security の設定の肝 - 重要な Bean が大量に登録される
FilterChainProxy
-
<http>タグが登録する Bean の1つ -
Filterインターフェースを継承している -
springSecurityFilterChainという名前で Spring コンテナに登録される -
DelegatingFilterProxyが委譲する Bean の正体
まとめ (サーバー起動~リクエスト受付)
- サーブレットコンテナが起動し、
web.xmlにListenerとして登録されているContextLoaderListenerが実行される。 -
XmlWebApplicationContextのインスタンスが生成され、/WEB-INF/applicationContext.xmlが読み込まれる。 -
<http>タグによってFilterChainProxyのインスタンスが"springSecurityFilterChain"という名前で Spring コンテナに登録される。 -
web.xmlにFilterとして登録されているDelegateFilterProxyが、サーブレットコンテナによって生成される。 - リクエストがあると、
DelegateFilterProxyが呼ばれ、自身の名前("springSecurityFilterChain")で Spring コンテナから Bean を取得する(FilterChainProxyが取得される)。 -
FilterChainProxyに処理が委譲される。
※分かりやすさ優先のため厳密には正しくないところもあります。雰囲気で見てください。
Hello World を読み解く
サーバー起動~リクエスト受付- Spring Security の入り口
- ログイン処理
- アクセス制御
- ログアウト処理
SecurityFilterChain
-
<http>が登録する重要なクラスの1つ - 名前の通り、
Filterを Chain (連鎖)させている -
FilterChainProxyは、受け取ったリクエストをSecurityFilterChainが持つFilter達に委譲する - デフォルトで登録される
Filterの例SecurityContextPersistenceFilterCsrfFilterAnonymousAuthenticationFilterExceptionTranslationFilterFilterSecurityInterceptor- etc...
Spring Security と Filter
- Spring Security は
Filterの組み合わせで実現されている - 機能ごとに
Filterが用意されており、そのFilterをSecurityFilterChainに登録するかどうかで機能の有効・無効を切り替えられる
URL パターンごとの SecurityFilterChain
-
SecurityFilterChainは URL パターン単位で定義することができる-
/api/**にマッチする URL へのアクセスの場合は REST API 用に設定したSecurityFilterChainを、 - それ以外の URL (
/**) へのアクセスは、通常の画面アクセス用に設定したSecurityFilterChainを使用する - といったことができる
-
- 例えば、「Form ログイン」のような機能は REST API でのアクセスでは不要になる
設定ファイルは次のようになる。
applicationContext.xml
<sec:http pattern="/api/**">
...
</sec:http>
<sec:http pattern="/**">
...
</sec:http>
-
<http>タグのpattern属性で指定する
(Ant 形式で指定できる) - 設定は上から順番に適用されるので、より限定的な設定をしているものを上に持ってくる
(/**の設定が/api/**より上にあると、/api/**へのアクセスが先に/**の設定にマッチしてしまう)
まとめ (Spring Security の入り口)
-
<http>は、SecurityFilterChainを Bean として登録する -
SecurityFilterChainは、複数のFilterを保持している - Spring Security は、機能ごとに
Filterが用意されている -
Filterを組み合わせることで、必要な機能だけを持つSecurityFilterChainを定義できる -
SecurityFilterChainは URL パターン単位で定義できるので、- REST API 用の
SecurityFilterChain - 通常の画面アクセス用の
SecurityFilterChain
という設定ができる
- REST API 用の
Hello World を読み解く
サーバー起動~リクエスト受付Spring Security の入り口- ログイン処理
- アクセス制御
- ログアウト処理
Form ログインの有効化
applicationContext.xml
<sec:http>
...
<sec:form-login />
...
</sec:http>
-
<form-login>タグを追加すると、 Form ログインが有効になる - Form ログインで必要になる
FilterがSecurityFilterChainに追加される
デフォルトのログインページ
-
<form-login>のlogin-page属性が指定されていない場合、DefaultLoginPageGeneratingFilterというFilterが登録される -
/loginに GET メソッドのリクエストがあると簡易なログインページを生成して返す - 動作確認をサクッと試したい場合に便利
- Remember-Me 認証を有効にしたら自動的に Remember-Me 用のチェックボックスが追加されたりする
ログインリクエストの処理
- ログイン処理は
UsernamePasswordAuthenticationFilterというFilterによって行われる -
/loginに POST メソッドによるリクエストがくると、認証処理を開始する - ただし、実際の認証処理は
AuthenticationManagerに委譲する
ここから「委譲」がたくさん出てきてややこしいので注意!
AuthenticationManager
applicationContext.xml
<sec:authentication-manager> ★
<sec:authentication-provider>
<sec:user-service>
<sec:user name="foo" ... />
<sec:user name="bar" ... />
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
-
<authentication-manager>タグを宣言することで、AuthenticationManagerの実装クラスであるProviderManagerが Spring コンテナに登録される - 認証処理の入り口となるクラス
- ただし、
ProviderManager自身は認証処理は行わず、AuthenticationProviderに委譲する
ProviderManager と AuthenticationProvider
-
AuthenticationProviderは、認証の種類ごとに多くの実装クラスが存在している -
ProviderManagerはアプリケーションがサポートする認証方式に従い、複数のAuthenticationProviderのインスタンスを保持している - それぞれの
AuthenticationProviderに対して、現在の認証リクエストがサポート対象かどうかを判断させ、サポートしている場合にAuthenticationProviderによる認証処理が行われる -
ProviderManagerは、複数のAuthenticationProviderをまとめあげ管理する役割を担っている(まさにProviderManager)
パスワード認証を行う AuthenticationProvider
applicationContext.xml
<sec:authentication-manager>
<sec:authentication-provider> ★
<sec:user-service>
<sec:user name="foo" ... />
<sec:user name="bar" ... />
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
-
<authentication-provider>タグを使用することで、DaoAuthenticationProviderというクラスがAuthenticationProviderの実装として登録される -
DaoAuthenticationProviderは、ユーザー名とパスワードの組み合わせを使った認証処理を行う - その際、ユーザー情報の取得に Dao (Data Access Object) を使用する
- Dao に当たる部分は、
UserDetailsServiceに委譲する
UserDetailsService
UserDetailsService
public interface UserDetailsService {
UserDetails
loadUserByUsername(String username)
throws UsernameNotFoundException;
}
- ユーザー名をもとにユーザー情報(
UserDetails)を返すメソッドを持つ - ユーザーが見つからなかった場合は
UsernameNotFoundExceptionをスローする - Spring Security にはこのインターフェースを実装したクラスがいくつか用意されている
InMemoryUserDetailsManager
メモリ内にユーザー情報を保存しておく実装
JdbcUserDetailsManager
JDBC 経由でデータベースからユーザー情報を検索してくる実装
InMemoryUserDetailsManager を使用する
applicationContext.xml
<sec:authentication-manager>
<sec:authentication-provider>
<sec:user-service> ★
<sec:user name="foo" ... />
<sec:user name="bar" ... />
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
-
<user-service>タグを使用すると、UserDetailsServiceの実装としてInMemoryUserDetailsManagerが Spring コンテナに登録される
UserDetails
UserDetails.java
public interface UserDetails extends Serializable {
String getUsername();
String getPassword();
Collection<? extends GrantedAuthority>
getAuthorities();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
-
UserDetailsは、ログインユーザーの詳細情報を提供するインターフェース - ユーザー名やパスワード、付与された権限のコレクション、期限切れやロックなどの状態を取得できる
- このインターフェースを実装したクラスとして、
Userというクラスが用意されている
User を使用する
applicationContext.xml
<sec:authentication-manager>
<sec:authentication-provider>
<sec:user-service>
<sec:user name="foo" ... /> ★
<sec:user name="bar" ... /> ★
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
-
<user>タグを使用すると、Userクラスを使ってユーザー情報が生成される - 生成されたユーザー情報は、
<user-service>すなわちInMemoryUserDetailsManagerに保存される
ここまでの流れを整理
-
/loginに POST リクエストがくると、UsernamePasswordAuthenticationFilterが認証処理を行う - 認証処理は
AuthenticationManagerに委譲される -
AuthenticationManagerの実装クラスであるProviderManagerは、所有するAuthenticationProviderに認証処理を委譲する -
<authentication-provider>タグで登録されたDaoAuthenticationProviderは、ユーザー名とパスワードをもとに認証処理を行う - その際、ユーザー情報の検索は
UserDetailsServiceに委譲する -
<user-service>タグで登録されたInMemoryUserDetailsManagerは、<user>タグで定義されたユーザー情報(UserDetails)をメモリ上に保存している
上記の関係性が、下の設定に込められている。
applicationContext.xml
<sec:authentication-manager>
<sec:authentication-provider>
<sec:user-service>
<sec:user name="foo" ... />
<sec:user name="bar" ... />
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
認証に成功したら
AuthenticationProvider.java
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
-
AuthenticationProviderの認証処理が成功すると、ログインしたユーザーの情報を保持したAuthenticationオブジェクトが返却される - この
Authenticationオブジェクトはセッションに保存され、次回以降のリクエストなどで参照されるようになる
まとめ (ログイン処理)
-
<form-login>で Form 認証が有効になる -
UsernamePasswordAuthenticationFilterが追加され、認証処理を開始する -
AuthenticationManagerは認証処理を制御する -
AuthenticationProviderが具体的な認証処理を行う -
UserDetailsServiceはユーザー情報を検索する Dao -
UserDetailsはユーザーの詳細情報を提供する - 認証に成功すると、
Authenticationオブジェクトがセッションに保存される
Hello World を読み解く
サーバー起動~リクエスト受付Spring Security の入り口ログイン処理- アクセス制御
- ログアウト処理
FilterSecurityInterceptor
-
<http>タグを定義することで追加される重要なFilterの1つ - セキュアオブジェクトの処理を実行する前後に処理を挟む
- HTTP リクエストに関するアクセス制御は、この
FilterSecurityInterceptorの中で開始される
セキュアオブジェクト
- セキュリティ保護対象 のことを、 Spring Security のリファレンスでは セキュアオブジェクト(Secure Object) と呼んでいる
- 「Object」とあるが、特定の Java オブジェクトのことではなく、「対象」という本来の Object の意味で使われている(たぶん)
- 「特定の URL への HTTP リクエスト」や「メソッドの実行」などがセキュアオブジェクトにあたる
アクセス制御の委譲
-
FilterSecurityInterceptor自身はアクセス制御についてのチェックは行わない - アクセス制御の判定は
AccessDecisionManagerに委譲する
投票によるアクセス制御
- Spring Security が提供する
AccessDecisionManagerの実装クラスは、「投票」によってアクセス制御を行う -
AccessDecisionManagerはAccessDecisionVoterにアクセスの可否を投票させる- 付与:アクセス可
- 拒否:アクセス不可
- 棄権:サポート対象外
-
AccessDecisionManagerは、投票結果を集計して結論を出す- 可の場合は何もしない
- 不可の場合は
AccessDeniedExceptionをスローする
AccessDecisionManager の実装クラス
-
AccessDecisionManagerには実装クラスが3つ用意されている -
AffirmativeBased- 「付与」が1つでもあればアクセス可
- デフォルトはこのクラスが使用される
-
ConsensusBased- 「拒否」<「付与」の場合はアクセス可
-
UnanimousBased- 全て「付与」の場合はアクセス可
式ベースのアクセス制御
applicationContext.xml
<sec:http>
<sec:intercept-url
pattern="/login"
access="permitAll" />
<sec:intercept-url
pattern="/**"
access="isAuthenticated() and hasAuthority('BAR')" />
...
</sec:http>
-
AccessDecisionVoterの実装には、デフォルトではWebExpressionVoterが使用される -
Expressionすなわち式ベースでアクセス可否を制御する - アクセス制御の式は
<intercept-url>タグのaccess属性で指定する -
pattern属性には、その制御を適用する URL のパターンを指定する
Spring Expression Language (SpEL)
applicationContext.xml
access="permitAll"
access="isAuthenticated() and hasAuthority('BAR')"
- アクセス制御の式には Spring Expression Language という Spring 独自の式言語を使用する
- 式を評価した結果が
booleanになるようにする-
trueならアクセス可 -
falseならアクセス不可
-
- Spring Security 用に関数や定数が拡張されている
-
permitAll:常にtrue -
isAuthenticated():認証済みならtrue -
hasAuthority():指定した権限を持っていればtrue
-
まとめ (アクセス制御)
-
FilterSecurityInterceptorでアクセス制御が開始される - 実際の制御は
AccessDecisionManagerが行う - 標準の実装は「投票」によってアクセス可否を判定している
-
AccessDecisionVoterが投票を行い、AccessDecisionManagerが投票結果を集計し結論を出す - デフォルトでは
WebExpressionVoterによる SpEL を利用した式ベースのアクセス制御が有効になっている
Hello World を読み解く
サーバー起動~リクエスト受付Spring Security の入り口ログイン処理アクセス制御- ログアウト処理
ログアウトを有効にする
applicationContext.xml
<sec:http>
...
<sec:logout />
</sec:http>
-
<logout>タグを使用すると、LogoutFilterが追加される -
/logoutに POST リクエストがくると、LogoutFilterがログアウトの処理を行う
全体ふりかえり
サーバー起動~リクエスト受付
Spring Security の入り口
ログイン処理
アクセス制御
これだけの意味がこの設定に込められている
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<filter>
<filter-name>
springSecurityFilterChain
</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
<filter-mapping>
<filter-name>
springSecurityFilterChain
</filter-name>
<url-pattern>
/*
</url-pattern>
</filter-mapping>
</web-app>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:sec="http://www.springframework.org/schema/security"
...>
<sec:http>
<sec:intercept-url
pattern="/login"
access="permitAll" />
<sec:intercept-url
pattern="/**"
access="isAuthenticated() and hasAuthority('BAR')" />
<sec:form-login />
<sec:logout />
</sec:http>
<sec:authentication-manager>
<sec:authentication-provider>
<sec:user-service>
<sec:user name="foo"
password="foo"
authorities="" />
<sec:user name="bar"
password="bar"
authorities="BAR" />
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
</beans>
いかがでしたでしょうか?
Spring Security 簡単そうと思いました?
【悲報】まだ Hello World
今日話していない機能とか要素とか
- GrantedAuthority
- Role の階層化
- メソッドのセキュリティ
- CSRF
- Memember-Me
- パスワードエンコード
- 各種カスタマイズ方法
- テスト
- etc...