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

@kawasimaさんが以前に書かれていた記事「[業務システムにおけるロールベースアクセス制御](https://qiita.com/kawasima/items/8dd7eda743f2fdcad78e)」で公開されていた[サンプルコード](https://github.com/kawasima/rbac-example/)を、Spring Security + Spring MVC + Thymeleafで作成してみました（Spring Boot 2も利用してます）。

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

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

# Spring SecurityのJava Config作成

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.formLogin(formLogin -> formLogin
                // configure login
                .loginPage("/login")
                .usernameParameter("account")
                .passwordParameter("password")
                .defaultSuccessUrl("/", true)
                .permitAll()
        ).logout(logout -> logout
                // configure logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")
                .permitAll()
                .invalidateHttpSession(true)
        ).authorizeHttpRequests(authorize -> authorize
                // configure URL authorization
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .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()
        );
        return http.build();
    }

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

ロールではなくパーミッションを`hasAuthority()`でチェックしているのがポイントです。
@kimullaaさんのブログ「[SpringSecurity 権限に基づいた認可をする](https://www.kimullaa.com/entry/2016/05/08/140538)」を参考にしました。

# ビジネスロジッククラス

```java: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);
    }
}
```

```java: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('パーミッション名')")`で権限チェックをしています。やはりここでも、ロール名ではなくパーミッション名で指定しています。

# ビュー

```html
<!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の書き込みができるようにしたい、とか。

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

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