コンテンツにスキップ

ありがとうございます。

ここから 第10段階:Spring Security 6 認証・認可設計 に進みます。

今回の教材管理システムでは、単純なログインだけではなく、以下の権限制御が必要になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
管理者
 ├ 全教材参照
 ├ 教材審査
 ├ 公開承認
 └ 全ユーザー管理

教師
 ├ 自分の教材作成
 ├ 自分の教材編集
 ├ 審査申請
 └ 他教師教材は編集不可

メンター
 ├ 将来的に学習状況参照
 └ 質問対応

受講者
 ├ 公開教材閲覧
 └ 学習履歴登録

第10段階の構成

作成するクラス:

1
2
3
4
5
6
security
├ SecurityConfig.java
├ CustomUserDetails.java
├ CustomUserDetailsService.java
├ LoginUser.java
└ PasswordConfig.java

1. Spring Security依存関係

Gradle:

1
2
3
4
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
}

2. SecurityConfig

Spring Security 6では、

WebSecurityConfigurerAdapter

は使用しません。

Bean方式で設定します。


SecurityConfig.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.example.lms.security;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final CustomUserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain securityFilterChain(
            HttpSecurity http) throws Exception {

        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(
                    "/css/**",
                    "/js/**"
                ).permitAll()

                .requestMatchers(
                    "/admin/**"
                )
                .hasRole("ADMIN")

                .requestMatchers(
                    "/teacher/**"
                )
                .hasRole("TEACHER")

                .requestMatchers(
                    "/student/**"
                )
                .hasAnyRole(
                    "STUDENT",
                    "TEACHER",
                    "ADMIN"
                )

                .anyRequest()
                .authenticated()
            )

            .formLogin(login -> login
                .loginPage("/login")
                .defaultSuccessUrl(
                    "/dashboard",
                    true
                )
                .permitAll()
            )

            .logout(logout -> logout
                .logoutSuccessUrl("/login")
            );

        return http.build();
    }
}

3. PasswordEncoder

パスワードは必ずBCrypt化します。


PasswordConfig.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.example.lms.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordConfig {

    @Bean
    public PasswordEncoder passwordEncoder(){

        return new BCryptPasswordEncoder();

    }
}

4. User Entityとの連携

前回作成したUser:

1
2
3
4
5
6
users

id
username
email
password

をSpring Security用に変換します。


5. CustomUserDetails

Spring Securityが利用するユーザー情報です。


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.example.lms.security;

import com.example.lms.entity.user.User;
import lombok.Getter;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.stream.Collectors;

@Getter
public class CustomUserDetails implements UserDetails {

    private final User user;

    public CustomUserDetails(User user){

        this.user = user;

    }


    @Override
    public java.util.Collection<
            ? extends org.springframework.security.core.GrantedAuthority>
    getAuthorities(){

        return user.getRoles()
                .stream()
                .map(role ->
                    new SimpleGrantedAuthority(
                        "ROLE_" + role.getCode()
                    )
                )
                .collect(
                    Collectors.toSet()
                );
    }


    @Override
    public String getPassword(){

        return user.getPassword();

    }


    @Override
    public String getUsername(){

        return user.getEmail();

    }


    @Override
    public boolean isEnabled(){

        return user.isEnabled();

    }

}

6. UserDetailsService

DBからユーザーを取得します。


CustomUserDetailsService.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.lms.security;

import com.example.lms.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;


@Service
@RequiredArgsConstructor
public class CustomUserDetailsService
        implements UserDetailsService {


    private final UserRepository userRepository;


    @Override
    public UserDetails loadUserByUsername(
            String email){

        return userRepository
            .findByEmail(email)
            .map(CustomUserDetails::new)
            .orElseThrow(
                () ->
                new UsernameNotFoundException(
                    "ユーザーが存在しません"
                )
            );
    }
}

7. UserRepository

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.example.lms.repository;

import com.example.lms.entity.user.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository
        extends JpaRepository<User,Long>{

    Optional<User> findByEmail(
            String email
    );

}

8. Login画面

配置:

1
2
3
templates

└ login.html

login.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>

<title>
ログイン
</title>

<link
href="/css/bootstrap.min.css"
rel="stylesheet">

</head>

<body>

<div class="container mt-5">

<h2>
教材管理システム
</h2>


<form method="post"
      th:action="@{/login}">


<div class="mb-3">

<label>
メールアドレス
</label>

<input
class="form-control"
name="username"
type="email">

</div>


<div class="mb-3">

<label>
パスワード
</label>

<input
class="form-control"
name="password"
type="password">

</div>


<button
class="btn btn-primary">
ログイン
</button>


</form>

</div>

</body>

</html>

9. LoginController

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Controller
public class LoginController {


    @GetMapping("/login")
    public String login(){

        return "login";

    }

}

10. ログインユーザー取得

ControllerやServiceで、

現在ログイン中の教師を取得します。


LoginUser.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.example.lms.security;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;


@Component
@RequiredArgsConstructor
public class LoginUser {


    public User get(){

        Authentication authentication =
            SecurityContextHolder
                .getContext()
                .getAuthentication();


        CustomUserDetails details =
            (CustomUserDetails)
            authentication.getPrincipal();


        return details.getUser();

    }
}

利用例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@PostMapping("/teacher/content/create")
public String create(){

    User teacher =
        loginUser.get();

    service.create(
        teacher
    );

    return "redirect:/teacher/content";
}

11. 教師の教材編集権限制御

重要部分です。

URL制御だけでは不足します。

理由:

1
2
3
4
5
6
7
教師A

/teacher/content/1/edit

↓

教師B教材

を防ぐ必要があります。


Service側チェック

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private void checkOwner(
        Content content,
        User loginUser){

    if(!content.getAuthor()
        .getId()
        .equals(loginUser.getId())){

        throw new AccessDeniedException(
            "編集できません"
        );

    }
}

12. Method Security利用

例えば:

管理者承認:

1
2
3
4
5
6
7
@PreAuthorize(
"hasRole('ADMIN')")
@Transactional
public void approve(
        Long versionId){

}

教師登録:

1
2
3
4
5
6
@PreAuthorize(
"hasRole('TEACHER')")
@Transactional
public Long create(){

}

13. Thymeleaf権限制御

例:

教師だけ表示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<div sec:authorize="hasRole('TEACHER')">

<a href="/teacher/content/create"
class="btn btn-primary">

教材作成

</a>

</div>

管理者:

1
2
3
4
5
6
7
8
9
<div sec:authorize="hasRole('ADMIN')">

<a href="/admin/approval">

審査管理

</a>

</div>

14. ログイン後リダイレクト

ロール別に変更します。

例:

1
AuthenticationSuccessHandler

を追加します。

結果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
ADMIN

↓

/admin


TEACHER

↓

/teacher


STUDENT

↓

/student

15. 現在の認証フロー

完成形:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
ブラウザ

↓

/login

↓

Spring Security

↓

CustomUserDetailsService

↓

users
roles

↓

Authentication

↓

Controller

↓

Service

↓

業務処理

第10段階終了時点

完成したもの:

✅ ログイン
✅ BCryptパスワード
✅ Role認可
✅ 教師・管理者画面分離
✅ ログインユーザー取得
✅ 教材所有者チェック設計


次の 第11段階 では、教材作成機能を実装します。

内容:

  1. 教材登録Form設計

  2. Bean Validation

  3. 教材タイプ別入力画面

  4. 動画登録処理

  5. Markdown登録処理

  6. クイズ登録処理(複数正解対応)

  7. ファイルアップロード処理

  8. Cloudflare R2保存設計

  9. ContentVersion発行処理

ここから実際に教師が教材を作成できる機能になります。