-
Notifications
You must be signed in to change notification settings - Fork 55
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
[Spring Data JPA] 안금서 미션 제출합니다. #110
base: goldm0ng
Are you sure you want to change the base?
Changes from 39 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
4c8b450
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package roomescape.authentication; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.servlet.HandlerInterceptor; | ||
import roomescape.authentication.jwt.JwtResponse; | ||
import roomescape.authentication.jwt.JwtUtils; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class AuthAdminRoleInterceptor implements HandlerInterceptor { | ||
|
||
private final JwtUtils jwtUtils; | ||
|
||
@Override | ||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { | ||
JwtResponse jwtResponse = jwtUtils.extractTokenFromCookie(request.getCookies()); | ||
if (jwtResponse.accessToken() == null || !isAdmin(jwtResponse.accessToken())) { | ||
response.setStatus(401); | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private boolean isAdmin(String token) { | ||
MemberAuthInfo memberAuthInfo = jwtUtils.extractMemberAuthInfoFromToken(token); | ||
return "ADMIN".equals(memberAuthInfo.role()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package roomescape.authentication; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
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 java.util.List; | ||
|
||
@Configuration | ||
@RequiredArgsConstructor | ||
public class AuthenticationConfig implements WebMvcConfigurer { | ||
|
||
private final LoginMemberArgumentResolver loginMemberArgumentResolver; | ||
private final AuthAdminRoleInterceptor authAdminRoleInterceptor; | ||
|
||
@Override | ||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { | ||
resolvers.add(loginMemberArgumentResolver); | ||
} | ||
|
||
@Override | ||
public void addInterceptors(InterceptorRegistry registry) { | ||
registry.addInterceptor(authAdminRoleInterceptor) | ||
.addPathPatterns("/admin/**"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package roomescape.authentication; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.core.MethodParameter; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.bind.support.WebDataBinderFactory; | ||
import org.springframework.web.context.request.NativeWebRequest; | ||
import org.springframework.web.context.request.ServletWebRequest; | ||
import org.springframework.web.method.support.HandlerMethodArgumentResolver; | ||
import org.springframework.web.method.support.ModelAndViewContainer; | ||
import roomescape.authentication.jwt.JwtResponse; | ||
import roomescape.authentication.jwt.JwtUtils; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { | ||
|
||
private final JwtUtils jwtUtils; | ||
|
||
@Override | ||
public boolean supportsParameter(MethodParameter parameter) { | ||
return parameter.getParameterType().equals(MemberAuthInfo.class); | ||
} | ||
|
||
@Override | ||
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { | ||
HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest(); | ||
|
||
JwtResponse jwtResponse = jwtUtils.extractTokenFromCookie(request.getCookies()); | ||
if (jwtResponse.accessToken() == null) { | ||
return null; | ||
} | ||
|
||
return jwtUtils.extractMemberAuthInfoFromToken(jwtResponse.accessToken()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +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,6 @@ | ||
package roomescape.authentication.jwt; | ||
|
||
public record JwtResponse( | ||
String accessToken | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package roomescape.authentication.jwt; | ||
|
||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.JwtException; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.security.Keys; | ||
import jakarta.servlet.http.Cookie; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
import roomescape.authentication.MemberAuthInfo; | ||
import roomescape.exception.JwtProviderException; | ||
import roomescape.exception.JwtValidationException; | ||
import roomescape.member.Member; | ||
|
||
import java.util.Arrays; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class JwtUtils { | ||
|
||
@Value("${roomescape.auth.jwt.secret}") | ||
private String secretKey; | ||
|
||
public JwtResponse createAccessToken(Member member) { | ||
try { | ||
String accessToken = Jwts.builder() | ||
.setSubject(member.getId().toString()) | ||
.claim("name", member.getName()) | ||
.claim("role", member.getRole()) | ||
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) | ||
.compact(); | ||
|
||
return new JwtResponse(accessToken); | ||
} catch (JwtException e) { | ||
throw new JwtProviderException("JWT 생성에 실패하였습니다.", e); | ||
} | ||
} | ||
|
||
public MemberAuthInfo extractMemberAuthInfoFromToken(String token) { | ||
if (token == null || token.isEmpty()) { | ||
throw new JwtValidationException("토큰이 존재하지 않습니다."); | ||
} | ||
|
||
try { | ||
Claims claims = Jwts.parserBuilder() | ||
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) | ||
.build() | ||
.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(id, name, role); | ||
} catch (JwtException e) { | ||
throw new JwtValidationException("유효하지 않은 JWT 토큰입니다.", e); | ||
} | ||
} | ||
|
||
public JwtResponse extractTokenFromCookie(Cookie[] cookies) { | ||
if (cookies == null) { | ||
throw new JwtValidationException("쿠키가 존재하지 않습니다."); | ||
} | ||
|
||
try { | ||
String accessToken = Arrays.stream(cookies) | ||
.filter(cookie -> cookie.getName().equals("token")) | ||
.map(Cookie::getValue) | ||
.findFirst() | ||
.orElseThrow(() -> new JwtValidationException("토큰이 존재하지 않습니다.")); | ||
|
||
return new JwtResponse(accessToken); | ||
} catch (Exception e) { | ||
throw new JwtValidationException("쿠키에서 토큰 추출 중 오류가 발생했습니다.", e); | ||
} | ||
} | ||
} |
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,46 @@ | ||
package roomescape.exception; | ||
|
||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.ExceptionHandler; | ||
import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
|
||
@RestControllerAdvice | ||
public class GeneralExceptionHandler { | ||
|
||
@ExceptionHandler(DuplicateReservationException.class) | ||
public ResponseEntity<String> handleDuplicatedReservation(DuplicateReservationException e) { | ||
return ResponseEntity.status(HttpStatus.CONFLICT).body(e.getMessage()); | ||
} | ||
|
||
@ExceptionHandler(TimeNotFoundException.class) | ||
public ResponseEntity<String> handleTimeNotFound(TimeNotFoundException e) { | ||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); | ||
} | ||
|
||
@ExceptionHandler(ThemeNotFoundException.class) | ||
public ResponseEntity<String> handleThemeNotFound(ThemeNotFoundException e) { | ||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); | ||
} | ||
|
||
@ExceptionHandler(MemberNotFoundException.class) | ||
public ResponseEntity<String> handleMemberNotFound(MemberNotFoundException e) { | ||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); | ||
} | ||
|
||
@ExceptionHandler(JwtValidationException.class) | ||
public ResponseEntity<String> handleJwtValidationException(JwtValidationException e) { | ||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); | ||
} | ||
|
||
@ExceptionHandler(JwtProviderException.class) | ||
public ResponseEntity<String> handleJwtProviderException(JwtProviderException e) { | ||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); | ||
} | ||
|
||
@ExceptionHandler(Exception.class) | ||
public ResponseEntity<String> handleGeneralException(Exception e) { | ||
e.printStackTrace(); | ||
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. e.printStackTrace 함수는 성능상으로 되게 안좋은 함수인데요 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. 언급해주신 참고 자료 잘 읽어보았습니다. <e.printStackTrace() 사용을 지양해야 하는 이유>
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. e.printStackTrace 말고 로깅을 하는 다른 방법은 여러 가지가 있는데, 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. |
||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); | ||
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. 아하 그렇군요! 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. Q1 > 서버에 어떤 라이브러리를 사용하고 있는지와 같은 정보들이 전달되는 것 자체가 문제가 될 수 있어요! |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package roomescape.exception; | ||
|
||
public class JwtProviderException extends RuntimeException { | ||
public JwtProviderException(String message, Throwable cause) { | ||
super(message, cause); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package roomescape.exception; | ||
|
||
public class JwtValidationException extends RuntimeException { | ||
public JwtValidationException(String message) { | ||
super(message); | ||
} | ||
|
||
public JwtValidationException(String message, Throwable cause) { | ||
super(message, cause); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package roomescape.exception; | ||
|
||
public class MemberNotFoundException extends RuntimeException { | ||
|
||
public MemberNotFoundException(String message) { | ||
super(message); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package roomescape.exception; | ||
|
||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.web.bind.annotation.ControllerAdvice; | ||
import org.springframework.web.bind.annotation.ExceptionHandler; | ||
import roomescape.PageController; | ||
|
||
@Slf4j | ||
@ControllerAdvice(assignableTypes = PageController.class) | ||
public class PageExceptionHandler { | ||
@ExceptionHandler(Exception.class) | ||
public String handleException(Exception e) { | ||
log.error("error: " + e.getMessage()); | ||
return "error/500"; //view 렌더링 페이지는 만들지 않음 | ||
} | ||
} | ||
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. 우선, GeneralExceptionHandler와 PageExceptionHandler를 나눈 이유는 예외처리를 할 때 상황에 따라 응답을 다르게 해주기 위해서입니다. 현재 구현되어 있는 컨트롤러들은 역할에 따라 크게 두가지로 나눌 수 있을 것 같아요.
같은 예외라 하더라도, 1번의 컨트롤러의 경우, 뷰 렌더링 과정에서 문제가 생기면 원래 받아야할 HTML 응답과 비슷한 형태의 예외 응답을 주는 것이 적절할 것이라고 판단하였습니다! 위 코드처럼 예외 페이지를 응답하거나 리다이렉트를 하는 방향으로요! 2번의 컨트롤러의 경우 상태 코드나, 오류 메세지 등을 전달해주는 방식으로 예외 응답을 줘서 프론트 측에서 예외 응답에 따라 적절한 조치를 취할 수 있도록 하는 것이죠. Q. 누누님은 이런 방식에 대해 어떻게 생각하시나요? 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. 솔직히 말씀드려서 말씀해주신 2가지가 같이 케이스는 없었던 것 같아요 1번은 정확하게 모르겠어요 저도 실무에서는 전혀 볼 일이 없는 코드다보니...? 2번의 경우에는 실제로 많이 쓰는데요
같은 형태이려나요? |
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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package roomescape.login; | ||
|
||
public record LoginCheckResponse(String name) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package roomescape.login; | ||
|
||
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import jakarta.validation.Valid; | ||
import lombok.RequiredArgsConstructor; | ||
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.authentication.MemberAuthInfo; | ||
import roomescape.authentication.jwt.JwtResponse; | ||
import roomescape.authentication.jwt.JwtUtils; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
public class LoginController { | ||
|
||
private final LoginService loginService; | ||
private final JwtUtils jwtUtils; | ||
|
||
@PostMapping("/login") | ||
public void login(@Valid @RequestBody LoginRequest loginRequest, HttpServletResponse response) { | ||
JwtResponse jwtResponse = loginService.login(loginRequest); | ||
|
||
Cookie cookie = new Cookie("token", jwtResponse.accessToken()); | ||
cookie.setHttpOnly(true); | ||
cookie.setPath("/"); | ||
response.addCookie(cookie); | ||
} | ||
|
||
@GetMapping("/login/check") | ||
public LoginCheckResponse checkLogin(HttpServletRequest request) { | ||
JwtResponse jwtResponse = jwtUtils.extractTokenFromCookie(request.getCookies()); | ||
MemberAuthInfo memberAuthInfo = jwtUtils.extractMemberAuthInfoFromToken(jwtResponse.accessToken()); | ||
LoginCheckResponse loginCheckResponse = loginService.checkLogin(memberAuthInfo); | ||
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. 제가 누누님이 하신 말씀이 정확히 이해했는지는 잘 모르겠는데, 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. 넵 맞아요 |
||
|
||
return loginCheckResponse; | ||
} | ||
|
||
@PostMapping("/logout") | ||
public void logout(HttpServletResponse response) { | ||
Cookie cookie = new Cookie("token", ""); | ||
cookie.setHttpOnly(true); | ||
cookie.setPath("/"); | ||
cookie.setMaxAge(0); | ||
response.addCookie(cookie); | ||
} | ||
} |
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.
이렇게 로직이 같은 것들은
같은 느낌으로 통일해보면 어떨까요?
중복을 줄일 수 있을 것 같아요
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.
네 좋습니다! 훨씬 깔끔하고 좋은 방식이네요.
잦은 코드 중복에 유의해야겠어요!