spring-security
spring
TERASOLUNA5

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

More than 1 year has passed since last update.

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に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のバージョン」を参照ください。

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

引数で渡された文字列を返すだけのサービスです。
認可処理の確認なので@PreAuthorizehasRole()のロールの部分にROLE_プレフィックスがあるかないかが違うだけです。

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です。

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

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

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. 認証」を参照ください。

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" />