-
Notifications
You must be signed in to change notification settings - Fork 78
[그리디] 김지우 Spring Core (배포) 7,8,9 단계 제출합니다. #178
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: ji-woo-kim
Are you sure you want to change the base?
Changes from all commits
eaef783
d74edeb
6c7e152
f1bd8d1
0338938
a7a70b0
e573b78
edb44c0
716da1f
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,16 @@ | ||
# 🔓 방탈출 예약 서비스 | ||
|
||
<br> | ||
<img width="800" height="413" alt="image" src="https://github.com/user-attachments/assets/9ce9fd16-4341-46d0-9f62-750021db4dd6" /> | ||
<img width="800" height="248" alt="image" src="https://github.com/user-attachments/assets/46d1c290-90ad-450c-8a0b-d05cb32ae7af" /> | ||
<br> | ||
|
||
## 🚀 주요 기능 | ||
|
||
1. **로그인**: 가입한 이메일과 비밀번호로 로그인할 수 있습니다. | ||
|
||
2. **예약**: '예약' 페이지에서 원하는 날짜, 테마, 시간을 선택하고 '예약하기' 버튼을 누르면 예약이 완료됩니다. | ||
|
||
3. **예약 확인 및 취소**: '내 예약' 페이지에서 예약 내역을 확인하거나, 원치 않는 예약을 취소할 수 있습니다. | ||
|
||
4. **예약 대기 신청**: 예약 대기를 신청하면, 취소 자리가 생겼을 때 우선적으로 예약할 기회를 드립니다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package auth; | ||
|
||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.Jws; | ||
import io.jsonwebtoken.JwtException; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.SignatureAlgorithm; | ||
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
|
||
import javax.crypto.spec.SecretKeySpec; | ||
import java.security.Key; | ||
import java.util.Arrays; | ||
import java.util.Date; | ||
|
||
public class JwtUtils { | ||
|
||
private final Key key; | ||
|
||
public JwtUtils(String secretKey) { | ||
this.key = new SecretKeySpec(secretKey.getBytes(), SignatureAlgorithm.HS256.getJcaName()); | ||
} | ||
|
||
public String createToken(String id, String role) { | ||
Claims claims = Jwts.claims().setSubject(id); | ||
claims.put("role", role); | ||
Date now = new Date(); | ||
Date validity = new Date(now.getTime() + 3600000); | ||
|
||
return Jwts.builder() | ||
.setClaims(claims) | ||
.setIssuedAt(now) | ||
.setExpiration(validity) | ||
.signWith(key, SignatureAlgorithm.HS256) | ||
.compact(); | ||
} | ||
|
||
private Claims getClaims(String token) { | ||
try { | ||
Jws<Claims> claims = Jwts.parserBuilder() | ||
.setSigningKey(key) | ||
.build() | ||
.parseClaimsJws(token); | ||
return claims.getBody(); | ||
} catch (JwtException | IllegalArgumentException e) { | ||
throw new IllegalArgumentException("유효하지 않은 토큰입니다."); | ||
} | ||
} | ||
|
||
public String getSubject(String token) { | ||
return getClaims(token).getSubject(); | ||
} | ||
|
||
public String getRole(String token) { | ||
return getClaims(token).get("role", String.class); | ||
} | ||
|
||
public String getTokenFromCookie(HttpServletRequest request) { | ||
Cookie[] cookies = request.getCookies(); | ||
if (cookies == null) { | ||
return null; | ||
} | ||
return Arrays.stream(cookies) | ||
.filter(cookie -> "token".equals(cookie.getName())) | ||
.findFirst() | ||
.map(Cookie::getValue) | ||
.orElse(null); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package auth.annotation; | ||
|
||
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 Login { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,33 @@ | ||
package roomescape; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.ControllerAdvice; | ||
import org.springframework.web.bind.annotation.ExceptionHandler; | ||
import roomescape.auth.exception.UnauthenticatedException; | ||
|
||
@ControllerAdvice | ||
public class ExceptionController { | ||
|
||
private static final Logger log = LoggerFactory.getLogger(ExceptionController.class); | ||
|
||
@ExceptionHandler(Exception.class) | ||
public ResponseEntity<Void> handleRuntimeException(Exception e) { | ||
e.printStackTrace(); | ||
return ResponseEntity.badRequest().build(); | ||
public ResponseEntity<String> handleRuntimeException(Exception e) { | ||
log.error("예상치 못한 예외가 발생했습니다.", e); | ||
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 ResponseEntity.internalServerError().body("서버 내부 오류가 발생했습니다."); | ||
} | ||
|
||
@ExceptionHandler(UnauthenticatedException.class) | ||
public ResponseEntity<String> handleUnauthenticatedException(UnauthenticatedException e) { | ||
log.error("인증되지 않은 요청입니다.", e); | ||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); | ||
} | ||
|
||
@ExceptionHandler(IllegalArgumentException.class) | ||
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) { | ||
log.error("잘못된 요청: {}", e.getMessage(), e); | ||
return ResponseEntity.badRequest().body(e.getMessage()); | ||
} | ||
} |
This file was deleted.
This file was deleted.
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.
JwtUtils에 Component를 제거한 이유가 있나요?
auth라는 roomspace와 동등한 레벨의 패키지로 분리한 이유도 궁금합니다!
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.
이런 방식으로 구현한 이유는 우선 미션 요구사항과 주어진 테스트를 통과시키기 위해서입니다.
주어진 테스트코드에서 JwtUtils 클래스가
@Component
어노테이션을 가지고 있지 않아야 통과하도록 짜여져 있었습니다. 또한, 미션 요구사항에 따라 roomescape와 동등한 레벨의 패키지를 생성하였고@Configuration
과@Bean
사용에 대한 해결 방향성을 참고하여 AuthConfig를 통해 secretKey를 주입받아 JwtUtils Bean을 생성하는 방식으로 구현했습니다.단순히 미션의 요구사항만을 충족하고 넘어가기엔 아쉬운 것 같아
왜 이렇게 구현을 해야하는지, 이 미션을 통해 무엇을 학습해봐야 하는지 생각해보았는데요!
자동으로 빈을 등록하는 방식(@component 사용)과 수동으로 등록하는 방식의 차이를 경험해보고
서비스 로직과 인증 로직을 분리하는 설계를 학습해보라는 의도가 있는 것이 아닌가 싶습니다.
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.
추가적인 고민까지 좋습니다!
jwtUtils는 어떤 애플리케이션에서 사용 할 수 있는 코드, 일종의 라이브러리로 간주 하였다면
AuthConfig.java
는 roomesacpe에 포함 되어야 할 것 같아요.roomescape
에서jwtUtils
를 빈으로 등록하는 방식으로요!실제로 외부 라이브러리를 빈에 등록 시킬 때,
@Bean
을 자주 활용 합니다.