Skip to content

[Spring Core] (배포) 안금서 미션 제출합니다. #114

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

Open
wants to merge 66 commits into
base: goldm0ng
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
5913de1
<ADD> gradle 의존성 추가
goldm0ng Dec 26, 2024
67da617
<ADD> 토큰 기반 인증 방식 jwt 관련 기능 추가
goldm0ng Dec 26, 2024
53828d8
<FIX> MemberRequest DTO를 record 형식으로 변경
goldm0ng Dec 26, 2024
0ec5186
<ADD> jwt를 사용한 로그인 및 인증 정보 조회 기능 구현
goldm0ng Dec 26, 2024
ebf6407
<ADD> 로그인 사용자 정보인 MemberAuthInfo를 조회하는 HandlerMethodArgumentResolver 구현
goldm0ng Dec 26, 2024
22f9e30
<FIX> HandlerMethodArgumentResolver 구현에 따른 예약 API 및 기능 리팩터링
goldm0ng Dec 26, 2024
347d0c7
<ADD> 관리자 기능 구현
goldm0ng Dec 26, 2024
2a96836
<ADD> 사용자의 정보를 조회하는 ArgumentResolver와 관리자만 해당 경로에 접근할 수 있도록 하는 Interc…
goldm0ng Dec 26, 2024
91bf7bf
<ADD> 예외처리 추가
goldm0ng Dec 26, 2024
16e217b
<ADD> 1 ~ 3단계 미션 테스트 추가
goldm0ng Dec 26, 2024
4a9ff8c
<FIX> DTO record 타입으로 변경
goldm0ng Jan 7, 2025
a5e8d66
<FIX> DTO record 타입으로 변경에 따른 수정
goldm0ng Jan 7, 2025
e027b31
<FIX> 응답 바디 타입 수정
goldm0ng Jan 7, 2025
c7a1875
<FIX> 인증 패키지 구조 변경
goldm0ng Jan 8, 2025
2358568
<FIX> JwtUtils 관련 수정 (멤버 변수 및 메서드)
goldm0ng Jan 8, 2025
b44dab1
<FIX> 에러 추적 용이하도록 에러 포함
goldm0ng Jan 8, 2025
330b40c
<FIX> gradle 의존성 jdbc -> jpa로 대체
goldm0ng Jan 8, 2025
7a7ed55
<ADD> JPA 관련 설정 추가
goldm0ng Jan 8, 2025
c5f4f97
<ADD> 초기 데이터베이스 seed 데이터 추가 및 기존 스키마 삭제
goldm0ng Jan 8, 2025
580c6ab
<FIX> Time 도메인 엔티티 매핑 및 JPA 전환
goldm0ng Jan 8, 2025
edfbc2c
<FIX> Theme 도메인 엔티티 매핑 및 JPA 전환
goldm0ng Jan 8, 2025
312bdea
<FIX> Reservation 도메인 엔티티 매핑 및 JPA 전환
goldm0ng Jan 8, 2025
bef9160
<FIX> Member 도메인 엔티티 매핑 및 JPA 전환
goldm0ng Jan 8, 2025
f164ce0
<FIX> JPA 전환에 따른 LoginService 수정 및 예외처리 방식 변경
goldm0ng Jan 8, 2025
e2a32bc
<ADD> 4단계 테스트 코드 추가
goldm0ng Jan 8, 2025
9c5e253
<FIX> Spring MVC (인증) 미션 테스트 이름 수정
goldm0ng Jan 8, 2025
0840983
<FIX> 초기값 설정을 위한 쿼리 수정
goldm0ng Jan 9, 2025
da71532
<FIX> 회원 인증 정보 DTO 수정 (id 정보 추가)
goldm0ng Jan 9, 2025
84f4e41
<ADD> 내 예약 목록 응답 DTO 추가
goldm0ng Jan 9, 2025
fd7a976
<FIX> Reservation - Member 추가 연관관계 매핑
goldm0ng Jan 9, 2025
fcf74d3
<FIX> Reservation DTO를 record 타입으로 변경
goldm0ng Jan 9, 2025
54d412e
<ADD> 내 예약 목록 조회 기능 구현
goldm0ng Jan 9, 2025
a4b3705
<FIX> JwtUtils id 추출 코드 수정
goldm0ng Jan 9, 2025
b8d792b
<ADD> 사용자 정의 예외 생성 및 핸들러에 추가
goldm0ng Jan 9, 2025
aba712d
<ADD> 5단계 테스트 코드 추가 및 전 단계 테스트 수정
goldm0ng Jan 9, 2025
d472034
<ADD> 예약 대기 요청 및 취소 기능 구현
goldm0ng Jan 9, 2025
751587d
<ADD> 예약 중복 방지 및 내 예약 조회 시 예약 대기 목록까지 보이도록 구현
goldm0ng Jan 9, 2025
e963234
<ADD> 중복 예약 관련 커스텀 예외 추가
goldm0ng Jan 9, 2025
8b292c3
<ADD> 6단계 테스트 코드 추가 및 테스트 깨지는 부분 수정
goldm0ng Jan 9, 2025
39a0f7d
<FIX> 예약 대기 취소 구현 오류 해결
goldm0ng Jan 13, 2025
e7e5c7b
<FIX> 공통 예외 처리 핸들러 중복 코드 제거
goldm0ng Jan 13, 2025
a80573b
<FIX> 예외 처리 오류 2차 해결 및 로깅 방식 수정
goldm0ng Jan 14, 2025
08017a7
<FIX> JPA 쿼리 메소드 적용
goldm0ng Jan 14, 2025
35ec5f7
<FIX> 기본 생성자 접근지정자 변경 및 생성 어노테이션 추가
goldm0ng Jan 14, 2025
1959159
<ADD> 변경에 유연하도록 인터페이스 추가 및 jwt 인증 구현체 추가
goldm0ng Jan 10, 2025
b4bf7fb
<FIX> 기존 JwtUtils를 클래스 속성에 맞도록 이름 수정
goldm0ng Jan 10, 2025
c74ea76
<ADD> jwt 인증 로직을 모아놓은 service 추가
goldm0ng Jan 10, 2025
c4d0384
<FIX> 인증 응답 이름 더 포괄적으로 수정
goldm0ng Jan 10, 2025
a25995f
<FIX> jwt 패키지와의 강한 결합 분리
goldm0ng Jan 10, 2025
f900f55
<FIX> 7단계 요구사항 구현 및 테스트 추가
goldm0ng Jan 15, 2025
ed9cc5d
<ADD> 인증 로직을 담은 빈이 수동 주입되도록 관리하는 AuthenticationConfig 추가
goldm0ng Jan 15, 2025
1d0ce07
<FIX> Reservation 생성자 필드 순서 수정
goldm0ng Jan 15, 2025
b00df65
<DELETE> 초기 데이터 sql 파일 삭제
goldm0ng Jan 15, 2025
8ace050
<ADD> 초기 데이터 삽입을 위한 클래스를 운영용/테스트용으로 분리해 구현
goldm0ng Jan 15, 2025
54e6add
<ADD> 8단계 테스트 코드 추가
goldm0ng Jan 15, 2025
61ab41f
Merge branch 'goldm0ng' into spring-core
goldm0ng Jan 15, 2025
4fd9e4a
<FIX> 500 error 발생 시, 내려지는 에러 메세지로 인한 보안 문제가 발생하지 않도록 Body 내용 수정 및 th…
goldm0ng Jan 21, 2025
1fa1b1a
<FIX> Request 검증 로직 Service 로 이동
goldm0ng Jan 21, 2025
01eb56c
<FIX> LoginController 에 존재하는 검증 로직 Service 로 이동 및 리팩토링
goldm0ng Jan 21, 2025
3ddd7e9
<ADD> 에러 응답 객체 생성 및 예외 응답 구성 변경
goldm0ng Jan 21, 2025
6356ed1
Merge remote-tracking branch 'origin/spring-core' into spring-core
goldm0ng Jan 21, 2025
19a7538
<FIX> 괄호 누락 수정
goldm0ng Jan 21, 2025
d8150ec
<FIX> Config 네이밍 범용적으로 수정
goldm0ng Jan 22, 2025
d13e1f9
<ADD> 인증 정보 추출 인터페이스 추가
goldm0ng Jan 22, 2025
4016bfa
<FIX> 인증 방식 변경 시, 변경 범위 최소화 되도록 인터페이스 두기
goldm0ng Jan 22, 2025
4248c0e
<FIX> JWT 인증 로직 구현체 빈 등록
goldm0ng Jan 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/roomescape/DataLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package roomescape;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import roomescape.member.Member;
import roomescape.member.MemberRepository;

