-
Notifications
You must be signed in to change notification settings - Fork 66
[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
base: goldm0ng
Are you sure you want to change the base?
Changes from all commits
5913de1
67da617
53828d8
0ec5186
ebf6407
22f9e30
347d0c7
2a96836
91bf7bf
16e217b
4a9ff8c
a5e8d66
e027b31
c7a1875
2358568
b44dab1
330b40c
7a7ed55
c5f4f97
580c6ab
edfbc2c
312bdea
bef9160
f164ce0
e2a32bc
9c5e253
0840983
da71532
84f4e41
fd7a976
fcf74d3
54d412e
a4b3705
b8d792b
aba712d
d472034
751587d
e963234
8b292c3
39a0f7d
e7e5c7b
a80573b
08017a7
35ec5f7
1959159
b4bf7fb
c74ea76
c4d0384
a25995f
f900f55
ed9cc5d
1d0ce07
b00df65
8ace050
54e6add
61ab41f
4fd9e4a
1fa1b1a
01eb56c
3ddd7e9
6356ed1
19a7538
d8150ec
d13e1f9
4016bfa
4248c0e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
} | ||
} |
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 |
---|---|---|
|
@@ -10,7 +10,7 @@ | |
|
||
@Configuration | ||
@RequiredArgsConstructor | ||
public class AuthenticationConfig implements WebMvcConfigurer { | ||
public class AuthenticationWebConfig implements WebMvcConfigurer { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 보통 요런 클래스는 인증 관련만 다루지 않게 되다보니 webConfig 같이 조금 더 범용적인 네이밍이 되면 더 좋을 것 같아요! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하! 네 수정하도록 할게요 🫡 그럼 보통은 한 클래스 내에 범용적으로 다루는 편인가요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 맞아요 |
||
|
||
private final LoginMemberArgumentResolver loginMemberArgumentResolver; | ||
private final AuthAdminRoleInterceptor authAdminRoleInterceptor; | ||
|
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) { | ||
} |
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 |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package roomescape.exception; | ||
|
||
public class DuplicateReservationException extends RuntimeException { | ||
public DuplicateReservationException(String message) { | ||
super(message); | ||
} | ||
} |
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; | ||
} | ||
} |
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); | ||
} | ||
} |
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); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Service 가 떨어지면서 service 라는 네이밍은 약간 애매해진 것 같아요!
There was a problem hiding this comment.
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 과 같은 인터페이스의 구현체를 등록하면 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 누누님의 의견에 동의합니다! 말씀하신 방식대로 리팩토링 한다면 변경이 발생했을 때, 변경의 범위를 최소화할 수 있을 것 같아요. 인증 방식이 변경되었을 때, WebConfig에 빈으로 등록되는 구현체만 갈아끼워주면 되는 거니까요!
사실 처음에 AuthenticationProvider을 인터페이스로 놓을 때
AuthenticationExtractor도 같이 인터페이스로 두려고 생각했었는데, 아래와 같은 고민 때문에 구현체로만 뒀었는데요!!
<고민사항>
Spring의 인증/인가 방식은 정말 다양햐더군요.
Q. 각각의 인증 방식마다 인증 로직과 방식 등이 다를 것인데,
현재 구현되어 있는 메서드들이 사용되는 경우도 있을 것이고, 방식이 달라서 추가해야하는 경우도 있을텐데 인터페이스로 두는 게 적절한 방식인건지!? 궁금합니다.
뭔가 인터페이스라고 하면, 공통된 로직에서 확장하여 추가적으로 메서드를 만들어 사용하는 방식이라고 생각이 들어서 더욱 확신이 안 서는 것 같아요.
There was a problem hiding this comment.
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 : 개발자가 정의한 방식에 따라 쿠키나 토큰을 사용할 수 있습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 중 대표적으로 사용되는 인증 방식은 JWT 토큰 인증 방식, OAuth 방식이라고 합니다.
(추가적으로, 토스는 어떤 인증 방식을 사용하는지도 궁금하네요!??)
이 두 인증 방식을 보니, 누누님께서 제안해주신 방법으로 리팩토링해도 현재 코드에서는 제가 질문드린 내용들을 생각하지 않아도 될 것 같네요!
참고해서 리팩토링 진행해보겠습니다~!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저희는 보통
근데, 이 레이어를 서비스 개발자가 알 필요는 전혀 없었던 것 같아요
인증은 다른 팀에서 전부 처리해주기 때문인데요
일단 이런 케이스에서는 솔직히 약간 짬인것 같은데요
사용하는 쪽에서 뭐가 필요한지를 먼저 생각해보고 나서 이 기능은 뭐가 되었든 무조건 필요하다 라는 생각이 들고 + 다른 방식으로 바뀔 수 있겠다 정도이면 인터페이스로 분리할 수 있을 것 같아요