今回はリリースから約1ヶ月経過したSpring Security 4.1の主な変更点を紹介します。「Spring 4.3の変更点」と同様に、公式リファレンスの「What’s New in Spring Security 4.1」で紹介されている内容を、サンプルコードを交えて具体的に説明していきます。(逆にいうと、「What’s New in Spring Security 4.1」にのっていない変更点は紹介しないので、あしからず・・・ )
そもそもSpring Securityって・・・という方は、「TERASOLUNA Server Framework (5.x)のガイドライン」を読もう!!Spring Secuirty 4.0ベースでのSpring Securityの基本的な使い方、スタートアップ用のチュートリアル、具体的なセキュリティ要件を充たすための実装サンプルが提供されており、体系的にSpring Securityのアーキテクチャと使い方が学べます
Spring Boot前提ではない+XMLベースのコンフィギュレーション例しか載ってないのが少しだけ残念ですが、そこはSpring BootやSpring Securityの公式リファレンスを参照して読み替えましょう!!
4.1.1.RELEASE (4.1.2.RELEAE)
- Spring 4.1.1.REALEASEで追加された
MvcRequestMatcher
の説明を追加しました。(差分は、「★8/13追加」でマークしてあります)
動作検証環境
- Spring Security 4.1.0.RELEASE
- Spring Boot 1.4.0.BUILD-SNAPSHOT (2016/6/5時点)
What’s New in Spring Security 4.1
Spring Securityの公式リファレンスでは、以下の6つのセクションで改善点が紹介されています。Spring 4.3の投稿ではセクション毎に投稿をわけましたが、今回はいっきに説明します!!
Java Configuration Improvements
以下は、JavaConfig関連の主な変更点です。
No | JavaConfig関連の主な変更点 |
---|---|
1 | カスタマイズしたUserDetailsService をSpring Securityに適用するためのBean定義がちょ〜簡単になりました。(ってかDIコンテナに登録しちゃえば自動検出されちゃう ) |
2 | カスタマイズしたAuthenticationProvider をSpring Securityに適用するためのBean定義がちょ〜簡単になりました。(こちらもDIコンテナに登録しちゃえば自動検出されちゃう ) |
3 |
LogoutSuccessHandler をリクエストの種類毎(RequestMatcher 毎)に適用するためのメソッドが追加されました。この変更により、クライアントの種類に応じてログアウト成功時のレスポンス(ステータスコードのみ、HTML、JSONなど)を切り替えられます。 |
4 | カスタマイズしたInvalidSessionStrategy をSpring Securityに適用するためのメソッドが追加されました。 |
5 | サーブレットフィルタを指定した登録済みのフィルタと同じポジション(優先順)に追加するためのメソッドが追加されました。 |
Web Application Security Improvements
以下は、Webアプリ関連の主な変更点です。
No | Webアプリ関連の主な変更点 |
---|---|
6 | Webリソースへの認可にて、Spring MVCのリクエストマッピングのルールと連動したRequestMatcher (MvcRequestMatcher )が追加されました。 「★8/13追加」 |
7 | XSS攻撃対策として、「Content Security Policy (CSP)」用のHTTPレスポンスヘッダの出力がサポートされました。 |
8 | 偽造された証明書による中間者攻撃対策として、「HTTP Public Key Pinning (HPKP)」用のHTTPレスポンスヘッダの出力がサポートされました。 |
9 | CSRFトークンの保存先としてCookieがサポートされました。これは、AngularJSのCSRF対策機能のデフォルト動作をサポートするために追加されたようです。 |
10 |
AuthenticationSuccessHandler とAuthenticationFailureHandler にて、遷移先にフォワードする実装クラスが追加されました。この変更により、遷移先でリクエストスコープの情報にアクセスすることが可能になります。 |
11 |
@AuthenticationPrincipal にexpression 属性が追加されました。expression 属性を利用すると、Spring MVCのHandlerメソッドの引数に、認証ユーザー情報(Authentication#getPrincipal() メソッドの返り値)が保持する任意のプロパティ値をインジェクションすることができます。 |
Authorization Improvements
以下は、認可関連の主な変更点です。
No | 認可関連の主な変更点 |
---|---|
12 | Webリソースへの認可にて、(待望の・・・)パス変数値を参照した認可制御が可能になりました |
13 | Javaメソッドへの認可時に使用するアノテーション(@PreAuthorize など)がメタアノテーションとして使用できるようになりました。 |
Crypto Module Improvements
以下は、暗号化関連の主な変更点です。
No | 暗号化関連の主な変更点 |
---|---|
14 | SCryptアルゴリズムをサポートしたPasswordEncoder の実装クラスが追加されました。 |
15 | PBKDF2アルゴリズムをサポートしたPasswordEncoder の実装クラスが追加されました。 |
Testing Improvements
以下は、テスト関連の主な変更点です。
No | テスト関連の主な変更点 |
---|---|
16 | 匿名ユーザーの認証情報をセットアップするためのアノテーション(@WithAnonymousUser )が追加されました。 |
17 |
@WithUserDetails にuserDetailsServiceBeanName 属性が追加されました。userDetailsServiceBeanName 属性を使用すると、テスト時に使用するUserDetailsService のBeanを明示的に指定することができます。 |
18 | 認証情報などを指定するアノテーション(@WithMockUser など)をメタアノテーションとして使用できるようになりました。 |
19 |
SecurityMockMvcResultMatchers#withAuthorities メソッドで、GrantedAuthority のサブクラスをキャストなしで扱えるようになりました??(公式リファレンスに記載されている内容となんとなく違い気がするけど・・・たぶんあっていると思う・・・ ) |
General Improvements
以下は、Spring Securityプロジェクトの(非機能的な)変更点です。
No | Spring Securityプロジェクトの変更点 |
---|---|
20 | サンプルプロジェクトが再編された模様です。 (Issue#3752) |
21 | Issueトラッキングが、Spring JIRAからGitHubに移管されました。 |
動作検証用のプロジェクト準備
今回紹介する内容の動作検証は、Spring Boot 1.4(Spring Security 4.1)で行います。実際にアプリ上で動作を確認したい方は、「SPRING INITIALIZR」からプロジェクトを作りましょう。
ダウンロードしたZipファイルを適当なところに解凍し、pom.xml
に以下のアーティファクトを追加します。
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
Spring Boot起動を起動しましょう。以下のようなログが出力されればOKです。ちなみに・・・停止は「Ctl+C」です。
$ ./mvnw spring-boot:run
...
2016-06-05 14:04:57.149 INFO 59552 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2016-06-05 14:04:57.215 INFO 59552 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-06-05 14:04:57.220 INFO 59552 --- [ main] com.example.Sec41DemoApplication : Started Sec41DemoApplication in 3.553 seconds (JVM running for 6.06)
UserDetailsService
(+PasswordEncoder
)の適用が超絶簡単になる
Spring Security 4.1から、カスタマイズしたUserDetailsService
及びPasswordEncoder
をSpring Securityに適用する方法が超絶簡単になりました。結論から言うと・・・SpringのDIコンテナに登録すると自動検出してくれます
Spring Bootが使うデフォルトのUserDetailsService
とPasswordEncoder
は?
Spring Bootの自動コンフィギュレーションでは、インメモリ実装のInMemoryUserDetailsManager
が利用され、デフォルトユーザーとして「ユーザ名:user
、パスワード:起動時に生成したUUID」が作成されます。起動時に生成されるパスワード(UUID)は、以下のようにコンソールログに出力されます。なお、使用されるPasswordEncoder
は、プレーンテキストのまま扱うPlaintextPasswordEncoder
が利用されます。
...
2016-06-05 14:10:05.479 INFO 59590 --- [ost-startStop-1] b.a.s.AuthenticationManagerConfiguration :
Using default security password: c93c44f6-9194-4249-b360-6d8a1753b946
2016-06-05 14:10:05.555 INFO 59590 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: Ant [pattern='/css/**'], []
...
独自のDB認証用のUserDetailsService
をSpring Securityに適用する
ここでは、認証情報をデータベースから取得するUserDetailsService
を作成し、Spring Securityに提供する方法を紹介します。
まず、ユーザー情報を保持するドメインオブジェクトと認証用のユーザー情報を作成します。
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private String name;
private String authorities;
// ...
}
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
public class MyUserDetails extends User {
private static final long serialVersionUID = 1L;
private final Account account;
public MyUserDetails(Account account) {
super(account.getUsername(), account.getPassword(),
AuthorityUtils.commaSeparatedStringToAuthorityList(account.getAuthorities()));
this.account = account;
}
public Account getAccount() {
return account;
}
}
ユーザー情報を保持するUserDetails
を返却するUserDetailsService
を作成します。
@Service // コンポーネントスキャン機能でDIコンテナに登録すれば自動検出される
public class MyUserDetailsService implements UserDetailsService {
@Autowired
NamedParameterJdbcOperations namedParameterJdbcOperations;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
Account account = namedParameterJdbcOperations.queryForObject(
"SELECT username, password, name, authorities FROM account WHERE username = :username"
, new MapSqlParameterSource("username", username),
new BeanPropertyRowMapper<>(Account.class));
return new MyUserDetails(account);
} catch (IncorrectResultSizeDataAccessException e) {
throw new UsernameNotFoundException("A specified user does not exist.", e);
}
}
}
実は、これだけで作成したMyUserDetailsService
クラスが、Spring Securityに適用されちゃいます!!ポイントは・・・@Service
アノテーションを付与してコンポーネントスキャン対象にしているところです。コンポーネントスキャン機能によってDIコンテナに登録されたMyUserDetailsService
が、Spring Securityによって自動検出されます。作成したUserDetailsService
をコンポーネントスキャン対象にしない場合は、普通にBean定義すればOKです。
@Configuration
public class SecurityConfig {
@Bean
UserDetailsService userDetailsService(){
return new MyUserDetailsService();
}
}
ちなみに・・・DDLとDMLはこんな感じ。これらのSQLファイルは、Spring Bootが自動で実行してくれます。
CREATE TABLE account (
username NVARCHAR(128) PRIMARY KEY,
password CHAR(60) NOT NULL,
name NVARCHAR(128) NOT NULL,
authorities TEXT
);
-- password = demo(Hashed by Bcrypt algorithm)
INSERT INTO account (username, password, name, authorities)
VALUES ('demo', '$2a$10$oxSJl.keBwxmsMLkcT9lPeAIxfNTPNQxpeywMrF7A3kVszwUTqfTK', 'Kazuki Shimizu', 'ROLE_USER');
INSERT INTO account (username, password, name, authorities)
VALUES ('admin', '$2a$10$oxSJl.keBwxmsMLkcT9lPeAIxfNTPNQxpeywMrF7A3kVszwUTqfTK', 'Administrator', 'ROLE_ADMIN,ROLE_USER');
PasswordEncoder
の適用
Spring Bootのデフォルトのままだとパスワードはプレーンテキストとして扱ってしまうため、ここではデータベースで管理するパスワードのハッシュ化アルゴリズムにあうPasswordEncoder
を適用しましょう。PasswordEncoder
も、UserDetailsService
と同様にDIコンテナに登録するだけで自動検出されます!!
@Configuration
public class SecurityConfig {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
AuthenticationProvider
の適用が超絶簡単になる
Spring Security 4.1から、カスタマイズしたAuthenticationProvider
をSpring Securityに適用する方法が超絶簡単になりました。結論から言うと・・・SpringのDIコンテナに登録すると自動検出してくれます
独自のAuthenticationProvider
をSpring Securityに適用する
ここでは、Spring Securityから提供されているDaoAuthenticationProvider
のチェック処理を拡張してみます。
@Component // コンポーネントスキャン機能でDIコンテナに登録すれば自動検出される
public class MyDaoAuthenticationProvider extends DaoAuthenticationProvider {
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// ... Please add a check
// ... (省略)
super.additionalAuthenticationChecks(userDetails, authentication);
// ... Please add a check
// ... (省略)
}
@Autowired
public void setUserDetailsService(UserDetailsService userDetailsService) {
super.setUserDetailsService(userDetailsService);
}
@Autowired
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
super.setPasswordEncoder(passwordEncoder);
}
}
作成したAuthenticationProvider
をコンポーネントスキャン対象にしない場合は、普通にBean定義すればOKです。
@Configuration
public class SecurityConfig {
@Bean
AuthenticationProvider authenticationProvider() {
return new MyDaoAuthenticationProvider();
}
}
LogoutSuccessHandler
の複数登録が可能になる
LogoutSuccessHandler
をリクエストの種類毎(RequestMatcher
毎)に適用するためのメソッドが追加されました。この変更により、クライアントの種類に応じてログアウト成功時のレスポンス(ステータスコードのみ、HTML、JSONなど)を切り替えられます。ここでは、Ajaxの場合はステータスコード(200 OK)のみを返却し、Ajaxでなければログイン画面(/login?logout
)に遷移させる場合のサンプルです。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().permitAll();
http.authorizeRequests().anyRequest().authenticated();
// ...
http.logout()
// Ajaxリクエストの場合は「200 OK」のみを返却
.defaultLogoutSuccessHandlerFor(new HttpStatusReturningLogoutSuccessHandler(),
request -> "XMLHttpRequest".equals(request.getHeader("X-Requested-With")))
.permitAll();
}
}
フォームとAjaxを使ってログアウトする画面も作成してみましょう。
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>2.2.4</version>
</dependency>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<script th:src="@{/webjars/jquery/2.2.4/jquery.js}"/>
<script type="text/javascript">
$(function () {
$("#logout").click(function () {
$.ajax({
url: '/logout',
type: 'POST',
success: function () {
window.location = "/login?logout";
},
error: function (data) {
console.log(data);
}
});
});
$(document).ajaxSend(function (event, xhr, options) {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
xhr.setRequestHeader(header, token);
});
});
</script>
<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
</head>
<body>
<h3>メニュー</h3>
<!-- フォームを使ったログアウトボタン -->
<form th:action="@{/logout}" method="post">
<button>ログアウト</button>
</form>
<br/>
<!-- Ajaxを使ったログアウトボタン -->
<button id="logout">ログアウト for Ajax</button>
</body>
</html>
package com.example.component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class WelcomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
ちなみに・・・・後述する「Content Security Policy (CSP)」対策を有効にすると、<script>
要素内のJava Scriptが実行できなくなる可能性があります。その場合は、<script>
内のコードをjsファイルに移動するのがセキュリティ的には一番安全です。
InvalidSessionStrategy
の適用が簡単になる
Spring Security 4.1から、カスタマイズしたInvalidSessionStrategy
をSpring Securityに適用するためのメソッドが追加されました。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.sessionManagement()
.invalidSessionStrategy(new MyInvalidSessionStrategy());
}
}
サーブレットフィルタの適用位置の指定が柔軟になる
Spring Security 4.1から、サーブレットフィルタを指定した登録済みのフィルタと同じポジション(優先順)に追加するためのメソッドが追加されました。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.addFilterAt(new MyFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
UsernamePasswordAuthenticationFilter.class
と同じポジション(優先順)で、指定したサーブレットフィルタがSpring Securityに追加されます。同じ優先順位で登録されますが、ほとんどのケースで第1引数に指定したサーブレットフィルタの方が先に実行されます。
Spring MVCのリクエストマッピングのルールと連動したRequestMatcher
が追加される
Spring Security 4.1.1から、Spring MVCのリクエストマッピングのルールと連動したRequestMatcher
(MvcRequestMatcher
)が追加されました。メンテナンスバージョンアップでこのような機能追加が行われたのは、どうやら「cve-2016-5007」の脆弱性対策に関係しているからみたいです
cve-2016-5007の脆弱性の詳細は、以下のページをご覧ください。
- https://github.com/spring-projects/spring-security/issues/3964
- https://pivotal.io/security/cve-2016-5007
- https://jira.spring.io/browse/SEC-2534
なお、Spring Securityの公式リファレンスにもURLの解釈の違いによってSpring Securityの設定をすり抜けるケースの説明や、MvcRequestMatcher
を利用してすり抜けを防止する方法が紹介されています。
以下に、公式リファレンスで紹介しているサンプルコードを例に、URLの解釈の違いによって発生するすり抜け例とMvcRequestMatcher
の利用方法を紹介します。
まず、管理者向けのエンドポイントを用意します。
@RequestMapping("/admin")
public String admin() {
// ...
}
管理者向けのエンドポイントに対してアクセスポリシーを設ける場合、多くの開発者は以下のような定義にすると思います。
protected configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin").hasRole("ADMIN");
}
ぱっとみ問題ない気がしますが・・・Spring MVCのデフォルトのリクエストマッピングルールでは、「/admin」に加えて以下のパスへのアクセスもadmin
メソッドにマッピングされます。(ちなみに・・・このあたりの動作は、Spring MVC側の設定を変更することで動作を変えることもできるみたいです。)
- 「/admin/」
- 「/admin.拡張子」(例: /admin.html, /admin.json, /admin.xml, etc ...)
つまり・・・Spring Security側の設定が「/admin」だけだと「/admin/」や「/admin.html」がすり抜けてしまうわけです antMatchers
には複数のパスパターンを指定することができるので、「/admin/と/admin.*」も対象に加えればすり抜けない気もするけど、そんな単純な話じゃないのかな!?
で、この問題を解決するための仕組みとして、Spring MVCのリクエストマッピングルールと連携するRequestMatcher
(MvcRequestMatcher
)が導入されたわけです。MvcRequestMatcher
を利用する場合は、以下のような定義に変えてください。
protected configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/admin").hasRole("ADMIN"); // mvcMatchersメソッドを使う!!
}
Note: メソッド認可の併用について
公式リファレンスのノートには、アノテーションを利用したJavaメソッドへの認可(メソッドセキュリティ)との併用が推奨されていますね。仮にWebレイヤのセキュリティをすり抜けても、Javaメソッドに適切なアクセスポリシーが定義されていれば、不正なアクセスを防ぐことができます。機密度が高い情報を扱う処理については、メソッドセキュリティとの併用を検討した方がよいかもしれませんね。
さらに、Spring Security 4.1.2からは、HttpSecurity
の適用範囲を明示的に指定する際にもMvcRequestMatcher
が利用できるようになっています。
protected void configure(HttpSecurity http) throws Exception {
http.mvcMatcher("/admin/**") // 適用範囲の指定でも利用できる
.authorizeRequests()
.antMatchers("/admin/system").hasRole("SYS_ADMIN")
.antMatchers("/admin/infra").hasRole("INFRA_ADMIN");
}
or
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.mvcMatchers("/admin/system/**", "/admin/infra/**") // 適用範囲の指定でも利用できる
.and()
.authorizeRequests().anyRequest().hasRole("SUPER_ADMIN");
Spring SecurityをSpring MVCと一緒に使う場合は、MvcRequestMatcher
を使いようにしましょう!!って話ですね
Content Security Policy (CSP)がサポートされる
Spring Security 4.1から、XSS攻撃対策として、「Content Security Policy (CSP)」用のHTTPレスポンスヘッダ(Content-Security-PolicyやContent-Security-Policy-Report-Only)の出力がサポートされました。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.headers()
.contentSecurityPolicy("default-src 'self'");
}
}
HTTP Public Key Pinning (HPKP)がサポートされる
Spring Security 4.1から、偽造された証明書による中間者攻撃対策として、「HTTP Public Key Pinning (HPKP)」用のHTTPレスポンスヘッダ(Public-Key-PinsやPublic-Key-Pins-Report-Only)の出力がサポートされました。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.headers()
.httpPublicKeyPinning()
.addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=",
"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=");
}
}
CSRFトークンの保存先としてCookieがサポートされる
Spring Security 4.1から、CSRFトークンの保存先としてCookie(CookieCsrfTokenRepository
)がサポートされました。これは、AngularJSのCSRF対策機能のデフォルト動作をサポートするために追加されたようです。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.csrf()
.csrfTokenRepository(new CookieCsrfTokenRepository());
}
}
なお、デフォルトで使用されるCookie名は「XSRF-TOKEN」で、CSRFトークン値を送信するためのリクエストパラメータ名は「_csrf」、CSRFトークン値を送信するためのリクエストヘッダ名は「X-XSRF-TOKEN」です。リクエストヘッダ名のデフォルト値がHttpSessionCsrfTokenRepository
使用時とことなる点に注意が必要です。(HttpSessionCsrfTokenRepository
使用時は「X-CSRF-TOKEN」がデフォルトです)
認証後の遷移方法としてForwardがサポートされる(≠Redirect)
AuthenticationSuccessHandler
とAuthenticationFailureHandler
にて、遷移先にフォワードする実装クラスが追加されました。この変更により、遷移先でリクエストスコープの情報にアクセスすることが可能になります。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.formLogin()
.successForwardUrl("/authenticationSuccess") // ForwardAuthenticationSuccessHandlerが設定される
.failureForwardUrl("/authenticationError"); // ForwardAuthenticationFailureHandlerが設定される
}
}
@AuthenticationPrincipal
を使ってSpring MVCのHandlerメソッドに渡せるオブジェクトが柔軟になる
Spring Security 4.1から、@AuthenticationPrincipal
にexpression
属性が追加されました。expression
属性を利用すると、Spring MVCのHandlerメソッドの引数に、認証ユーザー情報(Authentication#getPrincipal()
メソッドの返り値)が保持する任意のプロパティ値をインジェクションすることができます。
package com.example.component;
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;
@RequestMapping("/profile")
@Controller
public class ProfileController {
@GetMapping
public String view(@AuthenticationPrincipal(expression = "account") Account account, Model model) {
model.addAttribute(account);
return "profile";
}
}
Spring Security 4.0では、UserDetails
をメソッドの引数として受け取る必要がありました。
@GetMapping
public String view(@AuthenticationPrincipal MyUserDetails userDetails, Model model) {
model.addAttribute(userDetails.getAccount()); // 明示的にメソッドを呼び出す
return "profile";
}
Webリソースへの認可でパス変数値が参照可能になる
Spring Security 4.1から、Webリソースへの認可に対してパス変数値を参照した認可制御が可能になりました !!! 個人的にはSpring Security 4.1の最大の目玉だと思っています
ここでは、指定されたユーザー名のアカウント情報にアクセスするWebリソース(/accounts/{username}
)への認可を考えてみましょう。リソースへアクセスする処理(Handlerメソッド)は、おそらくこんな感じになります。
package com.example.component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/accounts")
@Controller
public class AccountController {
@Autowired
NamedParameterJdbcOperations namedParameterJdbcOperations;
@GetMapping("{username}")
public String detail(@PathVariable String username, Model model) {
try {
Account account = namedParameterJdbcOperations.queryForObject(
"SELECT username, password, name, authorities FROM account WHERE username = :username"
, new MapSqlParameterSource("username", username),
new BeanPropertyRowMapper<>(Account.class));
model.addAttribute(account);
} catch (EmptyResultDataAccessException e) {
model.addAttribute(new Account()); // この例外ハンドリングは適切ではありません。適切なハンドリングをしましょ!!
}
return "accountDetail";
}
}
ここでは、管理者(ROLE_ADMIN
)はすべてのアカウント情報にアクセスすることができ、一般ユーザー(ROLE_USER
)は自分のアカウント情報のみアクセスできるように認可制御します。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.authorizeRequests()
.antMatchers("/accounts/{username}")
.access("isAuthenticated() and (hasRole('ADMIN') or (#username == principal.username))")
.anyRequest()
.authenticated();
}
}
パス変数「{パス変数名}
」には、expression
属性の中では「#パス変数名
」でアクセスできます。ナイスですね
メソッド認可用のアノテーションがメタアノテーションとして利用可能になる
Spring Security 4.1から、Javaメソッドへの認可時に使用するアノテーション(@PreAuthorize
など)がメタアノテーションとして使用できるようになりました。
package com.example.component;
import org.springframework.security.access.prepost.PreAuthorize;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@PreAuthorize("isAuthenticated() and (#username == principal.username"))
@Retention(RetentionPolicy.RUNTIME)
public @interface SelfPermission {
}
@SelfPermission // カスタムアノテーションを指定
@GetMapping("{username}")
public String detail(@PathVariable String username, Model model) {
// ...
}
なお、メソッド認可用のアノテーションを使用する場合は、Javaメソッドへの認可機能を有効化する必要があります。
@EnableGlobalMethodSecurity(prePostEnabled = true) // Javaメソッドへの認可機能を有効化
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
}
SCryptアルゴリズムのPasswordEncoder
がサポートされる
Spring Security 4.1から、SCryptアルゴリズムをサポートしたPasswordEncoder
の実装クラス(SCryptPasswordEncoder
)が追加されました。SCryptPasswordEncoder
を使用する場合は、Bouncy Castle Providerを依存アーティファクトに追加する必要があります。
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.54</version> <!-- 投稿時点の最新バージョン -->
</dependency>
@Configuration
public class SecurityConfig {
@Bean
PasswordEncoder passwordEncoder() {
return new SCryptPasswordEncoder();
}
}
SCryptPasswordEncoder
を使う場合は、パスワードを保持するカラムサイズを140文字に変更する必要があります。
CREATE TABLE account (
username NVARCHAR(128) PRIMARY KEY,
password CHAR(140) NOT NULL, -- 140文字へ変更
name NVARCHAR(128) NOT NULL,
authorities TEXT
);
-- password = demo(Hashed by SCrypt algorithm)
INSERT INTO account (username, password, name, authorities)
VALUES ('demo', '$e0801$xJb9axROvGvtu06vSPZBaDtD8Xpd/4NzTVBkP81E4ZrBSvnF276hkCj7R9YbxpVW3jY2e03Mqk8EFlgOH6FV3g==$2m3TbgXxYCuDK4PQt8eHDwoXOIahFEMCwznuT7DFSo0=', 'Kazuki Shimizu', 'ROLE_USER');
INSERT INTO account (username, password, name, authorities)
VALUES ('admin', '$e0801$xJb9axROvGvtu06vSPZBaDtD8Xpd/4NzTVBkP81E4ZrBSvnF276hkCj7R9YbxpVW3jY2e03Mqk8EFlgOH6FV3g==$2m3TbgXxYCuDK4PQt8eHDwoXOIahFEMCwznuT7DFSo0=', 'Administrator', 'ROLE_ADMIN,ROLE_USER');
PBKDF2アルゴリズムのPasswordEncoder
がサポートされる
Spring Security 4.1から、PBKDF2アルゴリズムをサポートしたPasswordEncoder
の実装クラス(Pbkdf2PasswordEncoder
)が追加されました。
@Configuration
public class SecurityConfig {
@Bean
PasswordEncoder passwordEncoder() {
return new Pbkdf2PasswordEncoder();
}
}
SCryptPasswordEncoder
を使う場合は、パスワードを保持するカラムサイズを96文字に変更する必要があります。
CREATE TABLE account (
username NVARCHAR(128) PRIMARY KEY,
password CHAR(96) NOT NULL, -- 96文字へ変更
name NVARCHAR(128) NOT NULL,
authorities TEXT
);
-- password = demo(Hashed by PBKDF2 algorithm)
INSERT INTO account (username, password, name, authorities)
VALUES ('demo', 'a35fd0d412a681a1a35fd0d412a681a154793f9cc057581cf317649f4ab1766d15aea67b9bef7f062a348907160e7752', 'Kazuki Shimizu', 'ROLE_USER');
INSERT INTO account (username, password, name, authorities)
VALUES ('admin', 'a35fd0d412a681a1a35fd0d412a681a154793f9cc057581cf317649f4ab1766d15aea67b9bef7f062a348907160e7752', 'Administrator', 'ROLE_ADMIN,ROLE_USER');
テスト時に匿名ユーザー用の認証情報のセットアップができる
Spring Security 4.1から、匿名ユーザーの認証情報をセットアップするためのアノテーション(@WithAnonymousUser
)が追加されました。
package com.example;
import com.example.component.AccountController;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.ui.ExtendedModelMap;
@RunWith(SpringRunner.class)
@SpringBootTest
public class Sec41DemoApplicationTests {
@Autowired
AccountController accountController;
@WithAnonymousUser // 匿名ユーザー用の認証情報をセットアップ
@Test(expected = AccessDeniedException.class)
public void detailByAnonymous() throws Exception {
accountController.detail("demo", new ExtendedModelMap());
}
}
テスト時に利用するUserDetailsService
を指定できる
Spring Security 4.1から、@WithUserDetails
にuserDetailsServiceBeanName
属性が追加されました。userDetailsServiceBeanName
属性を使用すると、テスト時に使用するUserDetailsService
のBeanを明示的に指定することができます。
@WithUserDetails(value = "admin", userDetailsServiceBeanName = "myUserDetailsService")
@Test(expected = AccessDeniedException.class)
public void anonymous() throws Exception {
accountController.detail("demo", new ExtendedModelMap());
}
テスト時に利用する認証情報などを指定するアノテーションがメタアノテーションとして利用可能になる
Spring Security 4.1から、認証情報などを指定するアノテーション(@WithMockUser
など)をメタアノテーションとして使用できるようになりました。
@WithMockUser("demo")
@Retention(RetentionPolicy.RUNTIME)
public @interface DemoUser {}
@DemoUser // カスタムアノテーションを指定
@Test(expected = AccessDeniedException.class)
public void anonymous() throws Exception {
accountController.detail("admin", new ExtendedModelMap());
}
SecurityMockMvcResultMatchers#withAuthorities
メソッドの引数で扱える型が柔軟になる
Spring Security 4.1から、SecurityMockMvcResultMatchers#withAuthorities
メソッドで、GrantedAuthority
のサブクラスをキャストなしで扱えるようになりました。
package com.example;
import com.example.component.AccountController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import java.util.Collections;
import java.util.List;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
public class Sec41DemoApplicationTests {
@Autowired
WebApplicationContext was;
MockMvc mockMvc;
@Autowired
AccountController accountController;
@Before
public void setupMockMvc() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(was)
.apply(springSecurity())
.build();
}
@Test
public void login() throws Exception {
// 要素の型を具象クラス(SimpleGrantedAuthority)にしても・・・
List<SimpleGrantedAuthority> expectedAuthorities = new ArrayList<>();
expectedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
mockMvc.perform(
formLogin().user("demo").password("demo"))
.andExpect(status().isFound()).andExpect(redirectedUrl("/"))
.andExpect(authenticated().withAuthorities(expectedAuthorities)); // キャストなしでwithAuthoritiesに指定可能!!
}
}
でも・・・・このケースだと、Spring Securityから提供されているAuthorityUtils#createAuthorityList
メソッドを使う方がいいですけどね・・・(そうすればSpring Security 4.0でも問題なくコンパイル可能できます)
@Test
public void login() throws Exception {
List<GrantedAuthority> expectedAuthorities = AuthorityUtils.createAuthorityList("ROLE_USER");
mockMvc.perform(
formLogin().user("demo").password("demo"))
.andExpect(status().isFound()).andExpect(redirectedUrl("/"))
.andExpect(authenticated().withAuthorities(expectedAuthorities));
}
まとめ
今回はSpring Security 4.1の主な変更点を紹介しました。コンフィギュレーションが簡単になったり、Webリソースへの認可でパス変数値の参照が可能になったり、新しいPasswordEncoder
が追加されたり、新しいセキュリティヘッダがサポートされたりと、Spring Security 4.1は確実に進化しています。Spring Securityはサーブレットフィルタとして実装されているため、Spring MVC以外のフレームワークにも適用できます。Webアプリケーションのセキュリティ対策には是非Spring Securityを!!