Skip to content

[하녜담] Chapter 8. Spring Security - Security 구조, 폼 로그인#105

Open
hhd517 wants to merge 1 commit into
hanyedam/mainfrom
hanyedam/#104
Open

[하녜담] Chapter 8. Spring Security - Security 구조, 폼 로그인#105
hhd517 wants to merge 1 commit into
hanyedam/mainfrom
hanyedam/#104

Conversation

@hhd517
Copy link
Copy Markdown

@hhd517 hhd517 commented May 19, 2026

✏️ 작업 내용

#️⃣ 연관된 이슈

closes #104


💡 함께 공유하고 싶은 부분

해당 주차를 공부하면서 함께 이야기하고 싶은 주제를 남겨주세요.

(어려웠던 부분과 해결 과정, 핵심 코드, 참고한 자료 등)


🤔 질문

이번에 인증 실패 시 응답통일을 위해 CustomEntryPoint를 구현했는데,
ExceptionTranslationFilter 말고 다른 방식으로도 처리할 수 있는지 궁금합니다.


✅ 워크북 체크리스트

  • 모든 핵심 키워드 정리를 마쳤나요?
  • 핵심 키워드에 대해 완벽히 이해하셨나요?
  • 이론 학습 이후 직접 실습을 해보는 시간을 가졌나요?
  • 미션을 수행하셨나요?
  • 미션을 기록하셨나요?

✅ 컨벤션 체크리스트

  • 디렉토리 구조 컨벤션을 잘 지켰나요?
  • pr 제목을 컨벤션에 맞게 작성하였나요?
  • pr에 해당되는 이슈를 연결하였나요?
  • 적절한 라벨을 설정하였나요?
  • 스터디원들에게 code review를 요청하기 위해 reviewer를 등록하였나요?
  • 닉네임/main 브랜치의 최신 상태를 반영하고 있는지 확인했나요?

📌 주안점

CustomEntryPoint와 CustomAccessDenied를 구현해 인증/인가 실패 시
HTML 대신 JSON으로 응답통일한 부분을 봐주세요!

@hhd517 hhd517 requested review from a team and issuejong May 19, 2026 06:49
@hhd517 hhd517 self-assigned this May 19, 2026
@hhd517 hhd517 added the 🚀Week 8 8주차 워크북 미션 label May 19, 2026
@issuejong
Copy link
Copy Markdown
Member

Security 설정이 깔끔하게 잘 구성된 것 같아요 !

// query: id(ID순) 또는 rating(별점순)
@GetMapping("/reviews/my")
public ApiResponse<MissionResDTO.Pagination<ReviewResDTO.GetReview>> getMyReviews(
@RequestParam Long userId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아마 다음 주차에 나올 것 같긴한데, JWT를 도입하게 되면 @AuthenticationPrincipal를 사용해서 시큐리티컨텍스트에 저장된 객체를 빼서 주입해줄 수 있습니다. 이렇게 사용하면 userId 파라미터에 의존하고 있는 현재 구조를 더 안정적으로 바꿀 수 있다는 장점이 있는데, 다음주에 코드 수정하실때 한번 확인하고 해주시면 좋을 것 같아서 리뷰 남겨봅니당

Comment on lines +1 to +67
package com.example.Spring_Boot.global.resolver;

import com.example.Spring_Boot.domain.review.dto.ReviewCursor;
import com.example.Spring_Boot.global.apiPayload.code.GeneralErrorCode;
import com.example.Spring_Boot.global.exception.ProjectException;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
public class ReviewCursorArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(ReviewCursor.class);
}

@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {

String cursor = webRequest.getParameter("cursor");
String query = webRequest.getParameter("query");

if (cursor == null) cursor = "-1";
if (query == null) query = "id";

if (cursor.equals("-1")) {
return ReviewCursor.builder()
.query(query)
.cursor(cursor)
.hasCursor(false)
.build();
}

String[] cursorSplit = cursor.split(":");

switch (query.toLowerCase()) {
case "id" -> {
Long idCursor = Long.parseLong(cursorSplit[1]);
return ReviewCursor.builder()
.query(query)
.cursor(cursor)
.idCursor(idCursor)
.hasCursor(true)
.build();
}
case "rating" -> {
Integer ratingCursor = Integer.parseInt(cursorSplit[1]);
Long idCursor = Long.parseLong(cursorSplit[2]);
return ReviewCursor.builder()
.query(query)
.cursor(cursor)
.ratingCursor(ratingCursor)
.idCursor(idCursor)
.hasCursor(true)
.build();
}
default -> throw new ProjectException(GeneralErrorCode.BAD_REQUEST);
}
}
} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 너무 잘 반영해주셨네요! 감사합니다:)

