-
Notifications
You must be signed in to change notification settings - Fork 97
[그리디] 강동현 Spring JPA (2차) 4, 5, 6 단계 미션 제출합니다. #210
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: mintcoke123
Are you sure you want to change the base?
Changes from 52 commits
3d3151e
0c3e685
8b26e96
378e7c5
cfd15f6
b456b89
97cf233
451beff
f1e3288
0d5c991
f6a7db0
20d7526
1357d07
12171bb
1eb6513
a108bc6
069e91f
f7a7485
b4869c1
97e0106
4b90a36
9636a93
06b14d0
12f68bb
ee61649
589badc
d70f3c3
ee530e2
7554215
8d6b22b
3a20e9f
aacf60c
b935aca
43de312
744396a
be881da
ce83b57
c61db0a
d7e38ec
1c30a05
a44bd1f
739e9db
c05f17e
5b99e63
68ae9f5
c8ef4c0
26a581a
0149b40
94fd21f
134fb43
43a92c7
6358623
02dc95d
b69d20f
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 |
|---|---|---|
|
|
@@ -35,3 +35,6 @@ out/ | |
|
|
||
| ### VS Code ### | ||
| .vscode/ | ||
|
|
||
|
|
||
| application-local.properties | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,49 @@ | ||
| package roomescape; | ||
|
|
||
| import jakarta.persistence.EntityNotFoundException; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.ControllerAdvice; | ||
| import org.springframework.http.converter.HttpMessageNotReadableException; | ||
| import org.springframework.web.bind.MethodArgumentNotValidException; | ||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||
| import roomescape.common.ApiError; | ||
|
|
||
| @ControllerAdvice | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import java.util.NoSuchElementException; | ||
|
|
||
| @RestControllerAdvice | ||
| public class ExceptionController { | ||
| @ExceptionHandler(Exception.class) | ||
| public ResponseEntity<Void> handleRuntimeException(Exception e) { | ||
| e.printStackTrace(); | ||
| return ResponseEntity.badRequest().build(); | ||
| } | ||
| @ExceptionHandler(MethodArgumentNotValidException.class) | ||
| public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException e) { | ||
| return build(ApiError.BAD_REQUEST_INVALID_INPUT); | ||
| } | ||
|
|
||
| @ExceptionHandler({IllegalStateException.class}) | ||
| public ResponseEntity<Map<String, Object>> handleIllegalState(IllegalStateException e) { | ||
| return build(ApiError.BAD_REQUEST_ILLEGAL_STATE); | ||
| } | ||
|
|
||
| @ExceptionHandler({NoSuchElementException.class, EntityNotFoundException.class}) | ||
| public ResponseEntity<Map<String, Object>> handleNotFound(RuntimeException e) { | ||
| return build(ApiError.NOT_FOUND_RESOURCE); | ||
| } | ||
|
|
||
| @ExceptionHandler(HttpMessageNotReadableException.class) | ||
| public ResponseEntity<Map<String, Object>> handleNotReadable(HttpMessageNotReadableException e) { | ||
| return build(ApiError.BAD_REQUEST_INVALID_INPUT); | ||
| } | ||
|
|
||
| @ExceptionHandler(Exception.class) | ||
| public ResponseEntity<Map<String, Object>> handleUnknown(Exception e) { | ||
| e.printStackTrace(); | ||
| return build(ApiError.INTERNAL_SERVER_ERROR); | ||
| } | ||
|
|
||
| private ResponseEntity<Map<String, Object>> build(ApiError apiError) { | ||
| Map<String, Object> body = new HashMap<>(); | ||
| body.put("code", apiError.getCode()); | ||
| body.put("message", apiError.getMessage()); | ||
| return ResponseEntity.status(apiError.getHttpStatus()).body(body); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package roomescape; | ||
|
|
||
| import org.springframework.beans.factory.annotation.Value; | ||
| 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; | ||
| import roomescape.auth.AdminAuthInterceptor; | ||
| import roomescape.auth.LoginMemberArgumentResolver; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Configuration | ||
| public class WebConfig implements WebMvcConfigurer { | ||
| @Value("${roomescape.auth.jwt.secret}") | ||
| private String secretKey; | ||
|
|
||
| @Override | ||
| public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { | ||
| resolvers.add(new LoginMemberArgumentResolver(secretKey)); | ||
| } | ||
|
|
||
| @Override | ||
| public void addInterceptors(InterceptorRegistry registry) { | ||
| registry.addInterceptor(new AdminAuthInterceptor(secretKey)) | ||
| .addPathPatterns("/admin", "/admin/**"); | ||
| } | ||
| } | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package roomescape.auth; | ||
|
|
||
| import io.jsonwebtoken.Claims; | ||
| import io.jsonwebtoken.ExpiredJwtException; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import org.springframework.web.servlet.HandlerInterceptor; | ||
| import roomescape.common.ApiError; | ||
| import roomescape.util.JwtUtil; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| public class AdminAuthInterceptor implements HandlerInterceptor { | ||
|
|
||
| private final String secretKey; | ||
|
|
||
| public AdminAuthInterceptor(String secretKey) { | ||
| this.secretKey = secretKey; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { | ||
| String token = JwtUtil.extractTokenFromCookies(request.getCookies()); | ||
| if (token.isEmpty()) { | ||
| writeError(response, ApiError.UNAUTHORIZED_MISSING_TOKEN); | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| Claims claims = JwtUtil.parseClaims(token, secretKey); | ||
| String role = claims.get("role", String.class); | ||
| if (!"ADMIN".equals(role)) { | ||
| writeError(response, ApiError.FORBIDDEN_ADMIN_ONLY); | ||
| return false; | ||
| } | ||
| return true; | ||
| } catch (ExpiredJwtException e) { | ||
| writeError(response, ApiError.UNAUTHORIZED_EXPIRED_TOKEN); | ||
| return false; | ||
| } catch (Exception e) { | ||
| writeError(response, ApiError.UNAUTHORIZED_INVALID_TOKEN); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| private void writeError(HttpServletResponse response, ApiError apiError) { | ||
| response.setStatus(apiError.getHttpStatus().value()); | ||
| response.setContentType("application/json;charset=UTF-8"); | ||
| try { | ||
| String payload = "{\"code\":" + apiError.getCode() + ",\"message\":\"" + apiError.getMessage() + "\"}"; | ||
| response.getWriter().write(payload); | ||
| } catch (IOException ignored) { | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+46
to
+55
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. 나중에 이런 곳에 에러 로그 (ex. slf4j)를 추가하는 것도 좋아보이네요~
Author
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. 넵! 이후 코드를 짤때는 logger를 적용해보겠습니다! |
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package roomescape.auth; | ||
|
|
||
| import io.jsonwebtoken.Claims; | ||
| import io.jsonwebtoken.ExpiredJwtException; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| 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; | ||
| import roomescape.member.LoginMemberDto; | ||
| import roomescape.util.JwtUtil; | ||
|
|
||
| public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { | ||
|
|
||
| private final String secretKey; | ||
|
|
||
| public LoginMemberArgumentResolver(String secretKey) { | ||
| this.secretKey = secretKey; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean supportsParameter(MethodParameter parameter) { | ||
| return parameter.getParameterType().equals(LoginMemberDto.class); | ||
| } | ||
|
|
||
| @Override | ||
| public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { | ||
| HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); | ||
| String token = JwtUtil.extractTokenFromCookies(request.getCookies()); | ||
|
|
||
| if (token.isEmpty()) { | ||
| return null; | ||
| } | ||
|
|
||
| try { | ||
| Claims claims = JwtUtil.parseClaims(token, secretKey); | ||
|
|
||
| Long id = Long.valueOf(claims.getSubject()); | ||
| String name = claims.get("name", String.class); | ||
| String role = claims.get("role", String.class); | ||
|
|
||
| return new LoginMemberDto(id, name, null, role); | ||
| } catch (ExpiredJwtException e) { | ||
| return null; | ||
| } catch (Exception e) { | ||
| return null; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package roomescape.common; | ||
|
|
||
| import lombok.Getter; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| @Getter | ||
| public enum ApiError { | ||
| BAD_REQUEST_INVALID_INPUT(HttpStatus.BAD_REQUEST, 40001, "잘못된 요청입니다."), | ||
| BAD_REQUEST_ILLEGAL_STATE(HttpStatus.BAD_REQUEST, 40002, "요청을 처리할 수 없습니다."), | ||
| NOT_FOUND_RESOURCE(HttpStatus.NOT_FOUND, 40401, "리소스를 찾을 수 없습니다."), | ||
| UNAUTHORIZED_MISSING_TOKEN(HttpStatus.UNAUTHORIZED, 40101, "토큰이 없습니다."), | ||
| UNAUTHORIZED_INVALID_TOKEN(HttpStatus.UNAUTHORIZED, 40102, "토큰이 유효하지 않습니다."), | ||
| UNAUTHORIZED_EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, 40103, "토큰이 만료되었습니다."), | ||
| FORBIDDEN_ADMIN_ONLY(HttpStatus.FORBIDDEN, 40301, "관리자 권한이 필요합니다."), | ||
| INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50000, "서버 오류가 발생했습니다."); | ||
|
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. 요거 보면서 궁금한 점이 있는데요. 이걸 사용하면 어떤 점이 장점으로 느껴지시나요?? 특히 아래의 장점이 무엇일지 궁금해요
다른 클래스들도 맨 아래에 개행이 여러줄로 되어있던데 한줄로 삭제부탁드려요~
Author
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 HttpStatus httpStatus; | ||
| private final int code; | ||
| private final String message; | ||
|
|
||
| ApiError(HttpStatus httpStatus, int code, String message) { | ||
| this.httpStatus = httpStatus; | ||
| this.code = code; | ||
| this.message = message; | ||
| } | ||
| } | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package roomescape.member; | ||
|
|
||
| public record LoginMemberDto(Long id, String name, String email, String role) {} | ||
|
|
||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,30 @@ | ||
| package roomescape.member; | ||
|
|
||
| import jakarta.persistence.*; | ||
|
|
||
| @Entity | ||
| public class Member { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
| private String name; | ||
| @Column(unique = true) | ||
| private String email; | ||
|
Comment on lines
11
to
12
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. 요거 최근에 팀에서 이야기 나왔던 내용인데, 동현님의 생각도 궁금하네요
팀에서는 이런 이야기가 나왔었어요
Author
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 String password; | ||
| private String role; | ||
| @Enumerated(EnumType.STRING) | ||
| private Role role; | ||
|
|
||
| public Member() { | ||
| } | ||
|
|
||
| public Member(Long id, String name, String email, String role) { | ||
| public Member(Long id, String name, String email, Role role) { | ||
| this.id = id; | ||
| this.name = name; | ||
| this.email = email; | ||
| this.role = role; | ||
| } | ||
|
|
||
| public Member(String name, String email, String password, String role) { | ||
| public Member(String name, String email, String password, Role role) { | ||
| this.name = name; | ||
| this.email = email; | ||
| this.password = password; | ||
|
|
@@ -37,7 +47,7 @@ public String getPassword() { | |
| return password; | ||
| } | ||
|
|
||
| public String getRole() { | ||
| public Role getRole() { | ||
| return role; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,34 +3,68 @@ | |
| import jakarta.servlet.http.Cookie; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
| import roomescape.util.JwtUtil; | ||
|
|
||
| import java.net.URI; | ||
|
|
||
| @RestController | ||
| public class MemberController { | ||
| private MemberService memberService; | ||
| @Value("${roomescape.auth.jwt.secret}") | ||
| private String secretKey; | ||
|
|
||
| public MemberController(MemberService memberService) { | ||
| this.memberService = memberService; | ||
| } | ||
|
|
||
| @PostMapping("/members") | ||
| public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { | ||
| MemberResponse member = memberService.createMember(memberRequest); | ||
| return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); | ||
| public ResponseEntity createMember(@RequestBody MemberRequestDto memberRequest) { | ||
| MemberResponseDto member = memberService.createMember(memberRequest); | ||
|
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. 요거는
Author
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. Dto 가 명시되어야 하기 때문입니다! 제가 처음부터 구조를 짰다면 dto 폴더를 별도로 만들었을 텐데, 기존의 코드에 덧대어 코드를 작성하면서 dto의 이름을 MemberResponse고 하고 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. 제가 동현님 말을 잘 이해하지 못한 것 같은데요 dto 폴더를 별도로 만든다는게 member, reservation, ... 과 같은 레벨의 패키지를 둔다는 것인가요?? 요러면 그냥 member 패키지 안에 dto 패키지를 생성해도 문제 없을 것으로 보여서요 |
||
| return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); | ||
| } | ||
|
|
||
| @PostMapping("/login") | ||
| public ResponseEntity login(@RequestBody MemberRequestDto memberRequest, HttpServletResponse response) { | ||
| Member member = memberService.login(memberRequest.email(), memberRequest.password()); | ||
|
|
||
| String accessToken = createToken(member); | ||
|
|
||
| Cookie cookie = JwtUtil.createAuthCookie(accessToken, JwtUtil.DEFAULT_MAX_AGE_SECONDS, false); | ||
| response.addCookie(cookie); | ||
|
|
||
| return ResponseEntity.ok().build(); | ||
| } | ||
|
|
||
| @GetMapping("/login/check") | ||
| public ResponseEntity<MemberResponseDto> checkLogin(HttpServletRequest request) { | ||
| String token = JwtUtil.extractTokenFromCookies(request.getCookies()); | ||
|
|
||
| String name = JwtUtil.parseClaims(token, secretKey).get("name", String.class); | ||
|
|
||
| MemberResponseDto body = new MemberResponseDto(null, name, null); | ||
| return ResponseEntity.ok(body); | ||
| } | ||
|
|
||
| public String createToken(Member member) { | ||
| return JwtUtil.createToken(member.getId().toString(), member.getName(), member.getRole(), secretKey); | ||
| } | ||
|
|
||
|
|
||
| public String createTokenFromEmailAndPassword(String email, String password) { | ||
| Member member = memberService.login(email, password); | ||
| return JwtUtil.createToken(member.getId().toString(), member.getName(), member.getRole(), secretKey); | ||
| } | ||
|
|
||
|
|
||
| @PostMapping("/logout") | ||
| public ResponseEntity logout(HttpServletResponse response) { | ||
| Cookie cookie = new Cookie("token", ""); | ||
| cookie.setHttpOnly(true); | ||
| cookie.setPath("/"); | ||
| cookie.setMaxAge(0); | ||
| Cookie cookie = JwtUtil.createExpiredAuthCookie(); | ||
| response.addCookie(cookie); | ||
| return ResponseEntity.ok().build(); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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,PageController도 마찬가지입니다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.
리팩토링을 진행할 때 신경쓴 범위가 너무 좁았던 것 같습니다!
다만 지적해주신 것처럼 공용 관심사 성격이 명확한 클래스들이기 때문에, ExceptionController를roomescape.common.ExceptionController로 이동하였습니다!