-
Notifications
You must be signed in to change notification settings - Fork 179
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
[2단계 - 장바구니 기능] 허브(방대의) 미션 제출합니다. #300
Changes from all commits
2256b19
27e9089
02be345
1a0bd1b
33355cf
bf96972
64925cc
2cb430b
b5cf8e6
5d075c1
2d16d45
f13465b
6e8d373
6040e7e
bbf1884
3d81cf3
eebb50b
b273000
cbb8bc6
3d3799c
705361a
ac83592
eb44589
1ac95f0
7b8de76
aa41153
43eece1
fb42fa4
dd5dee5
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,11 @@ | ||
package cart.auth; | ||
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. 나머지는 레이어드 아키텍처 기준으로 패키지 규칙이 정해져있는데, 갑자기 도메인 기준의 패키지가 나와버리면 혼동이 올거 같아요 ㅎ auth 패키지 내에선 cart의 다른 도메인을 의존하지 않도록 잘 구성해주셨으니, 아예 cart/auth가 아니라 auth 패키지로 분리하는건 어떨까요? (이 경우 component scan 범위가 달라지는데, 해당 부분도 어떻게 해결할 수 있을지 학습해보면 좋을 거같아요) 좀 더 코드를 보다보니 Member에 대한 의존이 있었군요 ㅎ 저라면 Member를 똑같이 복사해 해당 패키지에서 다루던지, 아니면 Credential 객체를 Member 테이블로부터 직접 조회하는 방법을 쓸거같습니다
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. 현재의 패키지 규칙을 생각하지 못하고, 모두 auth 패키지 안에 모두 작성해 일관성이 깨졌네요.. 😢 생각하지 못했습니다.. @ComponentScan(basePackages = "auth")
@Configuration
public class ComponentScanConfiguration {
} 현재는 레이어드 방식으로 코드를 재구성하는 것이 더욱 간단해 보여서 1번을 선택해보겠습니다 😄 |
||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
@Target(ElementType.PARAMETER) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface Auth { | ||
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. 커스텀 어노테이션 활용 👍 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package cart.auth; | ||
|
||
import org.springframework.core.MethodParameter; | ||
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; | ||
|
||
public class AuthArgumentResolver implements HandlerMethodArgumentResolver { | ||
|
||
private final CredentialThreadLocal credentialThreadLocal; | ||
|
||
public AuthArgumentResolver(final CredentialThreadLocal credentialThreadLocal) { | ||
this.credentialThreadLocal = credentialThreadLocal; | ||
} | ||
|
||
@Override | ||
public boolean supportsParameter(final MethodParameter parameter) { | ||
final boolean hasParameterAnnotation = parameter.hasParameterAnnotation(Auth.class); | ||
final boolean hasCredentialType = Credential.class.isAssignableFrom(parameter.getParameterType()); | ||
return hasParameterAnnotation && hasCredentialType; | ||
} | ||
|
||
@Override | ||
public Object resolveArgument( | ||
final MethodParameter parameter, | ||
final ModelAndViewContainer mavContainer, | ||
final NativeWebRequest webRequest, | ||
final WebDataBinderFactory binderFactory | ||
) { | ||
final Credential credential = credentialThreadLocal.get(); | ||
credentialThreadLocal.clear(); | ||
return credential; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package cart.auth; | ||
|
||
import cart.dao.CredentialDao; | ||
import cart.exception.AuthenticationException; | ||
import javax.servlet.http.HttpServletRequest; | ||
import javax.servlet.http.HttpServletResponse; | ||
import org.springframework.web.servlet.HandlerInterceptor; | ||
|
||
public class AuthInterceptor implements HandlerInterceptor { | ||
|
||
private static final String AUTHORIZATION_HEADER_NAME = "Authorization"; | ||
|
||
private final CredentialDao credentialDao; | ||
private final BasicAuthorizationParser basicAuthorizationParser; | ||
private final CredentialThreadLocal credentialThreadLocal; | ||
|
||
public AuthInterceptor( | ||
final CredentialDao credentialDao, | ||
final BasicAuthorizationParser basicAuthorizationParser, | ||
final CredentialThreadLocal credentialThreadLocal | ||
) { | ||
this.credentialDao = credentialDao; | ||
this.basicAuthorizationParser = basicAuthorizationParser; | ||
this.credentialThreadLocal = credentialThreadLocal; | ||
} | ||
|
||
@Override | ||
public boolean preHandle( | ||
final HttpServletRequest request, | ||
final HttpServletResponse response, | ||
final Object handler | ||
) { | ||
final String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER_NAME); | ||
|
||
final Credential credential = basicAuthorizationParser.parse(authorizationHeader); | ||
final Credential savedCredential = credentialDao.findByEmail(credential.getEmail()) | ||
.orElseThrow(() -> new AuthenticationException("올바르지 않은 이메일입니다. 입력값: " + credential.getEmail())); | ||
|
||
if (credential.isNotSamePassword(savedCredential)) { | ||
throw new AuthenticationException(); | ||
} | ||
|
||
credentialThreadLocal.set(savedCredential); | ||
return true; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package cart.auth; | ||
|
||
import cart.exception.InvalidBasicCredentialException; | ||
import java.util.Base64; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class BasicAuthorizationParser { | ||
|
||
private static final String KEYWORD = "Basic "; | ||
private static final String DELIMITER = ":"; | ||
private static final int VALID_CREDENTIAL_SIZE = 2; | ||
private static final int EMAIL_INDEX = 0; | ||
private static final int PASSWORD_INDEX = 1; | ||
private static final String EMPTY = ""; | ||
|
||
public Credential parse(final String authorizationHeader) { | ||
final String[] credential = parseCredential(authorizationHeader); | ||
if (isInvalidBasicCredential(authorizationHeader)) { | ||
throw new InvalidBasicCredentialException(authorizationHeader); | ||
} | ||
return new Credential(credential[EMAIL_INDEX], credential[PASSWORD_INDEX]); | ||
} | ||
|
||
private String[] parseCredential(final String authorizationHeader) { | ||
final String credential = authorizationHeader.replace(KEYWORD, EMPTY); | ||
return decodeBase64(credential).split(DELIMITER); | ||
} | ||
|
||
private String decodeBase64(final String credential) { | ||
try { | ||
return new String(Base64.getDecoder().decode(credential)); | ||
} catch (final IllegalArgumentException e) { | ||
throw new InvalidBasicCredentialException(credential); | ||
} | ||
} | ||
|
||
private boolean isInvalidBasicCredential(final String authorizationHeader) { | ||
return !authorizationHeader.startsWith(KEYWORD) || | ||
parseCredential(authorizationHeader).length != VALID_CREDENTIAL_SIZE; | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package cart.auth; | ||
|
||
import org.springframework.lang.Nullable; | ||
|
||
public class Credential { | ||
|
||
private final Long memberId; | ||
private final String email; | ||
private final String password; | ||
|
||
public Credential(final String email, final String password) { | ||
this(null, email, password); | ||
} | ||
|
||
public Credential(final Long memberId, final String email, final String password) { | ||
this.memberId = memberId; | ||
this.email = email; | ||
this.password = password; | ||
} | ||
|
||
public boolean isNotSamePassword(final Credential credential) { | ||
return !this.password.equals(credential.getPassword()); | ||
} | ||
|
||
@Nullable | ||
public Long getMemberId() { | ||
return memberId; | ||
} | ||
|
||
public String getEmail() { | ||
return email; | ||
} | ||
|
||
public String getPassword() { | ||
return password; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package cart.auth; | ||
|
||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class CredentialThreadLocal { | ||
|
||
private final ThreadLocal<Credential> local = new ThreadLocal<>(); | ||
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. 감사합니다 😄 |
||
|
||
public void set(final Credential credential) { | ||
local.set(credential); | ||
} | ||
|
||
public Credential get() { | ||
return local.get(); | ||
} | ||
|
||
public void clear() { | ||
local.remove(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package cart.config; | ||
|
||
import cart.auth.AuthArgumentResolver; | ||
import cart.auth.AuthInterceptor; | ||
import cart.auth.BasicAuthorizationParser; | ||
import cart.auth.CredentialThreadLocal; | ||
import cart.dao.CredentialDao; | ||
import java.util.List; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.web.method.support.HandlerMethodArgumentResolver; | ||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; | ||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
|
||
@Configuration | ||
public class WebConfiguration 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. 스프링 제공기능인 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. 감사합니다! 👍 |
||
|
||
private final CredentialDao credentialDao; | ||
private final BasicAuthorizationParser basicAuthorizationParser; | ||
private final CredentialThreadLocal credentialThreadLocal; | ||
|
||
public WebConfiguration( | ||
final CredentialDao credentialDao, | ||
final BasicAuthorizationParser basicAuthorizationParser, | ||
final CredentialThreadLocal credentialThreadLocal | ||
) { | ||
this.credentialDao = credentialDao; | ||
this.basicAuthorizationParser = basicAuthorizationParser; | ||
this.credentialThreadLocal = credentialThreadLocal; | ||
} | ||
|
||
@Override | ||
public void addInterceptors(final InterceptorRegistry registry) { | ||
final AuthInterceptor authInterceptor = new AuthInterceptor( | ||
credentialDao, | ||
basicAuthorizationParser, | ||
credentialThreadLocal | ||
); | ||
registry.addInterceptor(authInterceptor).addPathPatterns("/cart-products/**"); | ||
} | ||
|
||
@Override | ||
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) { | ||
resolvers.add(new AuthArgumentResolver(credentialThreadLocal)); | ||
} | ||
} |
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.
API 명세서 좋네요 ㅎ
요청객체와 응답객체 형태, 그리고 발생할 수 있는 오류와 그 때의 오류 코드 및 응답객체 까지 추가한다면 현업에서도 활용할만한 API 문서가 됩니다
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.
오.. 코멘트를 읽고 궁금한게 웨지는 Swagger, Restdocs 중 어떤걸 선호하시나요?
현업에서는 RestDocs에서 OpenAPI Spec을 추출해서 Swagger UI를 연동해서 많이 사용하나요?
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.
예전엔 프로덕션 코드에 문서화 코드가 들어가는게 싫어서 restdocs 선호했는데 요샌 문서 옆에 띄워놓고 문서화 주석들을 바로 비교 / 수정이 가능한 Swagger가 더 괜찮다 싶긴해요.
회사마다 다르겠지만 저희 회사는 팀마다 문서화 방식이 달라서 각자 알아서하는데,
말씀하신 것처럼 Restdocs -> OpenApi Spec -> Swagger 같은 커스텀은 수요가 적을거 같아요 (그 작업 할 개발 리소스를 기능 개발 / 리팩토링에 투자할 거 같습니다)