Comment on lines +1 to +30
package com.example.Spring_Boot.global.security;

import com.example.Spring_Boot.global.apiPayload.ApiResponse;
import com.example.Spring_Boot.global.apiPayload.code.BaseErrorCode;
import com.example.Spring_Boot.global.apiPayload.code.GeneralErrorCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import java.io.IOException;

public class CustomEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException
) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED;

response.setContentType("application/json;charset=UTF-8");
response.setStatus(code.getStatus().value());

ApiResponse<Void> errorResponse = ApiResponse.onFailure(code, null);
objectMapper.writeValue(response.getOutputStream(), errorResponse);
}
} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이번에 인증 실패 시 응답통일을 위해 CustomEntryPoint를 구현했는데, ExceptionTranslationFilter 말고 다른 방식으로도 처리할 수 있는지 궁금합니다.

-> 아주 좋은 질문인 것 같습니다!
지금 저희가 CustomEntryPoint를 만든 이유부터 생각해 보면 좋을 것 같습니다. 만약 컨트롤러나 서비스에서 예외가 발생했다면 @RestControllerAdvice를 활용해서 만들어두신 GeneralExceptionAdvice가 바로 잡아 냈을 것입니다. 그러나 시큐리티 필터에서 에러가 났다면 RestControllerAdvice가 잡아낼 수 없기 때문에 CustomEntryPoint를 만들고 objectMapper로 JSON을 만들어 클라이언트에게 반환하게 해두신거죠

그런데 여기서 한 가지 아쉬운 점이 있습니다. CustomEntryPoint에서 ObjectMapper로 직접 JSON 응답을 만들고 있잖아요? 이러면 에러 응답 포맷을 바꿀 때 GeneralExceptionAdvice랑 CustomEntryPoint 두 군데를 다 수정해야 합니다.
이걸 해결하는 방법? 좀 더 깔끔한 구조를 만드는 방법?으로 HandlerExceptionResolver를 CustomEntryPoint에 주입하는 방식이 있습니다.
위에서 말씀드렸듯이 시큐리티 필터에서 에러가 났다면 RestControllerAdvice가 잡아낼 수 없기 때문에 이 방법을 사용해서 필터에서 터진 예외를 resolver.resolveException()으로 MVC 쪽에 토스해버리는 겁니다. 그러면 우리가 이미 만들어둔 @RestControllerAdvice가 그 예외를 잡아서 처리해주니까, 에러 응답 로직이 한 곳으로 모이게 됩니다.

구현은 GeneralExceptionAdvice에 AuthenticationException과 AccessDeniedException 전용 핸들러를 추가하는 방식으로 하면 될 것 같습니다!

근데 HandlerExceptionResolver 방식도 결국 ExceptionTranslationFilter → EntryPoint 호출 흐름 안에서 동작하는 거라, 아예 다른 방식이 있는지 궁금하신 거였다면,
다음 주차에 JWT 필터를 다룰 때 쓰게 될 방법이 하나 더 있습니다. 아예 필터 체인 앞에 예외 처리 전용 필터(ExceptionHandlerFilter)를 하나 세워두는 방식인데요, 이 필터가 try-catch로 뒤에 있는 필터들(JwtFilter 등)을 감싸서, 어디서 예외가 터지든 잡아내는 구조입니다.

HTTP 요청
  ↓
[SecurityContextPersistenceFilter]     ← 기본
  ↓
[ExceptionHandlerFilter]               ← 커스텀 (끼워넣음)
  ↓
[JwtFilter]                            ← 커스텀 (끼워넣음)
  ↓
[UsernamePasswordAuthenticationFilter] ← 기본
  ↓
[AnonymousAuthenticationFilter]        ← 기본
  ↓
[ExceptionTranslationFilter]           ← 기본
  ↓
[FilterSecurityInterceptor]            ← 기본
  ↓
애플리케이션 로직

사실 ExceptionTranslationFilter는 자기보다 뒤에 있는 필터에서 터진 예외만 잡을 수 있기 때문에, JwtFilter처럼 앞에 위치하는 필터의 예외는 커버가 안 됩니다. 그래서 JWT 환경에서는 이런 ExceptionHandlerFilter를 앞에 세워두는 방식이 자주 쓰인다는 점도 참고하시면 좋을 것 같습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🚀Week 8 8주차 워크북 미션

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants