ありがとうございます。
ここから 第20段階:Spring Security認証・認可実装設計 に進みます。
今回は教材管理システムの基盤となる ログイン・権限管理 を実装レベルまで設計します。
対象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | 管理者
└ システム管理
└ 教材承認
教師
└ 教材作成
└ 教材申請
メンター
└ 受講者フォロー(将来)
受講者
└ 教材受講
└ 学習履歴
|
第20段階 全体構成
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 | Login
|
v
Spring Security
|
UserDetailsService
|
User
|
+-----------+-----------+
Role Permission
| |
ROLE_ADMIN CONTENT_APPROVE
ROLE_TEACHER CONTENT_CREATE
ROLE_STUDENT CONTENT_VIEW
|
1. Spring Security依存関係
Gradle:
| dependencies {
implementation(
'org.springframework.boot:spring-boot-starter-security'
)
implementation(
'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
)
}
|
2. User Entity設計
ユーザー情報の中心です。
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 | package com.example.lms.domain.user.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.Set;
@Entity
@Table(name="users")
@Getter
@Setter
public class User {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
@Column(nullable=false)
private String username;
@Column(nullable=false,unique=true)
private String email;
@Column(nullable=false)
private String password;
@Enumerated(EnumType.STRING)
private UserStatus status;
@ManyToMany(fetch=FetchType.EAGER)
@JoinTable(
name="user_roles",
joinColumns=
@JoinColumn(name="user_id"),
inverseJoinColumns=
@JoinColumn(name="role_id")
)
private Set<Role> roles;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
|
3. UserStatus
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | public enum UserStatus {
ACTIVE,
LOCKED,
INACTIVE,
DELETED
}
|
4. Role Entity
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 | @Entity
@Table(name="roles")
@Getter
@Setter
public class Role {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
@Column(unique=true)
private String code;
private String name;
@ManyToMany(
fetch=FetchType.EAGER
)
@JoinTable(
name="role_permissions",
joinColumns=
@JoinColumn(name="role_id"),
inverseJoinColumns=
@JoinColumn(name="permission_id")
)
private Set<Permission> permissions;
}
|
5. Permission Entity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | @Entity
@Table(name="permissions")
@Getter
@Setter
public class Permission {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
@Column(unique=true)
private String code;
private String name;
}
|
6. 権限マスタ初期値
Role
| INSERT INTO roles
(code,name)
VALUES
('ADMIN','管理者'),
('TEACHER','教師'),
('MENTOR','メンター'),
('STUDENT','受講者');
|
Permission
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | INSERT INTO permissions
(code,name)
VALUES
('USER_MANAGE','ユーザー管理'),
('CONTENT_CREATE','教材作成'),
('CONTENT_UPDATE','教材編集'),
('CONTENT_APPROVE','教材承認'),
('CONTENT_VIEW','教材閲覧'),
('LEARNING_MANAGE','学習管理'),
('AUDIT_VIEW','監査ログ閲覧');
|
7. UserRepository
| @Repository
public interface UserRepository
extends JpaRepository<User,Long>{
Optional<User> findByUsername(
String username
);
}
|
8. UserDetails実装
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86 | @Getter
public class CustomUserDetails
implements UserDetails {
private final User user;
public CustomUserDetails(
User user){
this.user=user;
}
@Override
public Collection<? extends GrantedAuthority>
getAuthorities(){
Set<GrantedAuthority> authorities =
new HashSet<>();
user.getRoles()
.forEach(role->{
authorities.add(
new SimpleGrantedAuthority(
"ROLE_"+role.getCode()
)
);
role.getPermissions()
.forEach(permission->{
authorities.add(
new SimpleGrantedAuthority(
permission.getCode()
)
);
});
});
return authorities;
}
@Override
public String getPassword(){
return user.getPassword();
}
@Override
public String getUsername(){
return user.getUsername();
}
@Override
public boolean isEnabled(){
return
user.getStatus()
==
UserStatus.ACTIVE;
}
}
|
9. UserDetailsService
ログイン時に呼ばれます。
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 | @Service
@RequiredArgsConstructor
public class CustomUserDetailsService
implements UserDetailsService {
private final UserRepository repository;
@Override
public UserDetails loadUserByUsername(
String username)
throws UsernameNotFoundException{
User user =
repository
.findByUsername(username)
.orElseThrow(
()->new UsernameNotFoundException(
"ユーザーが存在しません"
));
return new CustomUserDetails(
user
);
}
}
|
10. PasswordEncoder
パスワードは必ず暗号化します。
1
2
3
4
5
6
7
8
9
10
11
12 | @Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
|
11. SecurityConfig
Spring Security 6対応。
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83 | @Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsService service;
@Bean
SecurityFilterChain filterChain(
HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/css/**",
"/js/**",
"/login"
)
.permitAll()
.requestMatchers(
"/admin/**"
)
.hasRole("ADMIN")
.requestMatchers(
"/teacher/**"
)
.hasRole("TEACHER")
.requestMatchers(
"/student/**"
)
.hasRole("STUDENT")
.anyRequest()
.authenticated()
)
.formLogin(login -> login
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl(
"/dashboard",
true
)
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
);
return http.build();
}
}
|
12. ログイン画面
配置:
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 | <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<form
method="post"
action="/login">
<div>
<label>
ユーザー名
</label>
<input
name="username">
</div>
<div>
<label>
パスワード
</label>
<input
type="password"
name="password">
</div>
<button>
ログイン
</button>
</form>
</body>
</html>
|
13. ログインユーザー取得
Controllerで利用:
1
2
3
4
5
6
7
8
9
10
11
12
13 | @GetMapping("/dashboard")
public String dashboard(
@AuthenticationPrincipal
CustomUserDetails user){
Long userId =
user.getUser().getId();
return "dashboard";
}
|
14. Thymeleaf権限制御
例:
管理者だけ表示:
| <div
sec:authorize="
hasRole('ADMIN')
">
管理者メニュー
</div>
|
教師だけ:
| <div
sec:authorize="
hasAuthority('CONTENT_CREATE')
">
教材作成
</div>
|
15. メソッド単位権限制御
Serviceにも設定できます。
有効化:
| @EnableMethodSecurity
@Configuration
public class MethodSecurityConfig {
}
|
利用:
| @PreAuthorize(
"hasAuthority('CONTENT_APPROVE')"
)
@Transactional
public void approve(
Long contentId){
}
|
16. ログイン後リダイレクト分岐
役割別:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | ログイン
↓
ADMIN
/admin/dashboard
TEACHER
/teacher/dashboard
STUDENT
/student/dashboard
|
Handler:
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 | @Component
public class LoginSuccessHandler
implements AuthenticationSuccessHandler{
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication auth)
throws IOException{
if(auth
.getAuthorities()
.stream()
.anyMatch(
a->a.getAuthority()
.equals("ROLE_ADMIN"))){
response.sendRedirect(
"/admin/dashboard"
);
return;
}
response.sendRedirect(
"/dashboard"
);
}
}
|
17. セキュリティログ連携
ログイン成功時:
| SecurityLog
↓
INSERT
↓
Audit管理画面表示
|
記録:
| LOGIN_SUCCESS
LOGIN_FAILURE
LOGOUT
PASSWORD_CHANGE
|
18. アカウント作成フロー
管理者:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | ユーザー登録
↓
Role設定
↓
初期パスワード発行
↓
メール通知
↓
本人変更
|
19. 第20段階終了時点
実装可能になった機能:
✅ ログイン認証
✅ BCryptパスワード管理
✅ Role認可
✅ Permission認可
✅ ADMIN/TEACHER/STUDENT分離
✅ Thymeleaf表示制御
✅ Service権限制御
✅ ログインユーザー取得
✅ セキュリティログ連携準備
次の 第21段階 では、教材管理の中心部分を実装します。
内容:
第21段階:教材ライブラリ・コンテンツ管理実装
予定:
-
Content Entity完全実装
-
ContentVersion管理
-
動画コンテンツ管理
-
Markdown教材管理
-
Quiz教材管理
-
ファイル教材管理
-
教材登録Service
-
教材編集Service
-
教材申請Service
-
管理者承認Service
-
Controller実装
へ進みます。