-
Notifications
You must be signed in to change notification settings - Fork 80
[그리디] 강동현 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 all 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 |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-17.0.17.10-hotspot | ||
|
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. 이거는 동현님만 사용 가능한 경로이기 때문에 요 파일 삭제해도 제대로 구동 가능하게 해주세요~ |
||
|
|
||
|
|
||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package auth; | ||
|
|
||
| import io.jsonwebtoken.Claims; | ||
| import io.jsonwebtoken.Jwts; | ||
| import io.jsonwebtoken.security.Keys; | ||
| import jakarta.servlet.http.Cookie; | ||
|
|
||
| import java.util.Date; | ||
|
|
||
| public final class JwtUtils { | ||
| public static final int DEFAULT_MAX_AGE_SECONDS = 10 * 60; // 10 minutes | ||
|
|
||
| private final String secretKey; | ||
|
|
||
| public JwtUtils(String secretKey) { | ||
| this.secretKey = secretKey; | ||
| } | ||
|
|
||
| public String extractTokenFromCookies(Cookie[] cookies) { | ||
| if (cookies == null || cookies.length == 0) { | ||
| return ""; | ||
| } | ||
| for (Cookie cookie : cookies) { | ||
| if ("token".equals(cookie.getName())) { | ||
| return cookie.getValue(); | ||
| } | ||
| } | ||
| return ""; | ||
| } | ||
|
|
||
| public Claims parseClaims(String token) { | ||
| return Jwts.parserBuilder() | ||
| .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) | ||
| .build() | ||
| .parseClaimsJws(token) | ||
| .getBody(); | ||
| } | ||
|
|
||
| public String createToken(String subject, String name, String role) { | ||
| Date now = new Date(); | ||
| Date expiresAt = new Date(now.getTime() + (long) DEFAULT_MAX_AGE_SECONDS * 1000); | ||
| return Jwts.builder() | ||
| .setSubject(subject) | ||
| .setExpiration(expiresAt) | ||
| .claim("name", name) | ||
| .claim("role", role) | ||
| .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) | ||
| .compact(); | ||
| } | ||
| } | ||
|
|
||
|
|
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package roomescape; | ||
|
|
||
| import auth.JwtUtils; | ||
| 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 { | ||
| private final JwtUtils jwtUtils; | ||
|
|
||
| public WebConfig(JwtUtils jwtUtils) { | ||
| this.jwtUtils = jwtUtils; | ||
| } | ||
|
|
||
| @Override | ||
| public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { | ||
| resolvers.add(new LoginMemberArgumentResolver(jwtUtils)); | ||
| } | ||
|
|
||
| @Override | ||
| public void addInterceptors(InterceptorRegistry registry) { | ||
| registry.addInterceptor(new AdminAuthInterceptor(jwtUtils)) | ||
| .addPathPatterns("/admin", "/admin/**"); | ||
| } | ||
| } | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package roomescape.auth; | ||
|
|
||
| import auth.JwtUtils; | ||
| 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 java.io.IOException; | ||
|
|
||
| public class AdminAuthInterceptor implements HandlerInterceptor { | ||
|
|
||
| private final JwtUtils jwtUtils; | ||
|
|
||
| public AdminAuthInterceptor(JwtUtils jwtUtils) { | ||
| this.jwtUtils = jwtUtils; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { | ||
| String token = jwtUtils.extractTokenFromCookies(request.getCookies()); | ||
| if (token.isEmpty()) { | ||
| writeError(response, ApiError.UNAUTHORIZED_MISSING_TOKEN); | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| Claims claims = jwtUtils.parseClaims(token); | ||
| 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,55 @@ | ||
| package roomescape.auth; | ||
|
|
||
| import auth.JwtUtils; | ||
| 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; | ||
|
|
||
| public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { | ||
|
|
||
| private final JwtUtils jwtUtils; | ||
|
|
||
| public LoginMemberArgumentResolver(JwtUtils jwtUtils) { | ||
| this.jwtUtils = jwtUtils; | ||
| } | ||
|
|
||
| @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 = jwtUtils.extractTokenFromCookies(request.getCookies()); | ||
|
|
||
| if (token.isEmpty()) { | ||
| return null; | ||
| } | ||
|
|
||
| try { | ||
| Claims claims = jwtUtils.parseClaims(token); | ||
|
|
||
| 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,26 @@ | ||
| 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, "서버 오류가 발생했습니다."); | ||
|
|
||
| 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,50 @@ | ||
| package roomescape.common; | ||
|
|
||
| import jakarta.persistence.EntityNotFoundException; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.http.converter.HttpMessageNotReadableException; | ||
| import org.springframework.web.bind.MethodArgumentNotValidException; | ||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import java.util.NoSuchElementException; | ||
|
|
||
| @RestControllerAdvice | ||
| public class ExceptionController { | ||
| @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,17 @@ | ||
| package roomescape.config; | ||
|
|
||
| import auth.JwtUtils; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
|
|
||
| @Configuration | ||
| public class AuthConfig { | ||
|
|
||
| @Bean | ||
| public JwtUtils jwtUtils(@Value("${roomescape.auth.jwt.secret}") String secretKey) { | ||
| return new JwtUtils(secretKey); | ||
| } | ||
| } | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package roomescape.member; | ||
|
|
||
| public record LoginMemberDto(Long id, String name, String email, String role) { | ||
| } | ||
|
|
||
|
|
||
|
|
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.
오잉 요게 삭제된 이유가 있나요??
gradle.properties,/node_modules/yarn-integrity는 추가됐는데 요거는 왜 추가된걸까요?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.
제 컴퓨터에서 자바 저번이 맞지 않아 빌드가 안되는 상황이 발생해 이것저것 시도해보다가 생긴 결과물입니다ㅠㅠ
sourceCompatibility 부분과 gradle.properties부분에 충돌이 일어나는 상황이라고 판단해 지웠었습니다만, 결론적으로는 로컬 경로 문제였기 때문에 삭제할 이유는 없었습니다.
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.
프로젝트의 자바 문법 고정을 위해 sourceCompatibility를 다시 추가하겠습니다!