Spring SecurityのロールにROLE_
プレフィックスを付与すべきかどうか、という話題が出たので調べてみました。個人的には使い分け(およびプロジェクトメンバへの説明)が面倒なので一律に付与すべきという結論になりました。
1. ROLE_プレフィックスと認可処理の関係
ロールにROLE_
プレフィックスが付与されているかどうかで、認可処理の結果が変わりました。
検証は「Spring Security 4.1.4」で行いました。検証内容の詳細については「3. 検証用のサンプルコード」を参照ください。
| 項番 | ロール
(権限) | ロールによる認可処理 | 結果 |
|:------:|:---------:|:-------------------------------|:-------|:-------|
| 1 | ROLE_EMPLOYEE | @PreAuthorize("hasRole('ROLE_EMPLOYEE')")
| 実行可能 |
| 2 | ROLE_EMPLOYEE | @PreAuthorize("hasRole('EMPLOYEE')")
| 実行可能 |
| 3 | EMPLOYEE | @PreAuthorize("hasRole('ROLE_EMPLOYEE')")
| 権限エラー(AccessDeniedException) |
| 4 | EMPLOYEE | @PreAuthorize("hasRole('EMPLOYEE')")
| 権限エラー(AccessDeniedException) |
-
項番1,2から、
hasRole()
で指定するロールはROLE_
プレフィックスを省略することができる(プレフィックスが付与されていてもいなくても動作は同じ) -
項番3,4から、ロールによる認可処理を行う場合、ロールには必ず
ROLL_
プレフィックスを付与しなけばならない(付与しないと権限エラーで実行できない)
個人的には使い分け(およびプロジェクトメンバへの説明)が面倒なので一律に付与すべきという結論になりました。
2. @WithMockUser
にROLE_プレフィックスを付けてはいけない
検証時に分かったことですが、@WithMockUser
のroles属性の値にROLE_プレフィックスが付与されているとjava.lang.IllegalArgumentException
でエラーになります。
//@WithMockUser(username="0001", roles="ROLE_EMPLOYEE")
// Caused by: java.lang.IllegalArgumentException: roles cannot start with ROLE_ Got ROLE_EMPLOYEE
@WithMockUser(username="0001", roles="EMPLOYEE")
// 作成されるロールは ROLE_EMPLOYEE
ROLE_
プレフィックスを付与しないでも、作成されるロールにはROLE_
プレフィックスが付与されているようです。
3. 検証用のサンプルコード
参考までに、検証用に作成したサンプルコードを記載します。
プロジェクトをゼロから作成するのが手間なので、TEASOLUNA5.x(version 5.3.0.RELEASE)のシングルプロジェクトを利用したいと思います。
- TEASOLUNA5.x(version 5.3.0.RELEASE)
- Spring Framework 4.3.5
- Spring MVC 4.3.5
- Spring Security 4.1.4
バージョンの詳細についてはTERASOLUNA5.xのガイドラインの「2.1.3. 利用するOSSのバージョン」を参照ください。
3.1. プロジェクトの作成
はじめてのSpring MVCアプリケーションの「2.3.2. 新規プロジェクト作成」を参考にTERASOLUNA5.3.0のシングルプロジェクトを作成します。
mvn archetype:generate -B^
-DarchetypeGroupId=org.terasoluna.gfw.blank^
-DarchetypeArtifactId=terasoluna-gfw-web-blank-archetype^
-DarchetypeVersion=5.3.0.RELEASE^
-DgroupId=com.example.rollprefix^
-DartifactId=roll-prefix-demo^
-Dversion=1.0.0-SNAPSHOT
3.2. 認可対象のService
引数で渡された文字列を返すだけのサービスです。
認可処理の確認なので@PreAuthorize
でhasRole()
のロールの部分にROLE_
プレフィックスがあるかないかが違うだけです。
package com.example.rollprefix.domain.service.demo;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class EchoServiceImpl implements EchoService {
@PreAuthorize("hasRole('ROLE_EMPLOYEE')")
@Override
public String speackWithPrefix(String value) {
return value;
}
@PreAuthorize("hasRole('EMPLOYEE')")
@Override
public String speackNoPrefix(String value) {
return value;
}
}
3.3. 検証用のUserDetailsService
ログインユーザのIDが0001
の場合はROLE_EMPLOYEE
(プレフィックスあり)、それ以外の場合はEMPLOYEE
(プレフィックスなし)のロールが付与された認証情報を返すデモ用のUserDetailsServiceです。
package com.example.rollprefix.domain.service.userdetails;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import com.example.rollprefix.domain.model.Account;
public class AccountUserDetailsService implements UserDetailsService {
private static final Logger LOGGER = LoggerFactory
.getLogger(AccountUserDetailsService.class);
private String PREFIX_USER_ID = "0001";
/**
* <p>
* username が 0001 の場合、ROLE_EMPLOYEE
* <p>
* username がそれ以外 の場合、EMPLOYEE
* @see org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername(java.lang.String)
*/
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
Account account = new Account();
account.setAccountId(username);
account.setAccountPass("dummyPass");
account.setAccountName("山田 太郎");
if (PREFIX_USER_ID.equals(username)) {
AccountUserDetails userDetails = new AccountUserDetails(account,
AuthorityUtils.createAuthorityList("ROLE_EMPLOYEE"));
LOGGER.debug("userDetails={}", userDetails);
return userDetails;
} else {
AccountUserDetails userDetails = new AccountUserDetails(account,
AuthorityUtils.createAuthorityList("EMPLOYEE"));
LOGGER.debug("userDetails={}", userDetails);
return userDetails;
}
}
}
3.4. 検証用のテストクラス
@WithUserDetails
アノテーションを利用して、先ほど作成したAccountUserDetailsService
でログインユーザの認証情報を作成します。
package com.example.rollprefix.domain.service.demo;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.is;
import javax.inject.Inject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:test-context.xml" })
public class EchoServiceImplTest {
private static final Logger LOGGER = LoggerFactory
.getLogger(EchoServiceImplTest.class);
@Inject
EchoService echoService;
/**
* ROLE_EMPLOYEE で @PreAuthorize("hasRole('ROLE_EMPLOYEE')")
*/
@WithUserDetails("0001")
@Test
public void testSpeackWithPrefix01() {
LOGGER.debug("testSpeackWithPrefix01 : ROLE_EMPLOYEE");
assertThat(echoService.speackWithPrefix("piyo"), is("piyo"));
}
/**
* ROLE_EMPLOYEE で @PreAuthorize("hasRole('EMPLOYEE')")
*/
@WithUserDetails("0001")
@Test
public void testSpeackNoPrefix01() {
LOGGER.debug("testSpeackNoPrefix01 : ROLE_EMPLOYEE");
assertThat(echoService.speackNoPrefix("nyah"), is("nyah"));
}
/**
* EMPLOYEE で @PreAuthorize("hasRole('ROLE_EMPLOYEE')")
*/
@WithUserDetails("0002")
@Test(expected = AccessDeniedException.class)
//@Test
public void testSpeackWithPrefix02() {
LOGGER.debug("testSpeackWithPrefix02 : EMPLOYEE");
assertThat(echoService.speackWithPrefix("piyo"), is("piyo"));
}
/**
* EMPLOYEE で @PreAuthorize("hasRole('EMPLOYEE')")
*/
@WithUserDetails("0002")
@Test(expected = AccessDeniedException.class)
//@Test
public void testSpeackNoPrefix02() {
LOGGER.debug("testSpeackNoPrefix02 : EMPLOYEE");
assertThat(echoService.speackNoPrefix("nyah"), is("nyah"));
}
/**
* ROLE_EMPLOYEE で @PreAuthorize("hasRole('ROLE_EMPLOYEE')")
*/
//@WithMockUser(username="0001", roles="ROLE_EMPLOYEE")
// Caused by: java.lang.IllegalArgumentException: roles cannot start with ROLE_ Got ROLE_EMPLOYEE
@WithMockUser(username="0001", roles="EMPLOYEE")
// 作成されるロールは ROLE_EMPLOYEE
@Test
public void testSpeackWithPrefix03() {
LOGGER.debug("testSpeackWithPrefix03 : ROLE_EMPLOYEE");
assertThat(echoService.speackWithPrefix("piyo"), is("piyo"));
}
}
3.5. test-context.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"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
">
<context:property-placeholder
location="classpath*:/META-INF/spring/*.properties" />
<bean id="exceptionLogger" class="org.terasoluna.gfw.common.exception.ExceptionLogger" />
<import resource="classpath:META-INF/spring/roll-prefix-demo-domain.xml" />
<import resource="classpath:META-INF/spring/spring-security.xml" />
</beans>
3.6. spring-security.xml
今回の検証のポイントとなる設定だけ記載しますので、不明な点がありましたらTERASOLUNA5.xのガイドラインの「9.2. 認証」を参照ください。
<!-- <sec:authentication-manager /> -->
<!-- 実装したデモ用のuserDetailsService -->
<bean id="accountUserDetailsService"
class="com.example.rollprefix.domain.service.userdetails.AccountUserDetailsService" />
<!-- 平文のpasswordEncoder -->
<bean id="passwordEncoder"
class="org.springframework.security.crypto.password.NoOpPasswordEncoder" />
<sec:authentication-manager>
<sec:authentication-provider
user-service-ref="accountUserDetailsService">
<sec:password-encoder ref="passwordEncoder" />
</sec:authentication-provider>
</sec:authentication-manager>
<!-- メソッド認可制御を有効 -->
<sec:global-method-security
pre-post-annotations="enabled" />