自己紹介
- opengl-8080
- 主に Qiita で技術メモを書いたり
- 関西の SIer 勤務
今日お話しすること
簡単な Hello World を通じて、 Spring Security の仕組みの基礎的な部分を説明
- どのようなクラスが、どのように連携しあっているのか
- 設定ファイルがどのように関係しているのか
背景
- 個人的に Spring Security の勉強を開始
- ちょっと Hello World を書こうとしたが手こずる
- この設定はなんで必要?
- ・・・と書くとなぜ~~~が有効に?
- この設定って最小限の Hello World で必要?
抽象化された設定
- Spring Security の設定は高度に抽象化されている
- 設定が簡潔になる一方で、裏で何が行われているかが分かりづらい
- 仕組みの理解や、カスタマイズがしづらくなる
※個人の所感です
対象者
Hello World を通じて Spring Security の仕組みを学ぶことで、
- Spring Security をこれから勉強する人の参考に(なれるように)
- 仕組みを知らずに Spring Security を使ってた人の参考に(なれるように)
前提知識
- Servlet, JSP について、そこそこの知識を持っている
-
Filter
などを知っており、web.xml
を書いて Servlet アプリを構築できる
-
- Spring についてそこそこの知識を持っている
- DI コンテナが何なのか、 Bean や AOP を理解している
Hello World アプリ(前提)
Spring Boot は使わない
- Spring Security の基礎を学ぶのが目的
- 素の Spring Security だけを使う
Java Config は使わない
- xml で設定を記述する
- 結局 Java Config もやってることは同じ
Servlet 3.0 の機能は使わない
- Servlet 3.0 の機能を利用すれば web.xml なしで設定が可能
- 仕組みの基礎を学ぶのが目的なので、こちらもやはり使わないようにする
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>
- ログイン後に表示されるトップページ
- メッセージとログアウトボタンがあるだけ
-
_csrf
についてはデフォルトで有効になっているため設定していますが、この後の説明では特に登場しないので詳細な説明は省きます- 詳細は Spring Security 使い方メモ CSRF | Qiita を参照してください
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
の例SecurityContextPersistenceFilter
CsrfFilter
AnonymousAuthenticationFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
- 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...