Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] #11스프링시큐리티 예외처리 및 JWT 로직 일부 구현 #11

Merged
merged 50 commits into from
Oct 9, 2024

Conversation

hyxklee
Copy link
Member

@hyxklee hyxklee commented Oct 6, 2024

1. 무슨 이유로 코드를 변경했나요?

  • 스프링 시큐리티 필터 단에서 발생하는 예외 처리를 위한 CustomEntryPoint 구현
  • 적절한 인증/인가를 위한 JWT provider와 JWT filter 구현
  • 구글 소셜 로그인을 위한 의존성 추가 및 구현

2. 어떤 위험이나 장애를 발견했나요?

  • 연관된 파트들을 작업하다 보니 PR이 많이 커졌습니다..
  • 빠르게 올려둘 테니 천천히 읽어보시고 리뷰 부탁드리겠습니다.

3. 관련 스크린샷을 첨부해주세요.

  • 인증 정보(jwt token) 없이 요청을 보낼시
image
  • 최초로 소셜 로그인 진행시
    image

  • 회원가입 이후 소셜 로그인 진행시
    image

4. 완료 사항

스프링 시큐리티 관련

인증의 경우 스프링 시큐리티 필터체인을 모두 돌아도 인증 정보가 없다면 AuthenticationException을 발생시킵니다. 해당 예외는 필터단에서 발생하는 예외이기 때문에 RestControllerAdvice를 적용한 global exception handler에서 잡지 못합니다. 따라서 해당 예외를 처리하는 AuthenticationEntryPoint를 구현한 커스텀 객체를 만들어 처리했습니다

JWT 관련

그리고 JWTfilter는 사용자의 요청이 들어온 경우 jwt 엑세스 토큰의 유효성 검사를 진행하도록 구현했습니다.
JWT refresh의 경우 요구사항에 맞게 구현할 계획이고, 구현하게 된다면 별도의 API로 리프레시 하는 방식으로 구현할 계획입니다.
따라서 refresh 토큰은 발급은 해주고 있지만, 관련된 아무런 로직도 구현하지 않은 상태입니다

jwt provider는 jwt 토큰 발급, 유효성 검사, 인증객체 생성에 사용되는 서비스 입니다. 토큰 유효성 검사를 통과한 후에 해당 토큰의 정보로 Authentication을 저장할 수 있습니다

소셜 로그인 관련

소셜 로그인은 X와 동일하게 구글로 진행했습니다.
플로우: 프론트에서 client_id(우리가 구글 로그인을 받기 위해 등록한 정보)를 토대로 사용자에게 구글 로그인을 받음 -> auth code를 반환 -> 프론트는 해당 auth code를 서버로 보내 로그인 요청 -> 서버는 해당 코드를 받아 구글 유저 조회 서버로 요청을 보냄 -> 유저 조회 성공 -> DB에 없는 유저라면 저장 / 있는 유저라면 로그인 진행 -> jwt accessToken과 refreshToken을 발급

client_id, client_secret, redirect_uri는 외부에 노출되면 안되기 때문에 환경변수 처리하였습니다.

5. 추가 사항

  • 자세한 설명은 코드 별로 코멘트 달아두겠습니다.
  • 궁금한 점 있으면 언제든 질문 주세요!

참고자료

https://velog.io/@gmlstjq123/구글-소셜로그인-구현
https://velog.io/@bdd14club/백엔드-2.-구글-소셜-로그인-구현하기

}
filterChain.doFilter(request, response);
}

Copy link
Member Author

Choose a reason for hiding this comment

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

필터는 스프링 어플리케이션과 영역이 달라 여기서 발생하는 예외는 GlobalExceptionHandler에서 잡아서 처리할 수 없습니다. 따라서 서비스 단에서 사용하는 예외처리 로직과는 차이가 있습니다.
따라서 커스텀된 처리를 위해 서블릿(request)에 토큰이 유효하지 않다는 정보를 저장한 후 CustomAuthenticationEntryPoint에 알려 예외처리를 진행합니다

Copy link
Member Author

Choose a reason for hiding this comment

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

또 JWT filter에서는 되도록이면 예외를 잡아서 자체적으로 처리하지 않는 것이 스프링 시큐리티가 의도하는 예외 처리에 맞는 방식이라고 생각합니다.
사용자 정의 filter에서 예외를 잡아서 처리하고 return 해버리면 스프링 시큐리티는 필터 단에서 어떤 예외가 발생했는지를 모르기 때문에 정상적인 동작을 하지 못하는 경우가 있습니다

Copy link
Collaborator

Choose a reason for hiding this comment

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

JWT 필터가 예외를 직접 처리하기보다는, 필터가 예외를 발생시키고 스프링 시큐리티가 이를 적절히 처리하도록 하는 것 이었군요! 자세한 설명 감사합니다.

