コンテンツにスキップ

ありがとうございます。

ここから 第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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@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
templates

└ 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
<!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権限制御

例:

管理者だけ表示:

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

管理者メニュー

</div>

教師だけ:

1
2
3
4
5
6
7
8
<div
sec:authorize="
hasAuthority('CONTENT_CREATE')
">

教材作成

</div>

15. メソッド単位権限制御

Serviceにも設定できます。

有効化:

1
2
3
4
5
6
@EnableMethodSecurity
@Configuration
public class MethodSecurityConfig {


}

利用:

1
2
3
4
5
6
7
8
9
@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. セキュリティログ連携

ログイン成功時:

1
2
3
4
5
6
7
8
9
SecurityLog

↓

INSERT

↓

Audit管理画面表示

記録:

1
2
3
4
5
6
7
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段階:教材ライブラリ・コンテンツ管理実装

予定:

  1. Content Entity完全実装

  2. ContentVersion管理

  3. 動画コンテンツ管理

  4. Markdown教材管理

  5. Quiz教材管理

  6. ファイル教材管理

  7. 教材登録Service

  8. 教材編集Service

  9. 教材申請Service

  10. 管理者承認Service

  11. Controller実装

へ進みます。