🍃

Spring Security 6.0でロール階層(RoleHierarchy)を実現する

2023/03/18に公開

その前に

業務でSpring Securityを使用してロール階層を実装しようとしたときに、ググっても古い実装方法しかでてこなかったため、1人日ほどかけてドキュメントを読み漁ってやっと実装できました。

日本語の記事もなかったためここに書き残そうと思います。

Githubに置きました

https://github.com/SeiyaIwabuchi/hierarchical-roles-with-spring-security-6.0

ロール階層ってなに?

たとえば、職場に「勤怠打刻システム」があったとして

  • システム管理者
  • 運用スタッフ
  • 一般ユーザー

というロールがあったとき、それぞれのロールは

  1. 一般ユーザーは、自分の出退勤時刻を管理できる。
  2. 運用スタッフは、上司が自分の部下の出退勤の時刻を管理できる。
  3. システム管理者は、所属する会社の全職員の出退勤の時刻を管理できる。

ができるとする。

システム管理者がとある部署の部長で部下の勤怠の管理をし、部長自身も出退勤時刻の管理を行うことができる。


このようなシステムを構築する場合、実際の管理者には「システム管理者、運用スタッフ、一般ユーザー」という権限が付与されなくてはならないので、1ユーザーで3つのロールを持つことになります。
そして、ロジックではユーザーがどのようなロールを持っているかチェックし、操作の可否判定を行います。このような場合、ロール階層を設定しておけば1ユーザーに1ロールを付与すればチェックも一回で済みます。

ソース

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests((requests) -> requests
                        .anyRequest().authenticated()
                )
                .formLogin(Customizer.withDefaults())
                .logout((logout) -> logout.permitAll());

        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {

        UserDetails userAdmin =
                User.withDefaultPasswordEncoder()
                        .username("admin")
                        .password("password")
                        .roles("ADMIN")
                        .build();

        UserDetails userStaff =
                User.withDefaultPasswordEncoder()
                        .username("staff")
                        .password("password")
                        .roles("STAFF")
                        .build();

        UserDetails userBasic =
                User.withDefaultPasswordEncoder()
                        .username("basic")
                        .password("password")
                        .roles("BASIC")
                        .build();

        return new InMemoryUserDetailsManager(userAdmin, userStaff, userBasic);
    }

    // --------- ここ! ---------
    @Bean
    public RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        String hierarchy = "ROLE_ADMIN > ROLE_STAFF \n ROLE_STAFF > ROLE_BASIC";
        roleHierarchy.setHierarchy(hierarchy);
        return roleHierarchy;
    }
    // -------------------------
}

Beanとして定義するだけ

@Bean
public RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
    String hierarchy = "ROLE_ADMIN > ROLE_STAFF \n ROLE_STAFF > ROLE_BASIC";
    roleHierarchy.setHierarchy(hierarchy);
    return roleHierarchy;
}

本当に動いているか確認

ユーザー「basic」でログイン

ユーザー「staff」でログイン

ユーザー「admin」でログイン

HTMLも載せておきます

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>ユーザ詳細情報</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css" integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls" crossorigin="anonymous">
</head>
<body>
<div style="
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
">
    <h3>ユーザ詳細情報</h3>
    <table class="pure-table pure-table-bordered">
        <tbody>
        <tr>
            <td>
                ユーザ名
            </td>
            <td sec:authentication="name">
            </td>
        </tr>
        <tr class="pure-table-odd">
            <td>BASICロール</td>
            <td>
                <span sec:authorize="hasRole('ROLE_BASIC')"></span>
                <span sec:authorize="!hasRole('ROLE_BASIC')">×</span>
            </td>
        </tr>
        <tr>
            <td>STAFFロール</td>
            <td>
                <span sec:authorize="hasRole('ROLE_STAFF')"></span>
                <span sec:authorize="!hasRole('ROLE_STAFF')">×</span>
            </td>
        </tr>
        <tr class="pure-table-odd">
            <td>ADMINロール</td>
            <td>
                <span sec:authorize="hasRole('ROLE_ADMIN')"></span>
                <span sec:authorize="!hasRole('ROLE_ADMIN')">×</span>
            </td>
        </tr>
        <tr>
            <td>ロールリスト</td>
            <td th:text="${role}">
            </td>
        </tr>
        </tbody>
    </table>
    <form th:action="@{/logout}" style="margin:10px">
        <input type="submit" value="ログアウト">
    </form>
</div>
</body>
</html>

参考にしたサイト

https://spring.io/guides/gs/securing-web/
https://qiita.com/opengl-8080/items/eb3bf3b5301bae398cc2
https://baubaubau.hatenablog.com/entry/2020/12/04/215710
https://www.baeldung.com/role-and-privilege-for-spring-security-registration
https://docs.spring.io/spring-security/reference/servlet/authorization/architecture.html#authz-hierarchical-roles

Discussion