Copy link
Member

Choose a reason for hiding this comment

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

궁금한 점이 있습니다!
Filter 단에서 발생한 JWT 유효성 검증 예외 처리의 경우는 GlobalExceptionHandler로 잡아서 처리할 수 없습니다
하지만 response에 에러 응답 상태를 추가하여 반환하는 방법도 있는 걸로 알고 있습니다.

response에 에러 응답 상태를 추가하는 경우, AuthenticationEntryPoint까지 가지 않아도 해당 인가 필터에서 예외를 처리할 수 있는 것으로 알고 있습니다.

그럼에도 AuthenticationEntryPoint에서 예외를 처리한 이유는 보안 관련 책임을 Filter가 아닌 AuthenticationEntryPoint에 위임하여, 보안 관련 책임을 명확하게 분리하고, 추가 예외 확장성을 고려한 접근 방식이라고 생각해도 될까요?

제가 잘못 알고 있는 것일 수도 있습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

넵 맞습니다. 해당 필터에서 response.header와 body, status를 설정해서 토큰 유효성 검사 실패시 바로 response를 날리게 설정하는 방식도 가능합니다

하지만 해당 방식으로 구현할 경우 예외 발생시 return을 해 다음 필터 진행을 막아버리는 경우 permitAll()이 동작하지 않아 흐름에 문제가 발생할 수도 있고, return하지 않고 그대로 진행을 하더라도 AuthenticationException이 발생하게됩니다.
따라서 AuthenticationEntryPoint를 커스텀해 스프링 시큐리티가 의도한 방향에 맞게 예외처리를 진행하고, 또 클라이언트에게 예외 응답을 할 때 기존 응답 형식과 동일하게 맞춰서 보내기 위해 해당 방식으로 구현을 했습니다!

말씀해주신 방식과 제가 구현한 방식 모두 많이 사용하는 것으로 알고 있습니다. 어떤 것이 더 나은지는 저도 아직 잘 모르겠으나, GlobalExceptionHandler에서 예외처리를 위임하 듯 AuthenticationEntryPoint에 인증관련 예외처리를 위임하는 로직이 프로젝트의 통일성을 높이는 방식 같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

또 request.setAttribute를 사용하지 않고, 그대로 진행을 해도 AuthenticationException이 발생하기 때문에 커스텀한 entrypoint에서 처리해줄 수 있는데, 저는 토큰 유효성 검사와 그냥 AuthenticationException이 발생하는 경우를 나누고 싶어
request.setAttribute를 사용해 예외 메시지를 구분하였습니다!

.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(exceptioHandling ->
exceptioHandling
.authenticationEntryPoint(customAuthenticationEntryPoint))

Copy link
Member Author

Choose a reason for hiding this comment

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

jwt filter와 CustomAuthenticationEntryPoint는 사용자가 재정의한 필터에서 사용하는 컴포넌트이기 때문에 SecurityConfig에 필터 체인에 명시적으로 등록을 해주어야 동작합니다.
JWT filter의 위치는 보통 UsernamePasswordAuthenticationFilter 앞에 위치하게 둔다고 하는데 자세한 부분은 스프링 시큐리티의 필터체인을 찾아보시면 좋습니당

Copy link
Member

@koreaioi koreaioi Oct 9, 2024

Choose a reason for hiding this comment

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

UsernamePasswordAuthenticationFilter의 역할

  • JWT에서 username과 password를 추출하여 인증 객체를 만듭니다.
  • 해당 인증 객체를 가지고 실제 사용자인지 아닌지 확인합니다.

UsernamePasswordAuthenticationFilter의 역할은 사용자 인증 입니다.
인증과 JWT 유효성 검증 책임을 분리하기 위해 UsernamePasswordAuthentication앞에 JWT 유효성 책임을 가진 JWTFilter를 두는 것으로 알고 있습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

좋은 정보 감사합니당!

@hyxklee hyxklee force-pushed the feat/#5/스프링시큐리티-예외처리 branch from 8b8cc81 to 9202c84 Compare October 6, 2024 13:37

@Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
Copy link
Collaborator

Choose a reason for hiding this comment

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

JWT필터에서 발생하는 예외들은, 일반적인 서비스 레이어의 처리 방식과 다른것을 알게 되었습니다!

}
filterChain.doFilter(request, response);
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

JWT 필터가 예외를 직접 처리하기보다는, 필터가 예외를 발생시키고 스프링 시큐리티가 이를 적절히 처리하도록 하는 것 이었군요! 자세한 설명 감사합니다.

Copy link
Member

@yechan-kim yechan-kim left a comment

Choose a reason for hiding this comment

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

