Skip to content
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

[✨6주차 실습과제 제출✨] #10

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open

[✨6주차 실습과제 제출✨] #10

wants to merge 12 commits into from

Conversation

JungYoonShin
Copy link
Member

@JungYoonShin JungYoonShin commented Jun 2, 2024

1️⃣ 로그인시 ATK와 RTK 함께 반환하는 API(/api/v1/members)
따로 로그인 API를 만들지는 않았고, 저번 세미나에서 이어서 회원가입시에 ATK와 RTK를 함께 반환하도록 했습니다!

issueTokenAndStoreRefreshToken라는 메서드를 따로 만들어서, jwtTokenProvider에서 한꺼번에 ATK와 RTK를 생성하고 TokenInfo dto를 받아온 후, RTK를 생성한후 저장하도록 했습니다.

@PostMapping("/members")
public ResponseEntity<UserJoinResponse> postMember(
@RequestBody @Valid MemberCreateRequest memberCreate
) {
UserJoinResponse userJoinResponse = memberService.createMember(memberCreate);
return ResponseEntity.status(HttpStatus.CREATED)
.body(
userJoinResponse
);
}

@Transactional
public UserJoinResponse createMember(
MemberCreateRequest memberCreate
) {
Member member = memberRepository.save(
Member.create(
memberCreate.name(), memberCreate.part(),
memberCreate.age(), memberCreate.email(), memberCreate.password()
)
);
UserAuthentication userAuthentication = UserAuthentication.createUserAuthentication(memberCreate.email());
TokenInfo token = issueTokenAndStoreRefreshToken(userAuthentication, member.getId());
return UserJoinResponse.of(token.accessToken(), token.refreshToken());
}

private TokenInfo issueTokenAndStoreRefreshToken(Authentication authentication, Long userId) {
TokenInfo issuedToken = jwtTokenProvider.issueToken(authentication);
RefreshToken refreshToken = RefreshToken.builder()
.id(userId)
.refreshToken(issuedToken.refreshToken())
.build();
refreshTokenRepository.save(refreshToken);
return issuedToken;
}
}


2️⃣ ATK 재발급 API(/api/v1/members/reissue)
우선 RTK rotation 방식을 적용해보았습니다! JWT가 가진 여러 이점이 있지만, 토큰이 탈취되었을 경우에 생기는 큰 취약점을 보완하고자 해당 방식을 적용해보았는데요, 로직은 아래와 같이 구성했습니다.

@GetMapping("/members/reissue")
public ResponseEntity<TokenInfo> reissue(@RequestHeader String refreshToken) {
return ResponseEntity.ok(memberService.reissue(refreshToken));
}

public TokenInfo reissue(String refreshToken) {
jwtTokenProvider.validateToken(refreshToken);
String userEmail = jwtTokenProvider.getUserFromJwt(refreshToken);
System.out.println("userEmail = " + userEmail);
Member member = findMember(userEmail);
//리프레시 토큰 탈취여부 확인(탈취범이 정상 유저보다 먼저 재발급 받았을 경우 -> 재로그인 유도)
jwtTokenProvider.matchRefreshToken(refreshToken, findRefreshToken(member.getId()).getRefreshToken());
UserAuthentication userAuthentication = UserAuthentication.createUserAuthentication(userEmail);
TokenInfo token = issueTokenAndStoreRefreshToken(userAuthentication, member.getId());
return token;
}

public void matchRefreshToken(String refreshToken, String storedRefreshToken) {
if (!refreshToken.equals(storedRefreshToken)) {
throw new UnauthorizedException(ErrorMessage.MISMATCH_REFRESH_TOKEN);
}
}

  1. request header로 넘어온 RTK 유효성 검사(만료여부 등)
  2. 리프레신 토큰 탈취여부 확인(저장되어있는 RTK와 넘어온 RTK가 다를 경우, 탈취되었다고 판단후 에러처리를 통해 재로그인 유도)
  3. 탈취여부 확인 후 통과 시에는 ATK, RTK 모두 재발급 후 반환

Refresh token rotaion 참고 링크


많은 피드백 해주시면 대환영입니다🙇‍♀️
files changed에 다른 주차 과제도 같이 올라와버린 이슈가....죄송합니다아...🥲

Copy link

@junggyo1020 junggyo1020 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드를 잘 읽어보았습니다..! PR에 wrapper파일까지 모두 올라가 있어 .gitignore에 추가하여 필요한 부분만 코드를 올리면 더 좋을 것 같다는 생각이 들었어요!!! LoginFilter를 따로 구현하여 로그인 부분을 처리하신 부분이 인상깊었습니다..!

과제하시느라 고생많으셨습니다 😊

@PatchMapping("/blog/{blogId}/title")
public ResponseEntity updateBlogTitle(
@PathVariable Long blogId,
@Valid @RequestBody BlogTitleUdateRequest blogTitleUdateRequest

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 코드 부분에 오타가 있는 것 같습니다!!
'BlogTitleUdateRequest' -> 'BlogTitleUpdateRequest'

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우왓 감사합니다!

Comment on lines +38 to +43
@GetMapping("/{memberId}")
public ResponseEntity<ApiResponse<?>> findMemberById(@PathVariable("memberId") Long memberId) {
return ApiUtils.success(HttpStatus.OK, memberService.findMemberById(memberId));
}

@DeleteMapping("/{memberId}")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위의 Controller의 @RequestMapping이 ("/api/v1") 이라서 만약 다른 Entity의 정보를 불러오는 GET api를 구현 시 주소가 일치하면서 오류들

(1)블로그 GET API ("/api/v1/{postId}") -> 실제 주소 : /api/v1/1
(2)멤버 GET API ("/api/v1/{memberId}") -> 실제 주소 : /api/v1/1 *구분이 어려움

이 생길 수 있어 "/members/{memberId}"로 바꾼다면 그러한 오류를 방지할 수 있을 것 같다는 생각이 들어서 적어보았습니다..!:)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 이 부분 제가 빠뜨렸네요... 말씀해주신 부분이 오류 방지 측면에서 맞는 것 같습니다! 지적 감사해요😀

