하위 태스크 1
ROLE 구조 파악
Member/Authority/MemberUserDetails 구조 이해
model/Member.java: 애플리케이션 사용자를 나타내는 엔티티 클래스다. 데이터베이스의member테이블과 대응된다.model/Authority.java: 사용자가 가진 권한을 나타내는 엔티티 클래스다. 데이터베이스의authority테이블과 대응된다.model/MemberUserDetails: Spring Security의UserDetails인터페이스를 구현한 클래스다.
하위 태스크 2
URL 기반 보안 설정
/admin/**,/user/**등 URL 패턴별 권한 설정
SecurityConfig 클래스의 securityFilterChain 메서드를 수정한다.
public class SecurityConfig {
// ...
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/home").permitAll()
.requestMatchers("/member/**").hasAuthority("ROLE_ADMIN")
+ .requestMatchers("/admin/**").hasRole("ROLE_ADMIN")
+ .requestMatchers("/user/**").hasAnyRole("ROLE_USER", "ROLE_ADMIN")
.anyRequest().authenticated())
.formLogin(withDefaults())
.logout(withDefaults());
return http.build();
}
// ...
}ROLE_USER 역할을 가진 계정으로 http://localhost:8080/admin에 접근하면 403 에러가 발생한다.

하위 태스크 3
익명 접근 경로 설정
로그인 없이 접근 가능한 URL 지정
SecurityConfig 클래스의 securityFilterChain 메서드를 수정한다. 로그인 없이 접근할 수 있도록 /, /login 등의 경로를 추가한다.
public class SecurityConfig {
// ...
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
- .requestMatchers("/", "/home").permitAll()
+ .requestMatchers("/", "/home", "/login", "/css/**").permitAll()
.requestMatchers("/member/**").hasAuthority("ROLE_ADMIN")
.requestMatchers("/admin/**").hasRole("ROLE_ADMIN")
.requestMatchers("/user/**").hasAnyRole("ROLE_USER", "ROLE_ADMIN")
.anyRequest().authenticated())
.formLogin(withDefaults())
.logout(withDefaults());
return http.build();
}
// ...
}접속한 사용자 계정에서 로그아웃한 뒤, http://localhost:8080/home에 접근하면 정상적으로 문서를 볼 수 있다.

하위 태스크 4
메서드 보안 활성화
@EnableMethodSecurity또는 유사 설정 확인
@EnableMethodSecurity 어노테이션은 메서드 단위의 보안을 활성화 한다. 추후 검사 대상 메서드에 @PreAuthorize 어노테이션을 사용해, 실행 전에 권한을 검사할 수 있다. SecurityConfig 클래스에 해당 어노테이션을 추가한다.
@Configuration
@EnableWebSecurity(debug = true)
+ @EnableMethodSecurity
public class SecurityConfig {
// ...하위 태스크 5
@PreAuthorize적용서비스/컨트롤러 메서드에 권한 조건 적용
PostController 클래스를 생성한다. 게시글을 삭제하는 delete 메서드에 @PreAuthorize 어노테이션을 추가한다. 그 결과로 ROLE_ADMIN 역할이거나 게시글의 작성자만이 게시글을 삭제할 수 있게 된다.
@Controller
@RequiredArgsConstructor
public class PostController {
private final PostRepository postRepository;
@GetMapping("/post/list")
public String list(Model model) {
model.addAttribute("posts", postRepository.findAll());
return "post-list";
}
@GetMapping("/post/new")
public String newForm(Model model) {
model.addAttribute("post", new Post());
return "post-form";
}
@PostMapping("/post/create")
public String create(@ModelAttribute Post post, @AuthenticationPrincipal MemberUserDetails userDetails) {
post.setMember(userDetails.getMember());
postRepository.save(post);
return "redirect:/post/list";
}
@PreAuthorize("hasAuthority('ROLE_ADMIN') or #writerId == principal.memberId")
@PostMapping("/post/delete")
public String delete(@RequestParam Long postId, @RequestParam Long writerId) {
postRepository.deleteById(postId);
return "redirect:/post/list";
}
}윤서준(ROLE_USER) 사용자로 로그인하고 http://localhost:8080/post/list에 접속한다.

첫 번째 게시글(윤서준 소유)을 삭제하면 정상적으로 삭제된다.

두 번째 게시글(윤광철 소유)을 삭제하면 403 에러 페이지로 이동한다.

하위 태스크 6 ~ 7
소유권 검사 로직 구현
작성자 여부를 판단하는 헬퍼/메서드 구현
@AuthenticationPrincipal사용현재 사용자 정보를 주입받아 로직에 활용
사용자 자신이 작성한 게시글 목록을 조회하는 기능을 구현한다.
PostRepository에 findByMemberId 메서드를 추가한다.
public interface PostRepository extends JpaRepository<Post, Long> {
+ List<Post> findByMemberId(Long memberId);
}PostController에 자신의 게시글 목록을 보여주는 뷰를 선택하기 위한 listMine 메서드를 추가한다.
public class PostController {
// ...
+ @GetMapping("/post/mine")
+ public String listMine(@AuthenticationPrincipal MemberUserDetails userDetails, Model model) {
+ model.addAttribute("posts", postRepository.findByMemberId(userDetails.getMemberId()));
+ return "post-list";
+ }
// ...
}하위 태스크 8
권한별 시나리오 테스트
USER/ADMIN/익명 사용자로 각각 접근 테스트
윤서준 사용자로 로그인한 뒤, http://localhost:8080/post/mine에 접속하면 해당 사용자가 작성한 게시글만 노출된다.

윤광철 사용자로 로그인한 뒤 같은 URL로 접속한 결과는 다음과 같다.

익명 사용자는 접속이 제한되어 로그인 페이지로 이동한다.