spring-security
spring
Thymeleaf
RBAC

Spring SecurityでRBACなサンプルを作ってみた

@kawasimaさんが以前に書かれていた記事「業務システムにおけるロールベースアクセス制御」で公開されていたサンプルコードを、Spring Security + Spring MVC + Thymeleafで作成してみました(Spring Boot 2も利用してます)。

コード -> https://github.com/MasatoshiTada/rbac-example-springsecurity

この記事を読む前に、上記@kawasimaさんのブログを先に読んでおくことをおすすめします。

Spring SecurityのJava Config作成

src/main/java/com/example/security/SecurityConfig.java
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) throws Exception {
        // Spring Security ignores URLs of static resources
        web.ignoring().requestMatchers(
                PathRequest.toStaticResources().atCommonLocations());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // configure login
        http.formLogin()
                .loginPage("/login")
                .usernameParameter("account")
                .passwordParameter("password")
                .defaultSuccessUrl("/", true)
                .permitAll();
        // configure logout
        http.logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")
                .permitAll()
                .invalidateHttpSession(true);
        // configure URL authorization
        http.authorizeRequests()
                .mvcMatchers("/signup").permitAll()
                .mvcMatchers(HttpMethod.GET, "/issues/").hasAuthority("readIssue")
                .mvcMatchers(HttpMethod.GET, "/issue/new").hasAuthority("writeIssue")
                .mvcMatchers(HttpMethod.POST, "/issues/").hasAuthority("writeIssue")
                .mvcMatchers("/users").hasAuthority("manageUser")
                .anyRequest().authenticated();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

ロールではなくパーミッションをhasAuthority()でチェックしているのがポイントです。
@kimullaaさんのブログ「SpringSecurity 権限に基づいた認可をする」を参考にしました。

ビジネスロジッククラス

src/main/java/com/example/services/IssueService.java
@Service
public class IssueService {

    // フィールド等省略

    @PreAuthorize("hasAuthority('readIssue')")
    @Transactional(readOnly = true)
    public List<Issue> findAll() {
        return issueRepository.findAll();
    }

    @PreAuthorize("hasAuthority('writeIssue')")
    @Transactional
    public void register(Issue issue, String account) {
        issueRepository.register(issue, account);
    }
}
src/main/java/com/example/services/UserService.java
@Service
public class UserService {

    // フィールド等省略

    @Transactional
    public void register(User user, String roleName) {
        String encodedPassword = passwordEncoder.encode(user.getPassword());
        user.setPassword(encodedPassword);
        userRepository.register(user, roleName);
    }

    @PreAuthorize("hasAuthority('manageUser')")
    @Transactional(readOnly = true)
    public List<User> findAll() {
        return userRepository.findAll();
    }
}

@PreAuthorize("hasAuthority('パーミッション名')")で権限チェックをしています。やはりここでも、ロール名ではなくパーミッション名で指定しています。

ビュー

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <!--/* 省略 */-->
</head>
<body>
<div class="ui fixed inverted menu">
    <div class="ui container">
        <a href="#" class="header item">
            RBAC Example
        </a>
        <a th:href="@{/}" href="../index.html" class="item"
           sec:authorize="isAuthenticated()">
            Home
        </a>
        <a th:href="@{/issues/}" href="../issue/list.html" class="item"
           sec:authorize="isAuthenticated() and hasAuthority('readIssue')">
            Issue
        </a>
        <a th:href="@{/users/}" href="../userAdmin/list.html" class="item"
           sec:authorize="isAuthenticated() and hasAuthority('manageUser')">
            Users
        </a>

    <!--/* 省略 */-->

sec:authorize属性とhasAuthority('パーミッション名')で権限チェックをしています。やはりここでも、ロール名ではなくパーミッション名で指定しています。

ロールをハードコーディングしない!

あるロールができることが増える(=あるロールが持つパーミッションが増える)ことって、けっこう頻繁に発生すると思います。例えば、guestロールでもIssueの書き込みができるようにしたい、とか。

そのような変更が発生した場合、ロールでチェックしているとソースコードへの修正が必要になります。

しかし、ソースコード内ではパーミッションでチェックしておけば、データベース側にレコードを追加するだけで済みます。