Comment on lines +17 to +19

private String cotent;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분에 오타가 있는 것 같습니다...!
'cotent' -> 'content' 로 바꾸면 더 좋을 것 같아요!!!

Copy link
Member Author

@JungYoonShin JungYoonShin Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 감사합니다!

Comment on lines +28 to +29
private static final String[] AUTH_WHITE_LIST = {"/api/v1/members", "/", "/api/v1/members/reissue"};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기에서 "/" 는 전체 경로를 추가하는 표현인가요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 "/"의 의도가 궁금합니다~ 추가로 알고 계실 것 같지만 와일드카드 표현도 있으니 찾아보시는 것 권장드립니당

Copy link

@tkdwns414 tkdwns414 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

부족하지만 요청하셨길래 이번에는 달아보았습니다.

}

@GetMapping("/{memberId}")
public ResponseEntity<ApiResponse<?>> findMemberById(@PathVariable("memberId") Long memberId) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApiResponse를 쓰더라도 안에 와일드카드가 아니라 실제 DTO값을 사용하시는게 같이 작업하는 사람 입장에서 코드의 가독성이 더 좋아질 것 같아요!

Comment on lines +28 to +29
private static final String[] AUTH_WHITE_LIST = {"/api/v1/members", "/", "/api/v1/members/reissue"};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 "/"의 의도가 궁금합니다~ 추가로 알고 계실 것 같지만 와일드카드 표현도 있으니 찾아보시는 것 권장드립니당

Comment on lines +13 to +36
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

private final AuthenticationManager authenticationManager;

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
// 사용자가 제공한 인증 정보 추출
String email = request.getParameter("email");
String password = request.getParameter("password");

// UsernamePasswordAuthenticationToken 생성(이때 authentication 는 인증 여부를 확인하는 authenticated 값이 false)
UserAuthentication authentication = UserAuthentication.createUserAuthentication(email, password);

/**
* 실제 인증을 수행하도록 AuthenticationManager에게 위임
* 사용자가 입력한 email, password가 UserDetails를 통해 읽어온 UserDetails 객체에 들어있는 emial, password와 일치하는지 확인
* 인증이 성공적으로 완료되면 SecurityContextHolder에 저장된다.
*/
return authenticationManager.authenticate(authentication);
}


}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

흥미롭네요~

.exceptionHandling(exception ->
{
exception.authenticationEntryPoint(customJwtAuthenticationEntryPoint);
exception.accessDeniedHandler(customAccessDeniedHandler);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

따로 role 사용하는 API 없었는데 accessDeniedHandler까지 하셨네요 짱입니다~


import java.io.IOException;

public class CustonAuthenticaitonFailureHandler implements AuthenticationFailureHandler {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오타가 있는 것 같습니다. 그리고 제가 Security를 잘 몰라서 그러는데 해당 부분은 어떤 역할을 하려고 만드신건지 궁금합니다! (진짜 궁금)

Comment on lines +50 to +63
public TokenInfo reissue(String refreshToken) {
jwtTokenProvider.validateToken(refreshToken);
String userEmail = jwtTokenProvider.getUserFromJwt(refreshToken);
System.out.println("userEmail = " + userEmail);
Member member = findMember(userEmail);

//리프레시 토큰 탈취여부 확인(탈취범이 정상 유저보다 먼저 재발급 받았을 경우 -> 재로그인 유도)
jwtTokenProvider.matchRefreshToken(refreshToken, findRefreshToken(member.getId()).getRefreshToken());

UserAuthentication userAuthentication = UserAuthentication.createUserAuthentication(userEmail);
TokenInfo token = issueTokenAndStoreRefreshToken(userAuthentication, member.getId());

return token;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

링크 걸어주신 블로그 읽고 같이 고민하신 흔적이 보여서 좋은 것 같습니다. 다만 블로그에서는 마지막에 결국 해결 불가능한 문제 같다고 찝찝해하는 내용이 있었던 것 같은데 제가 아는 선에서는 token의 경우 개발자의 잘못으로 유출되는 것이 아닌 유저가 잘못된 사용으로 인해 노출하지 않는 한 탈취될 위험이 없는 것으로 압니다.

카드 발급해줬더니 카드 잃어버렸다고 카드 발급한 사람 탓하면 안되듯이 개발자는 이정도로 최대한 보안을 신경쓰며 만드는 선이 최선이라고 저는 생각합니다~

Comment on lines +97 to +100
private RefreshToken findRefreshToken(Long userId) {
return refreshTokenRepository.findById(userId)
.orElseThrow(() -> new NotFoundException(ErrorMessage.EXPIRED_REFRESH_TOKEN));
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아예 잘못된 rtk를 보낼 수도 있는데 expired 보다는 그냥 not_found라는 표현이 적절하지 않을까 조심스레... 적어봅니다

@jun3327
Copy link

jun3327 commented Jun 5, 2024

과제하시느라 고생하셨습니다!! RTR 방식은 처음 접해봤는데요, 토큰 탈취범이 유저보다 먼저 리프레시 토큰을 발급할 때의 경우의 수까지 고려하는 것은 생각지도 못한 부분이었네요!! 링크 걸어주신 자료까지 잘 봤습니다! 탈취 시나리오도 앞으로 잘 고려해야 겠네요

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
FEAT 기능 추가
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants