spring-securityのROLE_プレフィックスと認可処理の動作について

Spring Securityのロールに`ROLE_`プレフィックスを付与すべきかどうか、という話題が出たので調べてみました。**個人的には使い分け（およびプロジェクトメンバへの説明）が面倒なので一律に付与すべきという結論になりました。**

# 1. ROLE_プレフィックスと認可処理の関係
ロールに`ROLE_`プレフィックスが付与されているかどうかで、認可処理の結果が変わりました。
検証は「Spring Security 4.1.4」で行いました。検証内容の詳細については「[3. 検証用のサンプルコード](#3-検証用のサンプルコード)」を参照ください。

| 項番 | ロール<br />(権限) | ロールによる認可処理 | 結果 |
|:------:|:---------:|:-------------------------------|:-------|:-------|
| 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`でエラーになります。

```java:@WithMockUserにROLL_プレフィックスを付与するとエラー
//@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のバージョン](http://terasolunaorg.github.io/guideline/5.3.0.RELEASE/ja/Overview/FrameworkStack.html#oss)」を参照ください。

### 3.1. プロジェクトの作成
はじめてのSpring MVCアプリケーションの「[2.3.2. 新規プロジェクト作成](http://terasolunaorg.github.io/guideline/5.3.0.RELEASE/ja/Overview/FirstApplication.html#id3)」を参考にTERASOLUNA5.3.0のシングルプロジェクトを作成します。

```console:プロジェクトの作成
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_`プレフィックスがあるかないかが違うだけです。

```java:EchoServiceImpl
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です。

```java:AccountUserDetailsService.java
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`でログインユーザの認証情報を作成します。

```java:EchoServiceImplTest.java
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: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. 認証](http://terasolunaorg.github.io/guideline/5.3.0.RELEASE/ja/Security/Authentication.html)」を参照ください。

```xml:spring-security.xml(ポイントだけ)
<!-- <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" />
```