今回は11/10にリリースが予定されているSpring Security 4.2の主な変更点を紹介します(なんとなくリリースは伸びる気がするけど・・・)。「Spring Security 4.1 主な変更点」の時と同様に、公式リファレンスの「What’s New in Spring Security 4.2」で紹介されている内容を、サンプルコードを交えて具体的に説明していきます。(逆にいうと、「What’s New in Spring Security 4.2」にのっていない変更点は紹介しないので、あしからず・・・ )
4.2.0.RELEASE (2016/11/10)
予想に反して!?11/10にちゃんとリリースされました
RC1のバグ修正や機能追加があったので反映しました。
動作検証環境
- Spring Security
4.2.0.RC1-> 4.2.0.RELEASE - Spring Boot
1.4.1.RELEASE-> 1.4.2.RELEASE
What’s New in Spring Security 4.2
Web Improvements
以下は、Web関連の主な変更点です。
No | Web関連の主な変更点 |
---|---|
1 | 認証情報などを保持するSpring Security提供のクラスを、JSONに変換するためのJacksonの拡張モジュールが提供されます。(分散セッション環境向け) |
2 | Referrer Policyヘッダを出力するための機能が追加されます。(★ 11/10追加) |
3 |
DefaultHttpFirewall にHTTPレスポンス分割攻撃対策が追加されます。 |
4 |
@AuthenticationPrincipal のexpression 属性の中でBean参照できるようになります。 |
5 | SSO連携用にRequestAttributeAuthenticationFilter が追加されます。 |
6 | ロードバランサなどのProxy Serverの設定に関するドキュメンテーションが追加されます。 |
7 | 無効化されたセッションからのリクエストを受けた時の振る舞いを実装するためのインタフェース(SessionInformationExpiredStrategy )とデフォルト実装が追加されます。(同一ユーザーでの同時セッション数を制限している時に使われます) |
8 | 複数のLogoutHandler をひとつのLogoutHandler として扱うための実装クラス(CompositeLogoutHandler )が追加されます。 |
Configuration Improvements
以下は、Configuration関連の主な変更点です。
No | Configuration関連の主な変更点 |
---|---|
9 | ロールであることを示すプレフィックス(デフォルト:ROLE_)を一元管理できるようになります。 |
10 | Java Config(WebSecurityConfigurerAdapter )使用時に、独自のConfigurer (AbstractHttpConfigurer のサブクラス)を読み込む仕組みが追加されます。 |
11 | XMLネームスペース(concurrency-control 要素のmax-sessions 属性)を使って同一ユーザーの同時セッション数を無制限(-1)にできるようになります。 |
12 | XMLネームスペース(intercept-url 要素にrequest-matcher-ref 属性)が追加され、アクセスポリシーの適用対象を柔軟に指定できるようになります。 |
13 |
Map<String, List<String>> から階層化ロールを表現する文字列(例: ROLE_ADMIN > ROLE_STAFF )を作るユーティリティメソッドが追加されます。 |
14 |
CookieCsrfTokenRepository で扱うCookieのパスが指定できるようになります。 |
15 |
InvalidSessionStrategy の実装クラス(無効なったセッションからのリクエストを検出した時の動作)を簡単に適用できるようになります。 |
16 | "Fix Exposing Beans for defaultMethodExpressionHandler can prevent Method Security"は、バグ修正のようなので説明は割愛します |
Miscellaneous
以下は、その他の主な変更点です。
No | その他の主な変更点 |
---|---|
17 | Spring 5上でも動作します(たぶん) ※試してませんけど |
18 |
User クラスに、UserBuilder とファクトリメソッドが追加されます。 |
19 | "Fix after csrf() is invoked, future MockMvc invocations use original CsrfTokenRepository"は、バグ修正のようなので説明は割愛します |
20 | 依存ライブラリとかのバージョンがアップされます(そして説明は割愛しま〜す ) |
Spring 4.2.0.RELEASEの適用する
まず、SPRING INITIALIZRから「Web」「Security」を指定してプロジェクトを作りましょう。
<!-- ... -->
<properties>
<!-- ... -->
<spring-security.version>4.2.0.RELEASE</spring-security.version> <!-- ← 追加 -->
</properties>
<!-- ... -->
分散セッション向けのJackson拡張モジュールが提供される
Spring Securityのデフォルト実装では、認証情報などをHTTPセッションに格納する仕組みになっており、複数のアプリケーションサーバでセッションを共有する場合は、以下のいずれかの方法を採用することになります。
- アプリケーションサーバー間でセッション情報をレプリケーションする
- RDBMSやKey/Valueストア(Redisなど)にセッション情報を保存する
いずれの方法を採用しても、セッション情報(Javaオブジェクト)をシリアライズ及びデシリアライズする仕組みが必要になります。もっとも標準的なのはJava標準の仕組み(java.io.Serializable
)ですが、実データに対してシリアライズ後のデータサイズの増加率が多い+パフォーマンスにも難点があり、別のソリューションが必要になるケースも少なくないようです。この問題を解決するソリューションのひとつとして使われるのが、セッション情報をJSONデータに変換して共有する方法になります。
本投稿では、Spring Sessionを使用してセッション情報をRedisに格納する際に、Spring Security 4.2で追加されたJackson拡張モジュールを使用してセッション情報をJSONに変換する方法を紹介します。
Redisのインストールと起動
本投稿ではインストール方法や起動方法の説明は割愛します。ネットで調べてインストールしてください
Spring Session(Redis)の適用
まず、依存ライブラリとして「org.springframework.session:spring-session-data-redis
」を追加します。
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
つぎに、セッションの保存先としてRedisを指定しましょう。
spring.session.store-type=redis
Note:
spring.session.store-type
は指定しなくても実は動くのですが・・・以下のようなWARNログが出てしまうので必ず指定するようにしましょう!!2016-11-10 23:44:46.257 WARN 31968 --- [ost-startStop-1] o.s.b.a.s.RedisSessionConfiguration : Spring Session store type is mandatory: set 'spring.session.store-type=redis' in your configuration
フォームログインの適用
Spring BootのデフォルトだとBasic認証が有効になており、ログインが成功しても認証情報がセッションに格納されないので、フォームログイン機能を有効化します。
package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.permitAll();
http.authorizeRequests()
.anyRequest().authenticated();
}
}
デフォルトユーザーのパスワード変更
Spring Bootのデフォルトだと、Spring Securityのデフォルトユーザー(user
)のパスワードは起動時に発行され、コンソール上に表示されます。
...
2016-11-05 17:12:26.520 INFO 12105 --- [ main] b.a.s.AuthenticationManagerConfiguration :
Using default security password: e825bc6d-19ef-4c76-93c7-d436a95172d2
毎回パスワードが変わるのは面倒なので、ここでは「password
」に変更しちゃいましょう。
security.user.password=password
ログインフォームの表示
「http://localhost:8080/login」にアクセスすると、Spring Security提供のログインフォームが表示されます。
ログイン画面を表示すると、HTTPセッションが生成されRedisに保存されます。今回はMedisというRedisのクライアントツールを使ってRedisの中身を覗いてみます。 (ツールはなんでもOKです)
画面に表示されているのは、HTTPセッションに格納したCSRFトークンを保持するクラスの中身です。・・・・中身がバイナリデータなのがわかるとおもいます。
Jackson拡張モジュールの適用
Spring Security 4.2で追加されたJackson拡張モジュールを使用して、Spring Sessionの中で利用しているSpring Data Redisの中でオブジェクトをJSONにシリアライズするようにしてみましょう。
package com.example.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.security.jackson2.SecurityJackson2Modules;
@Configuration
public class HttpSessionConfig implements BeanClassLoaderAware {
private ClassLoader classLoader;
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModules(SecurityJackson2Modules.getModules(classLoader)); // Spring Securityの拡張モジュールの適用
return new GenericJackson2JsonRedisSerializer(objectMapper); // Spring Data RedisにJSON変換用のコンポーネントを適用
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
}
Note:
Spring Bootを使わない場合は、
@Configuration
の代わりに@org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession
を指定してください!!
Jackson拡張モジュールを適用した後に、Redisの中身をクリアして再度ログイン画面を表示してみましょう。バイナリデータがJSON形式のデータになっていますね
さらに、ログイン後にRedisの中を覗くと、認証情報もJSONに変換されていることが確認できます
Warning:
RC1の時点では、事前認証関連のクラス(PreAuthenticatedAuthenticationToken
など)がサポートされていないみたいで、事前認証を使うとRedisからロードした認証情報(JSON)をパースする時にエラーになってしまいます(=認証後のアクセスでエラーになる・・・)。とりあえず、GitHubのIssueにコメントを残しておきました。4.2.0.RELEASEで解消されました!!
Referrer Policyヘッダの出力機能が追加される
Spring Security 4.2より、Referrer Policyをブラウザに指示するためのレスポンスヘッダを出力する機能が追加されます。なお、この機能はデフォルトでは適用されないので、適用したい場合は以下のようなBean定義を行う必要があります。
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.headers()
.referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.SAME_ORIGIN);
// ...
}
<sec:http>
<!-- ... -->
<sec:headers>
<sec:referrer-policy policy="same-origin"/>
</sec:headers>
<!-- ... -->
</sec:http>
以下のレスポンスヘッダが表示されます。
HTTP/1.1 200
...
Referrer-Policy: same-origin
...
HTTPレスポンス分割攻撃対策が追加される
HTTPレスポンス分割攻撃(HTTP Response Splitting Attack)は、HTTPヘッダ・インジェクション攻撃により複数のHTTPレスンポンスを作り出してキャッシュサーバ(プロキシサーバ)に偽のコンテンツをキャッシュさせることで、悪意のあるページを閲覧させるという攻撃手法です。Spring Secuirty 4.2からは、レスポンスヘッダの値に改行コードが含まれているとIllegalArgumentException
になります。
@AuthenticationPrincipal
のexpression
属性の中でBean参照できる
@AuthenticationPrincipal
のexpression
属性の中でBean参照できるようになったことで、認証情報を引数にして任意のBeanのメソッドを呼び出し、メソッドの返り値をHandlerメソッドの引数として受け取ることができるようになります。
Spring SecurityのリファレンスではJPAのEntityManager
との連携例が紹介されていますが、本投稿では、認証情報に紐づくプロファイル情報(電話番号、メールアドレス)を検索してHandlerメソッドの引数として受け取る例を紹介します。
プロファイル検索サービスの作成
まず、ユーザ名を受け取ってプロファイル情報を返却するメソッドを作成しましょう。本来であればデータベースから検索するところですが、本投稿では固定値を返しちゃいます。
package com.example.service;
import com.example.domain.Profile;
import org.springframework.stereotype.Service;
@Service
public class ProfileService {
public Profile find(String username) {
Profile profile = new Profile();
profile.setUsername(username);
profile.setPhone("09012345678");
profile.setEmail("test@gmail.com");
return profile;
}
}
プロファイル検索サービスの呼び出し
@AuthenticationPrincipal
のexpression
属性の中からプロファイル検索サービスのBeanを参照し、検索メソッドを呼び出した結果をHandlerメソッドの引数として受け取りましょう。
package com.example.controller;
import com.example.domain.Profile;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/profile")
public class ProfileRestController {
@GetMapping
public Profile getProfile(
@AuthenticationPrincipal(expression = "@profileService.find(username)") Profile profile) {
return profile;
}
}
ブラウザから「http://localhost:8080/profile」にアクセスして、ログインすると以下のJSONが返却されます。
{"username":"user","phone":"09012345678","email":"test@gmail.com"}
RequestAttributeAuthenticationFilter
が追加される
AbstractPreAuthenticatedProcessingFilter
のサブクラスとして、リクエストスコープから資格情報を取得して事前認証を行うRequestAttributeAuthenticationFilter
クラスが追加されます。このクラスを使うことで、
といったSSO(Single Sign-On)のプロダクトと連携することができます。
Note:
類似のクラスとしては、リクエストヘッダから資格情報を取得するクラス(
RequestHeaderAuthenticationFilter
)があります。
SSO(Single Sign-On)のプロダクトと実際に連携するのはちょっと面倒なので・・・本投稿では、擬似的に同じ状態を作り出して動作を確認することにします。
package com.example.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesUserDetailsService;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails;
import org.springframework.security.web.authentication.preauth.RequestAttributeAuthenticationFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.permitAll();
http.authorizeRequests()
.anyRequest().authenticated();
// ↓ 事前認証用のフィルターの適用(フォーム認証より優先)
http.addFilterBefore(preAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
// 事前認証用のフィルターをセットアップ
private RequestAttributeAuthenticationFilter preAuthenticationFilter() {
RequestAttributeAuthenticationFilter filter = new RequestAttributeAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager);
filter.setAuthenticationDetailsSource(
request -> new PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails(
request, AuthorityUtils.createAuthorityList("ROLE_USER")));
filter.setExceptionIfVariableMissing(false);
return filter;
}
// 事前認証用の認証プロバイダーをセットアップ
@Autowired
void configurePreAuthenticatedAuthenticationProvider(AuthenticationManagerBuilder builder) {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new PreAuthenticatedGrantedAuthoritiesUserDetailsService());
builder.authenticationProvider(provider);
}
// 擬似的にSSOプロダクトを通過した状態を作り出すためのフィルター
// 認証が必要なリソースに対して「?user=xxxx」を付与するとSSOプロダクトを通過した状態になる
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
static class SsoUsernameStoringFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String user = request.getParameter("user");
if (user != null) {
request.setAttribute("REMOTE_USER", user);
}
filterChain.doFilter(request, response);
}
}
}
事前認証適用後に前セクションで作成した「/profile」にアクセスすると、ログインフォームを使用した認証を行わずに「/profile」にアクセスすることができます。
$ curl -D - http://localhost:8080/profile?user=kazuki43zoo
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: SESSION=acffc6c9-d555-4da5-828d-ec4e20a937d6;path=/;HttpOnly
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 05 Nov 2016 15:07:39 GMT
{"username":"kazuki43zoo","phone":"09012345678","email":"test@gmail.com"}
事前認証が成功するとSpring Secuirtyの認証情報はセッションに格納され、以降のリクエストでは認証処理が不要になります。これを確認するために、2回目のリクエストでは、「?user=xxxxx」を省略してみましょう。
$ curl -D - http://localhost:8080/profile -H 'Cookie: SESSION=acffc6c9-d555-4da5-828d-ec4e20a937d6'
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 05 Nov 2016 15:15:46 GMT
{"username":"kazuki43zoo","phone":"09012345678","email":"test@gmail.com"}
今度は違うユーザー名を指定してみましょう。
$ curl -D - http://localhost:8080/profile?user=hoge -H 'Cookie: SESSION=acffc6c9-d555-4da5-828d-ec4e20a937d6'
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 05 Nov 2016 15:16:47 GMT
{"username":"kazuki43zoo","phone":"09012345678","email":"test@gmail.com"}
既に同一セッション内に認証情報があるため、認証処理が行われないことがわかります。
Proxy Server関連のドキュメンテーションが追加される
Spring Securityのリファレンスページに、ロードバランサなどのProxy Server利用時の設定に関するドキュメンテーションが追加されます。
SessionInformationExpiredStrategy
が追加される
無効化されたセッションからのリクエストを受けた時の振る舞いを実装するためのインタフェース(SessionInformationExpiredStrategy
)とデフォルト実装が追加されます。追加されたインタフェースやクラスは、同一ユーザーでの同時セッション数を制限していて+同時セッション数を超えた時にもっともアクセスがないセッションを無効化する設定になっている時です。
デフォルトで使われる実装クラスは、ConcurrentSessionFilter
の内部クラスとして実装されているResponseBodySessionInformationExpiredStrategy
で、レスポンスBODYに「This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).」が出力されます。
また、無効化されたセッションからのリクエストを受けた際に、指定したURLにリダイレクトする設定にするとSimpleRedirectSessionInformationExpiredStrategy
というクラスが使われます。
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.sessionManagement()
.maximumSessions(5)
.maxSessionsPreventsLogin(false)
.expiredUrl("/login?session-expired");
// ...
}
なお、Spring Securityが用意している実装で要件が充たせない場合は、独自の実装を適用することもできます。
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.sessionManagement()
.maximumSessions(5)
.maxSessionsPreventsLogin(false)
.expiredSessionStrategy(expiredSessionStrategy());
// ...
}
private SessionInformationExpiredStrategy expiredSessionStrategy() {
return event -> {
// 要件を満たすための独自処理を実装
};
}
CompositeLogoutHandler
が追加される
複数のLogoutHandler
をひとつのLogoutHandler
として扱うための実装クラス(CompositeLogoutHandler
)が追加されます。おそらく・・・このクラスを直接使うことはあまりないと思うので、本投稿では説明は割愛させてもらいます。
ロールであることを示すプレフィックスが一元管理できる
Spring Securityのデフォルトでは、ロールベースの権限を"ROLE_"ではじまる文字列として管理しています。
たとえば、あるユーザーに「利用者」と「管理者」のロールを割り当てる場合は、「ROLE_USER」「ROLE_ADMIN」といった感じのロール名を権限情報として保持するようにします。
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User(username, "N/A", AuthorityUtils.createAuthorityList("ROLE_USER")); // "ROLE_"が必要
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.authorizeRequests()
.anyRequest().hasRole("USER"); // "ROLE_"は省略できる(Spring Security 4.0〜)
// ...
}
デフォルトのプレフィックス("ROLE_")文字はカスタマイズすることができ、Spring Security 4.2からは、org.springframework.security.config.core.GrantedAuthorityDefaults
のBeanを定義するだけで実現できます。ここでは、プレフィックス("ROLE_")をなくす場合の設定例を紹介しましょう。
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("");
}
上記の設定を行うと、認証情報に保持するロールにプレフィックス(デフォルトでは"ROLE_")を指定する必要がありません。
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User(username, "N/A", AuthorityUtils.createAuthorityList("USER")); // "ROLE_"は不要
}
}
Spring Security 4.0より、リソースに対してアクセスポリシーを指定する際にロール名からプレフィックスを省略できるように改善されており、認証情報で保持するロール名からもプレフィックス("ROLE_")を外したいと思った方もいるのではないでしょうか? Spring Security 4.2から導入されたこの仕組みを使うと簡単にプレフィックスをなくすことができます!
Warning:
認証情報で保持するロール名からプレフィックスをなくすことができると紹介しましたが・・・、
hasRole
メソッドを使ってアクセスポリシーを指定していると正しく動作しないことがわかっています・・・http.authorizeRequests() .anyRequest().hasRole("USER"); // ← hasRoleメソッドの中で"ROLE_"を固定で付与している・・・
この問題の回避策としては・・・、
access
メソッドを使用し、ExpressionとしてhasRole
メソッドを指定することで正しく動作させることができます。http.authorizeRequests() .anyRequest().access("hasRole('USER')"); // Expression経由だとプレフィックスのカスタマイズ内容が反映される
これはSpring Securityのバグ?な気がするので・・・
とりあえず・・・GitHubのIssueにコメントを残しておきました。11/16追記
Robから別途Issueを作って!!とコメントが返ってきたので作りました。
https://github.com/spring-projects/spring-security/issues/4134
独自のSecurityConfigurer
をデフォルトで適用できるようになる
Java Configを使ってSpring Securityのコンフィギュレーションを行う場合、WebSecurityConfigurerAdapter
を継承したJava Configクラスを作成し、WebSecurityConfigurerAdapter
クラスのメソッドをオーバライドしてコンフィギュレーションするのが一般的です。具体的には、以下のようなコンフィギュレーションクラスを作成してコンフィギュレーションを行います。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.permitAll();
http.logout()
.permitAll();
http.authorizeRequests()
.anyRequest().authenticated();
}
}
では、SecurityConfigurer
とは何者なのでしょうか?
実は・・・いつも何気なくつかっている、formLogin
、logout
、authorizeRequests
メソッドなどから返却されるオブジェクトがSecurityConfigurer
インタフェースを実装しており、開発者が指定した設定内容に応じてSpring Securityの各機能のセットアップを行う役割を担います。この仕組みのおかげで、我々開発者はたった数行のコンフィギュレーションを行うだけで、Spring Securityが提供する様々な機能を簡単に利用することができるのです。
本投稿では、Spring Security 4.2でサポートされた仕組みを紹介する前に、SecurityConfigurer
の作成方法と適用例を簡単に紹介します。
独自のSecurityConfigurer
の作成
Spring Securityから様々なSecurityConfigurer
インタフェースの実装クラスが提供されており、多くのアプリケーションではそれらのクラスを使ってSpring Securityのセットアップができると思います。とはいえ・・・Spring Securityも万能ではないため、どうしても独自のカスタマイズが必要になることがあります。たとえば・・・本投稿で紹介しているRequestAttributeAuthenticationFilter
を使った事前認証を行う際には、事前認証を適用するために独自のBean定義が必要になります。ひとつ又は数個のアプリケーションに対して適用するだけならアドホックにBean定義してしまってもよいでしょうが、これが数十、数百というアプリケーションに対して同じ設定を適用したい場合は、独自のSecurityConfigurer
の作成を検討してみるのがよいと思います。
ここでは、RequestAttributeAuthenticationFilter
を使った事前認証のところでアドホックにBean定義した部分を、SecurityConfigurer
の実装クラスに置き換えてみます。
package com.example.config;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesUserDetailsService;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails;
import org.springframework.security.web.authentication.preauth.RequestAttributeAuthenticationFilter;
public class MyPreAuthenticationConfigurer extends AbstractHttpConfigurer<MyPreAuthenticationConfigurer, HttpSecurity> {
@Override
public void init(HttpSecurity http) throws Exception {
// 事前認証用の認証プロバイダーをセットアップ
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new PreAuthenticatedGrantedAuthoritiesUserDetailsService());
http.authenticationProvider(provider);
}
@Override
public void configure(HttpSecurity http) throws Exception {
// ↓ 事前認証用のフィルターの適用(フォーム認証より優先)
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
http.addFilterBefore(createPreAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class);
}
private RequestAttributeAuthenticationFilter createPreAuthenticationFilter(AuthenticationManager authenticationManager) {
RequestAttributeAuthenticationFilter filter = new RequestAttributeAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager);
filter.setAuthenticationDetailsSource(
request -> new PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails(
request, AuthorityUtils.createAuthorityList("ROLE_USER")));
filter.setExceptionIfVariableMissing(false);
return filter;
}
}
Note:
開発者にカスタマイズポイントを提供する場合は、setterメソッドなどを提供し、外部から設定された内容に応じてBean定義するのがよいでしょう。(本投稿では説明をシンプルにするために、カスタマイズポイントはあえて提供していません)
独自のSecurityConfigurer
のマニュアル適用
作成したSecurityConfigurer
インタフェースの実装クラスを使ってSpring Securityをセットアップしたい場合は、HttpSecurity
のapply
メソッドを使います。(Spring Security 4.1以前でも利用可能な方法です)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.apply(new MyPreAuthenticationConfigurer());
}
}
Note:
Spring Securityのリファレンスページで紹介されているように・・・ファクトリメソッドを作って(+staticインポートして)以下のようなスタイルにしてもよいでしょう。(好みの問題かなと・・・)
http.apply(preAuth());
独自のSecurityConfigurer
のデフォルト適用
Spring Security 4.2より、デフォルトで適用したいSecurityConfigurer
インタフェースの実装クラスを、クラスパス内の/META-INF/spring.factories
に指定できるようになります。ただし・・・この仕組みを利用する場合は、SecurityConfigurer
の実装クラスはorg.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer
を継承して作成する必要があります。さもないと・・・起動時にエラーになってしまいます。
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = com.example.config.MyPreAuthenticationConfigurer
なお、デフォルト適用される設定をカスタマイズしたい場合や、何かしらの理由でデフォルト適用外にしたい場合は・・・前述のマニュアル適用を行うことで対応することができます。
http.apply(new MyPreAuthenticationConfigurer())
.principalEnvironmentVariable("AUTH_USER"); // カスタマイズ用のsetterメソッドなどを別途用意しておく
http.apply(new MyPreAuthenticationConfigurer())
.disable(); // disableメソッドを呼ぶと無効化できる
XMLのBean定義で同一ユーザーの同時セッション数を無制限にできる
Spring Security 4.2より、XMLネームスペース(concurrency-control
要素のmax-sessions
属性)を使って同一ユーザーの同時セッション数を無制限(-1)にできるようになります。Spring Security 4.1以前から機能的には無制限にすることができましたが、XMLネームスペースを使って無制限(-1)にすることができませんでした。
<sec:http>
<!-- ... -->
<sec:session-management>
<sec:concurrency-control max-sessions="-1" />
</sec:session-management>
</sec:http>
intercept-url
要素にrequest-matcher-ref
属性が追加される
XMLネームスペース(intercept-url
要素にrequest-matcher-ref
属性)が追加され、アクセスポリシーの適用対象を柔軟に指定できるようになります。Java Configでは以前からサポートされていましたが、アクセスポリシーの適用対象を指定する際に、XMLでもRequestMatcher
を指定できるようになります。
<sec:http>
<!-- ... -->
<sec:intercept-url request-matcher-ref="adminRolePathMatcher" access="hasRole('ADMIN')"/>
</sec:http>
<bean id="adminRolePathMatcher" class="org.springframework.security.web.util.matcher.AndRequestMatcher">
<constructor-arg>
<list>
<bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<constructor-arg value="/admin/**" />
</bean>
<bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<constructor-arg value="/config/**" />
</bean>
<bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<constructor-arg value="/monitor/**" />
</bean>
</list>
</constructor-arg>
</bean>
Map
から階層化ロールを表現する文字列を簡単に作れる
Map<String, List<String>>
から階層化ロールを表現する文字列(例: ROLE_ADMIN > ROLE_STAFF
)を作るユーティリティメソッドが追加されます。これは、Spring BootのType-safe Configuration Propertiesとの連携を強く意識仕組みで、以下のようなプロパティクラスを作成してRoleHierarchy
のBeanを定義を行うだけで、簡単に階層化ロールを利用することができます。
本投稿では、Spring Security 4.2で追加された仕組みを活用して、以下のような階層化ロールを表現する文字列を生成するための定義例を紹介します。
ROLE_ADMIN > ROLE_MANAGER
ROLE_ADMIN > ROLE_STAFF
ROLE_STAFF > ROLE_USER
ROLE_USER > ROLE_GUEST
package com.example.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ConfigurationProperties("security.roles")
@Component
public class SecurityRolesProperties {
private Map<String, List<String>> hierarchyMap = new LinkedHashMap<>();
public Map<String, List<String>> getHierarchyMap() {
return hierarchyMap;
}
public void setHierarchyMap(Map<String, List<String>> hierarchyMap) {
this.hierarchyMap = hierarchyMap;
}
public RoleHierarchy getRoleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy(CollectionUtils.isEmpty(hierarchyMap) ? ""
: RoleHierarchyUtils.roleHierarchyFromMap(hierarchyMap)); // Mapから階層化ロール文字列に変換
return roleHierarchy;
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
@Bean
RoleHierarchy roleHierarchy(SecurityRolesProperties rolesProperties) {
return rolesProperties.getRoleHierarchy();
}
}
プロパティやYAMLファイルを使用して、ロールの階層化定義を行います。
security.roles.hierarchyMap.ROLE_ADMIN = ROLE_MANAGER,ROLE_STAFF
security.roles.hierarchyMap.ROLE_STAFF = ROLE_USER
security.roles.hierarchyMap.ROLE_USER = ROLE_GUEST
or
security:
roles:
hierarchyMap:
ROLE_ADMIN:
- ROLE_MANAGER
- ROLE_STAFF
ROLE_STAFF:
- ROLE_USER
ROLE_USER:
- ROLE_GUEST
CookieCsrfTokenRepository
で扱うCookieのパスが指定できる
Spring Security 4.1で追加されたCookieCsrfTokenRepository
ですが、CookieのパスにはWebアプリケーションのコンテキストパスが固定で設定される仕組みになっていました。Spring Security 4.2からは、Cookieのパスに設定する値を明示的に指定することができるようになります。
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.csrf()
.csrfTokenRepository(csrfTokenRepository());
}
private CsrfTokenRepository csrfTokenRepository() {
CookieCsrfTokenRepository repository = new CookieCsrfTokenRepository();
repository.setCookiePath("/foo"); // 明示的にパスを指定できる
return repository;
}
InvalidSessionStrategy
の実装クラスを簡単に適用できる
Spring Security 4.2から、InvalidSessionStrategy
の実装クラス(無効なったセッションからのリクエストを検出した時の動作)を簡単に適用できるようになります。これまでは、無効なったセッションからのリクエストを検出した時に表示するURLの指定しかできませんでした。
Note:
Java Configの方は4.1からサポートされていたような気がするけど・・・・
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.sessionManagement()
.invalidSessionStrategy(invalidSessionStrategy());
// ...
}
private InvalidSessionStrategy invalidSessionStrategy() {
return (request, response) -> {
// 要件を満たすための独自処理を実装
};
}
<sec:http>
<!-- ... -->
<sec:session-management invalid-session-strategy-ref="invalidSessionStrategy" />
<!-- ... -->
</sec:http>
<bean id="invalidSessionStrategy" class="com.example.CustomInvalidSessionStrategy" />
UserBuilder
とファクトリメソッドが追加される
User
クラスに、UserBuilder
とファクトリメソッドが追加されます。
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(User.withUsername("kazuki43zoo").password("password").roles("USER").build());
return userDetailsManager;
}
まとめ
今回はこれからリリースされるSpring Security 4.2の主な変更点を紹介しました。Spring Security 4.1に引き続きコンフィギュレーションが簡単になったり、いくつか新機能も追加されています!!(Spring 4.1にくらべるとインパクトのある変更少ない気はしますが・・・)。
既に「Spring Security Reactive」の開発も始まっているので、そちらの動向も見逃せないところです