@Profile("default")
@Component
@RequiredArgsConstructor
public class DataLoader implements CommandLineRunner {

private final MemberRepository memberRepository;

@Override
public void run(String... args) throws Exception {
Member member1 = new Member("어드민", "admin@email.com", "password", "ADMIN");
Member member2 = new Member("브라운", "brown@email.com", "password", "USER");

memberRepository.save(member1);
memberRepository.save(member2);
}
}
48 changes: 48 additions & 0 deletions src/main/java/roomescape/TestDataLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package roomescape;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import roomescape.member.Member;
import roomescape.member.MemberRepository;
import roomescape.reservation.Reservation;
import roomescape.reservation.ReservationRepository;
import roomescape.theme.Theme;
import roomescape.theme.ThemeRepository;
import roomescape.time.Time;
import roomescape.time.TimeRepository;

@Profile("test")
@Component
@RequiredArgsConstructor
public class TestDataLoader implements CommandLineRunner {

private final TimeRepository timeRepository;
private final ThemeRepository themeRepository;
private final MemberRepository memberRepository;
private final ReservationRepository reservationRepository;

@Override
public void run(String... args) throws Exception {
Member adminMember = memberRepository.save(new Member("어드민", "admin@email.com", "password", "ADMIN"));
Member userMember = memberRepository.save(new Member("브라운", "brown@email.com", "password", "USER"));

final Time time1 = timeRepository.save(new Time("10:00"));
final Time time2 = timeRepository.save(new Time("12:00"));
final Time time3 = timeRepository.save(new Time("14:00"));
final Time time4 = timeRepository.save(new Time("16:00"));
final Time time5 = timeRepository.save(new Time("18:00"));
final Time time6 = timeRepository.save(new Time("20:00"));

final Theme theme1 = themeRepository.save(new Theme("테마1", "테마1입니다."));
final Theme theme2 = themeRepository.save(new Theme("테마2", "테마2입니다."));
final Theme theme3 = themeRepository.save(new Theme("테마3", "테마3입니다."));

reservationRepository.save(new Reservation("어드민", "2024-03-01", time1, theme1, adminMember));
reservationRepository.save(new Reservation("어드민", "2024-03-01", time2, theme2, adminMember));
reservationRepository.save(new Reservation("어드민", "2024-03-01", time3, theme3, adminMember));

reservationRepository.save(new Reservation("브라운", "2024-03-01", time1, theme2, userMember));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package roomescape.authentication;

import jakarta.servlet.http.Cookie;

public interface AuthenticationExtractor {

MemberAuthInfo extractMemberAuthInfoFromToken(String token);

AuthenticationResponse extractTokenFromCookie(Cookie[] cookies);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,29 @@
import jakarta.servlet.http.Cookie;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import roomescape.authentication.jwt.JwtAuthenticationInfoExtractor;
import roomescape.member.Member;

@Service
@RequiredArgsConstructor
public class AuthenticationService {

Choose a reason for hiding this comment

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

Service 가 떨어지면서 service 라는 네이밍은 약간 애매해진 것 같아요!

Choose a reason for hiding this comment

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

미션에서 뭘 바라는지는 대충 고민해봤는데요
그냥 단순 개인의 제안으로 봐주세요

AuthenticationServixe가 진짜 service, 어노테이션이 없으면 좋나?는 잘 모르겠어요
반대로 authentication Provider, jwtAuthenticationExtractor는 이대로 있으면 변경하기 쉬울까? 도 잘 모르겠어요

제가 생각하는 방향은 다음과 같은데요

인증 방식이 바뀌었을 때 어디까지 변경이 있어야 할까를 고민해보시면 조금 더 결정하기 쉬우실 것 같아요
지금의 경우에는 authentication Service. AuthenticationConfiguration까지는 바뀌겠죠?
JwtExtractor에 직접 의존하고 있고,
그렇다면 조금 더 적은 범위만 바뀔 수 있을까요?
극단적으로 가면 configuration + 다른 인증 방식의 토큰관련 로직을 당당하는 클래스 정도만 추가되면 인증을 바꿀 수 있도록 하려면 어떻게 해야 할까요?
Authenticationservicw에서 직접 jwt를 의존하지 않고, extractor 의 인터페이스에 의존하고 진짜 service가 되고
Configuration파일에서는 관련되는 extractor 과 같은 인터페이스의 구현체를 등록하면 어떨까요?

Copy link
Author

@goldm0ng goldm0ng Jan 22, 2025

Choose a reason for hiding this comment

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

Authenticationservicw에서 직접 jwt를 의존하지 않고, extractor 의 인터페이스에 의존하고 진짜 service가 되고
Configuration파일에서는 관련되는 extractor 과 같은 인터페이스의 구현체를 등록하면 어떨까요?

저도 누누님의 의견에 동의합니다! 말씀하신 방식대로 리팩토링 한다면 변경이 발생했을 때, 변경의 범위를 최소화할 수 있을 것 같아요. 인증 방식이 변경되었을 때, WebConfig에 빈으로 등록되는 구현체만 갈아끼워주면 되는 거니까요!

사실 처음에 AuthenticationProvider을 인터페이스로 놓을 때
AuthenticationExtractor도 같이 인터페이스로 두려고 생각했었는데, 아래와 같은 고민 때문에 구현체로만 뒀었는데요!!

<고민사항>

Spring의 인증/인가 방식은 정말 다양햐더군요.
Q. 각각의 인증 방식마다 인증 로직과 방식 등이 다를 것인데,

  • 누누님 말씀대로 Extractor를 인터페이스로 둘 경우
    현재 구현되어 있는 메서드들이 사용되는 경우도 있을 것이고, 방식이 달라서 추가해야하는 경우도 있을텐데 인터페이스로 두는 게 적절한 방식인건지!? 궁금합니다.
    뭔가 인터페이스라고 하면, 공통된 로직에서 확장하여 추가적으로 메서드를 만들어 사용하는 방식이라고 생각이 들어서 더욱 확신이 안 서는 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

그런데 이런 고민을 하기 전에 먼저 Spring의 인증 방식에 대해 알아볼 필요가 있는 것 같아,
사용 방식에 따른 인증 방식을 gpt 선생님을 이용해 분류해봤습니다.

<쿠키에서 정보를 추출하거나 사용하는 방식>

  • Form-Based Authentication : 사용자의 로그인 정보는 세션과 연관된 쿠키에 저장됩니다. 서버는 쿠키를 통해 세션을 확인하고 사용자를 인증합니다.

  • Session-Based Authentication : 세션 ID가 쿠키에 저장되며, 이를 통해 서버가 인증 상태를 유지합니다.

  • Remember-Me Authentication : 사용자가 "로그인 상태 유지"를 선택하면 쿠키에 인증 정보 또는 토큰이 저장됩니다.

<토큰에서 정보를 추출하거나 사용하는 방식>

  • Token-Based Authentication (JWT) : 클라이언트가 서버로부터 발급받은 JWT를 사용합니다. JWT는 인증 정보를 포함하며, 서버는 이를 검증하여 사용자 인증 상태를 확인합니다.

  • OAuth 2.0 : 액세스 토큰을 발급받아 API 요청 시 사용합니다. 토큰에 사용자 정보나 권한 정보가 포함될 수 있습니다.

  • OpenID Connect : OAuth 2.0의 확장으로, ID 토큰을 통해 인증 및 프로필 정보를 제공합니다.

  • SAML Authentication : SAML 토큰(XML 형식)을 사용하여 인증 정보를 교환합니다.

<기타 방식 (쿠키나 토큰 비사용 또는 선택적 사용)>

  • Basic Authentication : HTTP 헤더에 Base64 인코딩된 사용자 이름과 비밀번호를 포함하여 인증합니다. 쿠키나 토큰을 사용하지 않습니다.

  • Digest Authentication : 요청 시 헤더에 해시값을 포함하여 인증합니다. 쿠키나 토큰을 사용하지 않습니다.

  • X.509 Certificate Authentication : 클라이언트의 인증서를 사용하여 인증합니다. 쿠키나 토큰과는 관계없습니다.

  • LDAP Authentication : LDAP 서버에서 사용자 정보를 조회하여 인증합니다. 쿠키나 토큰과는 직접적인 관계는 없습니다.

  • Social Login : OAuth 2.0 기반이므로 액세스 토큰이나 쿠키가 사용될 수 있습니다.

  • Multi-Factor Authentication (MFA) : 기본 인증 방식과 조합되며, 인증 방법에 따라 쿠키나 토큰 사용 여부가 달라집니다.

  • Custom Authentication Provider : 개발자가 정의한 방식에 따라 쿠키나 토큰을 사용할 수 있습니다.

Copy link
Author

@goldm0ng goldm0ng Jan 22, 2025

Choose a reason for hiding this comment

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

이 중 대표적으로 사용되는 인증 방식은 JWT 토큰 인증 방식, OAuth 방식이라고 합니다.
(추가적으로, 토스는 어떤 인증 방식을 사용하는지도 궁금하네요!??)
이 두 인증 방식을 보니, 누누님께서 제안해주신 방법으로 리팩토링해도 현재 코드에서는 제가 질문드린 내용들을 생각하지 않아도 될 것 같네요!

참고해서 리팩토링 진행해보겠습니다~!

Choose a reason for hiding this comment

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

저희는 보통

  1. 전용선 or vpn + oauth2
  2. mtls
  3. https + 특정 ip 만 허용 + oauth2 를 사용하는 것 같아요

근데, 이 레이어를 서비스 개발자가 알 필요는 전혀 없었던 것 같아요
인증은 다른 팀에서 전부 처리해주기 때문인데요

누누님 말씀대로 Extractor를 인터페이스로 둘 경우
현재 구현되어 있는 메서드들이 사용되는 경우도 있을 것이고, 방식이 달라서 추가해야하는 경우도 있을텐데 인터페이스로 두는 게 적절한 방식인건지!? 궁금합니다.
뭔가 인터페이스라고 하면, 공통된 로직에서 확장하여 추가적으로 메서드를 만들어 사용하는 방식이라고 생각이 들어서 더욱 확신이 안 서는 것 같아요.

일단 이런 케이스에서는 솔직히 약간 짬인것 같은데요
사용하는 쪽에서 뭐가 필요한지를 먼저 생각해보고 나서 이 기능은 뭐가 되었든 무조건 필요하다 라는 생각이 들고 + 다른 방식으로 바뀔 수 있겠다 정도이면 인터페이스로 분리할 수 있을 것 같아요


private final AuthenticationProvider authenticationProvider;
private final JwtAuthenticationInfoExtractor jwtAuthenticationInfoExtractor;
private final AuthenticationExtractor authenticationExtractor;

public AuthenticationResponse createToken(Member member) {
return authenticationProvider.createAuthenticationMethod(member);
}

public AuthenticationResponse extractToken(Cookie [] cookies){
return jwtAuthenticationInfoExtractor.extractTokenFromCookie(cookies);
return authenticationExtractor.extractTokenFromCookie(cookies);
}

public MemberAuthInfo extractMemberInfo(String token) {
return jwtAuthenticationInfoExtractor.extractMemberAuthInfoFromToken(token);
return authenticationExtractor.extractMemberAuthInfoFromToken(token);
}

public MemberAuthInfo getMemberAuthInfoFromCookies(Cookie[] cookies) {
AuthenticationResponse authenticationResponse = extractToken(cookies);
return extractMemberInfo(authenticationResponse.accessToken());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

@Configuration
@RequiredArgsConstructor
public class AuthenticationConfig implements WebMvcConfigurer {
public class AuthenticationWebConfig implements WebMvcConfigurer {

Choose a reason for hiding this comment

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

보통 요런 클래스는 인증 관련만 다루지 않게 되다보니 webConfig 같이 조금 더 범용적인 네이밍이 되면 더 좋을 것 같아요!

Copy link
Author

@goldm0ng goldm0ng Jan 22, 2025

Choose a reason for hiding this comment

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

아하! 네 수정하도록 할게요 🫡

그럼 보통은 한 클래스 내에 범용적으로 다루는 편인가요?
A 관련은 AConfig, B 관련은 BConfig 이렇게 나누지 않고요!
즉, 인증 관련 빈만 존재하지 않고 다양한 카테고리가 존재할 경우 말씀드리는 겁니다!

Choose a reason for hiding this comment

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

넵 맞아요
스프링에서 그렇게 인터페이스를 만들어두어서 그렇게 진행합니다!


private final LoginMemberArgumentResolver loginMemberArgumentResolver;
private final AuthAdminRoleInterceptor authAdminRoleInterceptor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ public boolean supportsParameter(MethodParameter parameter) {
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest();

AuthenticationResponse authenticationResponse = authenticationService.extractToken(request.getCookies());
if (authenticationResponse.accessToken() == null) {
MemberAuthInfo memberAuthInfo = authenticationService.getMemberAuthInfoFromCookies(request.getCookies());

if (memberAuthInfo == null){
return null;
}

return authenticationService.extractMemberInfo(authenticationResponse.accessToken());
return memberAuthInfo;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package roomescape.authentication;

public record MemberAuthInfo(
Long id,
String name,
String role) {
}
22 changes: 22 additions & 0 deletions src/main/java/roomescape/authentication/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package roomescape.authentication;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import roomescape.authentication.jwt.JwtAuthenticationInfoExtractor;
import roomescape.authentication.jwt.JwtAuthenticationProvider;

@Configuration
@RequiredArgsConstructor
public class WebConfig {

@Bean
AuthenticationProvider authenticationProvider(){
return new JwtAuthenticationProvider();
}

@Bean
AuthenticationExtractor authenticationExtractor(){
return new JwtAuthenticationInfoExtractor();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@
import jakarta.servlet.http.Cookie;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import roomescape.authentication.AuthenticationExtractor;
import roomescape.authentication.AuthenticationResponse;
import roomescape.authentication.MemberAuthInfo;
import roomescape.exception.JwtValidationException;

import java.util.Arrays;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationInfoExtractor {
public class JwtAuthenticationInfoExtractor implements AuthenticationExtractor {

@Value("${roomescape.auth.jwt.secret}")
private String secretKey;
Expand All @@ -33,10 +32,11 @@ public MemberAuthInfo extractMemberAuthInfoFromToken(String token) {
.parseClaimsJws(token)
.getBody();

Long id = Long.valueOf(claims.getSubject());
String name = claims.get("name", String.class);
String role = claims.get("role", String.class);

return new MemberAuthInfo(name, role);
return new MemberAuthInfo(id, name, role);
} catch (JwtException e) {
throw new JwtValidationException("유효하지 않은 JWT 토큰입니다.", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import roomescape.authentication.AuthenticationProvider;
import roomescape.authentication.AuthenticationResponse;
import roomescape.exception.JwtProviderException;
import roomescape.member.Member;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationProvider implements AuthenticationProvider {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

public class DuplicateReservationException extends RuntimeException {
public DuplicateReservationException(String message) {
super(message);
}
}
19 changes: 19 additions & 0 deletions src/main/java/roomescape/exception/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package roomescape.exception;

public class ErrorResponse {
private final String message;
private final String type;

public ErrorResponse(String message, String type) {
this.message = message;
this.type = type;
}

public String getMessage() {
return message;
}

public String getType() {
return type;
}
}
30 changes: 18 additions & 12 deletions src/main/java/roomescape/exception/GeneralExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
package roomescape.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GeneralExceptionHandler {

@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<String> handleMemberNotFound(MemberNotFoundException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
@ExceptionHandler({MemberNotFoundException.class, JwtValidationException.class, JwtProviderException.class})
public ResponseEntity<ErrorResponse> handleMemberNotFound(Exception e) {
ErrorResponse authenticationErrorResponse = new ErrorResponse(e.getMessage(), "authentication_error");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(authenticationErrorResponse);
}

@ExceptionHandler(JwtValidationException.class)
public ResponseEntity<String> handleJwtValidationException(JwtValidationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
@ExceptionHandler({TimeNotFoundException.class, ThemeNotFoundException.class})
public ResponseEntity<ErrorResponse> handleTimeNotFound(Exception e) {
ErrorResponse notFoundErrorResponse = new ErrorResponse(e.getMessage(), "not_found");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(notFoundErrorResponse);
}

@ExceptionHandler(JwtProviderException.class)
public ResponseEntity<String> handleJwtProviderException(JwtProviderException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
@ExceptionHandler(DuplicateReservationException.class)
public ResponseEntity<ErrorResponse> handleDuplicatedReservation(DuplicateReservationException e) {
ErrorResponse duplicationErrorResponse = new ErrorResponse(e.getMessage(), "duplicate_reservation");
return ResponseEntity.status(HttpStatus.CONFLICT).body(duplicationErrorResponse);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGeneralException(Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
public ResponseEntity<ErrorResponse> handleGeneralException(Exception e) {
log.error("Exception [Err_Location] : {}", e.getStackTrace()[0], e);
ErrorResponse errorResponse = new ErrorResponse("잠깐 문제가 생겼어요. 다음에 다시 시도해주세요.", "internal_server_error");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package roomescape.exception;

public class MemberNotFoundException extends RuntimeException {
public MemberNotFoundException(String message, Throwable cause) {
super(message, cause);

public MemberNotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

public class ThemeNotFoundException extends RuntimeException {
public ThemeNotFoundException(String message) {
super(message);
}
}
7 changes: 7 additions & 0 deletions src/main/java/roomescape/exception/TimeNotFoundException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

public class TimeNotFoundException extends RuntimeException {
public TimeNotFoundException(String message) {
super(message);
}
}
8 changes: 1 addition & 7 deletions src/main/java/roomescape/login/LoginController.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,13 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import roomescape.authentication.AuthenticationService;
import roomescape.authentication.MemberAuthInfo;
import roomescape.authentication.AuthenticationResponse;

@RestController
@RequiredArgsConstructor
public class LoginController {

private final LoginService loginService;
private final AuthenticationService authenticationService;

@PostMapping("/login")
public void login(@Valid @RequestBody LoginRequest loginRequest, HttpServletResponse response) {
Expand All @@ -32,10 +29,7 @@ public void login(@Valid @RequestBody LoginRequest loginRequest, HttpServletResp

@GetMapping("/login/check")
public LoginCheckResponse checkLogin(HttpServletRequest request) {
AuthenticationResponse authenticationResponse = authenticationService.extractToken(request.getCookies());
MemberAuthInfo memberAuthInfo = authenticationService.extractMemberInfo(authenticationResponse.accessToken());
LoginCheckResponse loginCheckResponse = loginService.checkLogin(memberAuthInfo);

LoginCheckResponse loginCheckResponse = loginService.checkLogin(request.getCookies());
return loginCheckResponse;
}

Expand Down
Loading