-
Notifications
You must be signed in to change notification settings - Fork 10
[22기_배승식] Spring Security & JWT 미션 제출합니다. #19
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: ba2slk
Are you sure you want to change the base?
Conversation
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.
안녕하세요. MT에 참여했었던 이호연입니다.
가볍게 PR을 해보았는데요. 참고삼아 봐주세요 ㅎㅎ.
앞에 p 붙은건 아래와 같은 룰로 작성해 보았습니다.
P1: 꼭 반영해주세요 (Request changes)
P2: 적극적으로 고려해주세요 (Request changes)
P3: 웬만하면 반영해 주세요 (Comment)
P4: 반영해도 좋고 넘어가도 좋습니다 (Approve)
P5: 그냥 사소한 의견입니다 (Approve)
- iat: | ||
3. Verify Signature: Payload가 위변조되지 않았다는 사실을 증명하는 문자열 | ||
- Base64 기반 인코딩 헤더, Payload, 임의의 Secret Key 조합 | ||
- SECRET_KEY를 알아야 복호화할 수 있음 |
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.
p2. 보통은 복호화 하지 못할거에요
1. 만료 시간 전까지 해당 토큰이 유효하다는 점 | ||
2. 발급된 토큰을 수정할 수 없다는 점 | ||
3. 이로 인해 공격자에 의해 탈취된 토큰에 대한 대처가 어렵다는 점 | ||
과 같은 단점이 있다. 이를 극복하기 위해 Refresh Token 사용 |
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.
p4. Refresh Token의 형식은? Refresh Token이 탈취 당하면 어떻게 되나요?
private final int status; | ||
private final String divisionCode; |
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.
p3. 이 둘을 구분한 이유가 어떻게 되나요?
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.
도메인별 맥락이나 예외의 세부 내용을 내부적으로 더 구체화하기 위해 divisionCode를 두어 실제 HTTP 상태 코드를 나타내는 status와 구분했습니다!
|
||
// 회원 가입 | ||
@PostMapping("/signup") | ||
public ResponseEntity<String> signup(@RequestBody SignUpRequestDTO signUpRequestDTO) { |
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.
p3. Response은 언제나 JsonObject 포멧으로 내려주는게 가장 좋습니다.
- 클라 측에서 모든 API 호출에 동일한 처리가 가능해집니다.
- 추후 어떠한 변동성에 유연하게 대응하기 위해서 입니다. (확장성) 예를들어 회원가입과 동시에 로그인이 되도록 스팩이 변경된다면, 이 Api에서도 AccessToken과 RefreshToken을 반환해줘야겠죠
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.
일관성을 고려하지 못했던 것 같습니다.. response를 클라이언트가 예측 가능한 형태로 구성하는 데 집중하겠습니다! 감사합니다.
@Value("${spring.jwt.token.access_expiration}") | ||
private long accessExpiration; | ||
|
||
@Value("${spring.jwt.token.refresh_expiration}") | ||
private long refreshExpiration; |
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.
p2. Duration 이라는 클래스를 사용하면 좀 더 가독성 있게 작성 할 수 있습니다.
yml 에서도 Duration을 지원해줍니다
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.
바로 적용해보겠습니다!!
|
||
@PostMapping("/api/reservation") | ||
public ResponseEntity<ReservationResponseDTO> createReservation(@RequestBody ReservationRequestDTO reservationRequestDTO, | ||
@AuthenticationPrincipal org.springframework.security.core.userdetails.User user) { |
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.
p4.
@AuthenticationPrincipal org.springframework.security.core.userdetails.User user) { | |
@AuthenticationPrincipal User user) { |
여기는 User Entity를 안쓰고 있어서 생략이 가능해보이네요.
단, 이러한 이슈가 계속 발생하고 클래스 명이 같아 가독성도 떨어질 것 같아요
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.
알려주셔서 감사합니다. 정돈해보겠습니다!
User user = userRepository.findByEmail(email) | ||
.orElseThrow(() -> new IllegalArgumentException("User not found: " + email)); | ||
|
||
Showtime showtime = showtimeRepository.findById(reservationRequestDTO.showtimeId()) | ||
.orElseThrow(() -> new IllegalArgumentException("Showtime not found: " + reservationRequestDTO.showtimeId())); |
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.
p3. 에러 사유를 상세하게 작성해주시는 건 좋으나,
이러한 메세지 노출(응답)은 개발 환경에서만 해야합니다.
악의적 사용자가 여러 email로 시도를 하면 주로 User not found 가 발생하나,
우연히 존재하는 이메일을 입력하면 Showtime not found 메세지가 내려가게 되면서
해당 email을 가진 유저가 존재한단 사실을 알게 되어버립니다.
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.
개발 편의만 고려하다 보니 취약점을 놓쳤던 것 같습니다. GlobalExceptionHandler를 더 개발해서 내부 상세 로그와 외부 응답을 분리하고 민감 정보 노출도 개발 과정에서 깊이 고민해 보겠습니다!
|
||
/* 예매 */ | ||
@Transactional | ||
public ReservationResponseDTO createReservation(ReservationRequestDTO reservationRequestDTO, String email) { // TODO: Spring Security 기반 User 정보 획득 |
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.
p3. 유저 식별 단위를 email 보다는 PK 로 하는게 좋지 않을까요?
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.
유저가 email과 password를 기반으로 로그인하기 때문에 해당 일차원적으로 email을 기준으로 유저를 식별하려고 했던 것 같습니다. PK를 사용하도록 수정하겠습니다!
List<Inventory> inventories = inventoryRepository.findAllByTheaterId(theaterId).stream() | ||
.filter(Inventory::getIsAvailable) |
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.
p4. 어플리케이션 서버단 보다는 DB 단에서 필터링을 해서 통신하는건 어떨까요?
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.
말씀해주신 것처럼 지금 방식은 불필요한 데이터까지 서버로 가져와서 필터링하는 오버헤드가 있는 것 같습니다. Repository 쪽을 수정해서 애초에 쿼리 되는 데이터 양을 줄여보겠습니다! 감사합니다.
import java.util.Optional; | ||
|
||
public interface UserRepository extends JpaRepository<User, Long> { | ||
Optional<User> findByName(String name); |
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.
p2. 동명이인에 대해선 고려하지 않는 스팩일까요?
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.
개발 당시에 고려하지 못한 것 같습니다.. PK나 name + 다른 key 조합으로 찾을 수 있도록 개선하겠습니다. 알려주셔서 감사합니다!
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.
3주차 과제도 수고많으셨습니다!!
구현할 기능이 많았는데도 꼼꼼하게 잘 구현해주셨네요 😊
앞으로도 파이팅입니다~!!
@Transactional | ||
public JwtTokenDTO login(LoginRequestDTO loginRequestDTO) { | ||
Authentication authToken = new UsernamePasswordAuthenticationToken( | ||
loginRequestDTO.email(), | ||
loginRequestDTO.password() | ||
); | ||
|
||
Authentication authentication = authenticationManager.authenticate(authToken); | ||
|
||
org.springframework.security.core.userdetails.User principal = | ||
(org.springframework.security.core.userdetails.User) authentication.getPrincipal(); | ||
|
||
|
||
String accessToken = tokenProvider.createAccessToken( | ||
principal.getUsername(), | ||
authentication | ||
); | ||
|
||
return new JwtTokenDTO("Bearer", accessToken, null); | ||
} |
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.
LoginFilter를 만들어서 처리해보는 방식도 있을거 같아요!
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.
아무래도 수동으로 로그인 로직을 처리한 것 같아서 불안했는데, LoginFilter로 바꿔보겠습니다!
/* 영화 찜하기 */ | ||
@PostMapping("/api/movie/{movieId}/like") | ||
public ResponseEntity<Void> likeMovie(@PathVariable Long movieId, | ||
@RequestParam Long userId){ // TODO: Spring Security 기반 User 정보 획득 |
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.
그런 방법도 있었네요... 한 번 적용해보겠습니다. 감사합니다!!
public static MovieListDTO from(Movie movie) { | ||
return new MovieListDTO( | ||
movie.getId(), | ||
movie.getTitle(), | ||
movie.getDuration(), | ||
movie.getDirector(), | ||
movie.getRating(), | ||
movie.getStar() | ||
); |
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.
서비스 규모가 커지면 엔티티와 DTO 간의 변환 로직을 다 작성하는게 어려울수도 있으니
MapStruct처럼 변환 코드를 자동으로 생성해주는 라이브러리를 사용하는 방법도 알아두면 좋을거 같습니다!!
참고 자료: https://marklee1117.tistory.com/121#MapStruct%20%EC%82%AC%EC%9A%A9%EB%B2%95-1
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.
저도 엔티티 DTO 변환 로직 이렇게 적고 있었는데.. 저도 배워갑니다 감사합니다 😂
|
||
@CreationTimestamp | ||
private LocalDateTime createdAt; | ||
} |
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.
시스템 시간 측정 기능을 BaseEntity에 위임시켜버릴 수 도 있군요
저도 적용시켜봐야겠네요 배워갑니다!
@Enumerated(EnumType.STRING) | ||
@Builder.Default | ||
private GenreType type = GenreType.UNKNOWN; | ||
} |
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.
전 엔티티마다 다 기본 생성자로 대입했었는데.. 빌드 패턴이 확실히 더 편해보이네요..
빌드 패턴으로 값 초기화할 때는 이렇게 하면 되군요. 배워갑니다!!
@RequestParam(defaultValue = "10") int size, | ||
@RequestParam(defaultValue = "releaseDate") String sortBy | ||
){ | ||
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy).descending()); |
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.
저도 그냥 페이지처리 했었는데, 커서 기반 페이지네이션.. 저도 배워갑니다...! 감사합니다
public static MovieListDTO from(Movie movie) { | ||
return new MovieListDTO( | ||
movie.getId(), | ||
movie.getTitle(), | ||
movie.getDuration(), | ||
movie.getDirector(), | ||
movie.getRating(), | ||
movie.getStar() | ||
); |
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.
저도 엔티티 DTO 변환 로직 이렇게 적고 있었는데.. 저도 배워갑니다 감사합니다 😂
Optional<Theater> Theater = theaterRepository.findById(theaterId); | ||
return Theater.map(TheaterDetailsDTO::from).orElseThrow(); | ||
} | ||
} |
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.
클래스 단에 @transactional(readOnly=true) 달아놔도 되지 않을까욤..?
제가 알기론 readOnly=false로 해놓고 싶은 메서드가 따로 있으면 해당 메서드에 다시 @transactional 만 달아도 오버라이드 된다고 하더라고요..
혹시 메서드 별로 달아놓으신 이유가 있으신가요 .. 잘 몰라서 여쭤봅니당...
movieLikeService.unlikeMovie(userId, movieId); | ||
return ResponseEntity.ok().build(); | ||
} | ||
} |
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.
cgv 보면 하트모양 토글 버튼을 눌렀다 떼는 걸로 찜 등록/취소를 하더라구요..
서비스 단에서 boolean 값으로 찜 상태 확인받아주면, 찜 등록/취소 api를 하나로 합쳐볼 수도 있을 것 같아요
} | ||
this.quantity = quantity; | ||
} | ||
} |
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.
재고 수량을 Item 엔티티에서 같이 관리해볼 수 있지 않을까 싶습니다..!
혹시 재고 수량을 Item 엔티티와 나눠 관리하는 이유가 있나요..?
override-with-generic-response: false | ||
``` | ||
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.
과제 수고하셨습니다 :)
/* 영화 찜하기 */ | ||
@PostMapping("/api/movie/{movieId}/like") | ||
public ResponseEntity<Void> likeMovie(@PathVariable Long movieId, | ||
@RequestParam Long userId){ // TODO: Spring Security 기반 User 정보 획득 |
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.
사용자 ID를 파라미터로 사용하는 것 보다는 @AuthenticationPrincipal을 사용하는 것이 안전하지 않을까요?
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class JwtTokenBlacklist { |
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.
어떤 기준으로 블랙리스트에 등록되고 삭제되는지 궁금합니다 !
@PutMapping("/api/reservation") | ||
public ResponseEntity<ReservationResponseDTO> cancelReservation(@RequestBody ReservationCancelDTO reservationCancelDTO, | ||
@AuthenticationPrincipal org.springframework.security.core.userdetails.User user) { | ||
ReservationResponseDTO reservation = reservationService.cancelReservation(reservationCancelDTO, user.getUsername()); | ||
return ResponseEntity.ok(reservation); | ||
} |
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.
예매 상태만 변경하는 거라면 PATCH가 적합하지 않을까요 ?
3주차 미션 제출합니다.