하위 태스크 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 사용

현재 사용자 정보를 주입받아 로직에 활용

사용자 자신이 작성한 게시글 목록을 조회하는 기능을 구현한다.

PostRepositoryfindByMemberId 메서드를 추가한다.

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로 접속한 결과는 다음과 같다.

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