4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?