고생하셨습니다! restApi를 통한 Google 소셜 로그인 잘 구현된 것 같습니다! OAuth을 통해서만 소셜 로그인을 할 수 있을 줄 알았는데, 새롭게 배워갑니다! 추가적으로 궁금한 점이 있어 의견 남겨두었으니, 확인 부탁드립니다!

if(response.status() == LoginStatus.LOGIN){
return ResponseDto.response(LOGIN_SUCCESS.getCode(), LOGIN_SUCCESS.getMessage(), response);
}
return ResponseDto.response(USER_SAVE_SUCCESS.getCode(), USER_SAVE_SUCCESS.getMessage(), response);
}
Copy link
Member

Choose a reason for hiding this comment

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

회원가입과 로그인을 하나의 api로 통일한 이유가 있을까요?

Comment on lines 35 to 40
if (userRepository.existsByEmail(userInfo.email())){
return loginUser(userInfo.email());
}
// 아니라면 회원가입
return registerUser(userInfo);
}
Copy link
Member

Choose a reason for hiding this comment

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

코드 재사용성 관점에서 존재하는 유저인지 확인하는 함수를 따로 만드는건 어떨까요?

private Boolean checkExists(String email){
     if (userRepository.existsByEmail(email())){
            return true
        }
        return false;
}

이후 다른 도메인의 서비스 로직에서 사용자를 구분할 때 사용 할 수 있을것 같아요!
Boolean으로 리턴하지 않고 Response 객체로 생성해서 리턴해주거나 User 객체로 넘겨주는 방식도 좋을 것 같습니다

Copy link
Member Author

Choose a reason for hiding this comment

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

유저 조회, 유저 있는지 확인하는 로직은 타 도메인에서도 많이 사용되고, 타 도메인에서 UserRepository에 직접 접근하는 것은 별로 좋지 않기 때문에 코멘트 달아주신 대로 수정하도록 하겠습니다!
차후에 유저를 반환하는 메서드도 구현하게 된다면 위 코멘트처럼 타 도메인에서도 재사용할 수 있도록 구현하겠습니다!

Comment on lines +49 to +58
private UserSocialLoginResponse registerUser(GoogleUserInfoResponse userInfo) {
User user = User.builder()
.name(userInfo.name())
.email(userInfo.email())
.build();

userRepository.save(user);

return new UserSocialLoginResponse(user.getId(), REGISTER, generateToken(user.getEmail()));
}
Copy link
Member

Choose a reason for hiding this comment

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

이 코드도 재사용성 관점에서 변경하면 좋을것 같아요!

  1. register 보다 create로 메서드명을 변경
  2. 리턴을 dto가 아니라 User 객체로 반환
  3. dto 로 매핑해주는 클래스, 혹은 메서드를 구현

위 방향이 User 객체가 필요할 떄 Create 메서드를 사용할 수 있어 이후 중복 코드가 줄어들 수 있을것 같습니다 !

Copy link
Member Author

Choose a reason for hiding this comment

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

해당 메서드는 소셜 로그인시에만 호출되는 메서드로 현재 요구사항에는 확장이 되지 않는 메서드라고 생각합니다!
자체 로그인으로 확장이 되더라도, 회원가입시 유저를 저장하는 dto 구조가 달라 소셜 회원가입과 자체 회원가입은 별도의 메서드로 관리가 되어야한다고 생각합니다!
해당 메서드의 특수성을 생각했을 때 User 객체가 필요한 상황이라면 별도의 메서드를 구현하는 것이 관리가 용이할 것 같다는 생각이 들었습니다!
제가 질문을 제대로 이해했는지 약간 의구심이 들지만 제가 했던 생각을 적어봤습니다! 감사합니다

Comment on lines +4 to +8
String access_token,
String expires_in,
String scope,
String token_type,
String id_token
Copy link
Member

Choose a reason for hiding this comment

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

카멜케이스가 아니라 스네이크 케이스를 사용하신 이유가 궁금합니다 !

Copy link
Member Author

Choose a reason for hiding this comment

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

해당 DTO는 restClient를 이용해 구글 auth 서버로 요청을 보낸후 응답을 매핑하여 받아오는 객체이기 때문에 구글 서버에서 돌아오는 변수명과 동일하게 맞춰서 값을 제대로 받을 수 있게 구현했습니다!
변수명이 달라지면 반환값을 제대로 받아오지 못하는 문제 때문에 통일하였습니다!

hyxklee and others added 2 commits October 9, 2024 21:22
@hyxklee hyxklee merged commit 18e0b35 into main Oct 9, 2024
2 checks passed
@hyxklee hyxklee deleted the feat/#5/스프링시큐리티-예외처리 branch October 9, 2024 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat 기능 개발, 구현 refactor 기능 수정
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[feat] #5 스프링 시큐리티 예외 처리 구현 및 로그인 구현
5 participants