기존 문제점: 관리자(Admin) 권한이 있어도 댓글 수정/삭제 불가
기존 코드 분석
현재 @AuthenticationPrincipal을 이용하여 컨트롤러에서 userId를 추출하고, 이를 서비스 계층에서 권한 검증하고 있다.
@PutMapping("/{commentId}")
public ResponseEntity<CommentResponse> updateComment(
@AuthenticationPrincipal JwtUserDetails userPrincipal,
@PathVariable Long commentId,
@Valid @RequestBody CommentRequest request
) {
return ResponseEntity.ok(
commentService.update(
userPrincipal.getId(),
commentId,
request.getContent()
)
);
}
@DeleteMapping("/{commentId}")
public ResponseEntity<Void> deleteComment(
@AuthenticationPrincipal JwtUserDetails userPrincipal,
@PathVariable Long commentId
) {
commentService.delete(userPrincipal.getId(), commentId);
return ResponseEntity.ok().build();
}
문제점
- Admin이 댓글을 수정/삭제할 권한이 없다고 판정됨
- 컨트롤러에서 userId를 추출하고 서비스에서 권한을 검증하는 방식이 번거로움
- 컨트롤러에서 인증을 하고 서비스에서 인가를 하는 방식 → 역할이 모호함
해결 방법: @PreAuthorize를 활용하여 서비스 계층에서 권한 관리
개선 방향
- 컨트롤러에서는 @PreAuthorize를 통해 서비스 계층에서 직접 권한을 확인하도록 변경
- Admin도 댓글 수정/삭제가 가능하도록 hasRole('ADMIN') 추가
- 컨트롤러에서 불필요한 인증 로직을 제거하여 코드 가독성을 높임
📌 개선된 코드
1. 댓글 수정/삭제 서비스 코드
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public CommentResponse update(Long userId, Long commentId, String newContent) {
Comment comment = commentRepository.findByIdWithUser(commentId)
.orElseThrow(() -> new EntityNotFoundException(PostError.COMMENT_NOT_EXIST.getMessage() + commentId));
comment.updateContent(newContent);
return CommentResponse.from(comment);
}
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public void delete(Long userId, Long commentId) {
Comment comment = commentRepository.findByIdWithUser(commentId)
.orElseThrow(() -> new EntityNotFoundException(PostError.COMMENT_NOT_EXIST.getMessage() + commentId));
Post post = comment.getPost();
post.decreaseCommentCounts();
commentRepository.deleteById(commentId);
}
변경된 사항
- @PreAuthorize를 적용하여 권한 검사를 서비스 계층에서 수행
- Admin 또는 댓글 작성자만 수정/삭제 가능하도록 hasRole('ADMIN') 추가
- 컨트롤러에서 불필요한 userId 전달 제거
✅ 2. 컨트롤러 코드 (불필요한 인증 로직 제거)
@PutMapping("/{commentId}")
public ResponseEntity<CommentResponse> updateComment(
@PathVariable Long commentId,
@Valid @RequestBody CommentRequest request
) {
return ResponseEntity.ok(
commentService.update(
SecurityUtil.getCurrentUserId(),
commentId,
request.getContent()
)
);
}
@DeleteMapping("/{commentId}")
public ResponseEntity<Void> deleteComment(
@PathVariable Long commentId
) {
commentService.delete(SecurityUtil.getCurrentUserId(), commentId);
return ResponseEntity.ok().build();
}
변경된 사항
- 컨트롤러에서 @AuthenticationPrincipal을 사용하지 않고 SecurityUtil을 활용하여 현재 로그인한 사용자 ID 추출
- 컨트롤러는 단순한 API 요청 처리에 집중하고, 권한 검사는 서비스에서 처리
3. SecurityUtil을 활용하여 현재 로그인한 사용자 ID 가져오기
public class SecurityUtil {
public static Long getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new IllegalStateException(AuthenticationError.AUTHENTICATION_FAILURE.getMessage());
}
JwtUserDetails userDetails = (JwtUserDetails) authentication.getPrincipal();
return userDetails.getId();
}
}
- 컨트롤러 코드에서 불필요한 인증 로직 제거
- 현재 로그인한 사용자의 ID를 손쉽게 가져올 수 있음
4. 추가적으로 SecurityFilter 정리
기존 문제: SecurityFilter의 requestMatchers()가 계속 늘어나고 있음
//경로별 인가 작업
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/", "/api/login", "/api/join", "/api/token/**", "/api/posts/list", "/api/posts/grid")
.permitAll()
.requestMatchers(HttpMethod.GET, "/api/posts/{code:^[0-9]*$}")
.permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated());
문제점
- 매번 새로운 엔드포인트가 추가될 때마다 requestMatchers()를 수정해야 함
- 불필요하게 복잡한 보안 설정
✅ 해결: 메소드 단위에서 인증 및 인가 수행
기본적인 설정만 유지하고, 세부적인 권한 검사는 @PreAuthorize를 활용하여 서비스 계층에서 수행
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/", "/api/login", "/api/join", "/api/token/**").permitAll() // 기본 공개 API
.requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated());
5. 테스트 코드 개선
✅ 권한이 필요한 기능은 통합 테스트로 분리
@PreAuthorize가 적용된 서비스 메서드는 단순한 단위 테스트가 아닌 통합 테스트(@SpringBootTest)로 수행해야 한다.
권한 관련 테스트는 이후 controller 테스트에 함께 포함할 예정
추가
@PreAuthorize를 서비스 메소드에 적용한 이유
- 재사용성:
- 같은 서비스 메서드를 여러 컨트롤러에서 호출할 수 있음.
- 컨트롤러에서 권한 검사를 하지 않아도 다른 서비스에서 호출할 때도 보안이 유지됨.
- 비즈니스 로직과 보안 로직을 분리:
- 컨트롤러는 HTTP 요청 처리에 집중, 서비스 계층은 비즈니스 로직을 담당.
References:
https://velog.io/@joon6093/SecuredPreAuthorize-PostAuthorize
https://velog.io/@shon5544/Spring-Security-4.-%EA%B6%8C%ED%95%9C-%EC%B2%98%EB%A6%AC
'Project > Boilerplate' 카테고리의 다른 글
필터 예외 처리 (0) | 2025.03.17 |
---|---|
API 예외 처리 (0) | 2025.03.17 |
Cursor 페이지네이션에서의 정렬, 필터링 (0) | 2025.03.10 |
게시글 Pagination 적용 (2 / 2 - 인피니티 스크롤형 게시판) (0) | 2025.03.10 |
게시글 Pagination 적용 (1 / 2 - 리스트형 게시판) (0) | 2025.03.10 |