-
Notifications
You must be signed in to change notification settings - Fork 2
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
Feat/#385 토큰을 관리하는 방식 수정 #424
Changes from all commits
8a02476
f9f5d3d
6d9071c
d83945b
4d5282c
850dc5e
8c5f57b
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,28 @@ | ||
package shook.shook.auth.application; | ||
|
||
import java.util.Set; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.scheduling.annotation.Scheduled; | ||
import org.springframework.stereotype.Component; | ||
import shook.shook.auth.exception.TokenException; | ||
import shook.shook.auth.repository.InMemoryTokenPairRepository; | ||
|
||
@RequiredArgsConstructor | ||
@Component | ||
public class TokenPairScheduler { | ||
|
||
private final TokenProvider tokenProvider; | ||
private final InMemoryTokenPairRepository inMemoryTokenPairRepository; | ||
|
||
@Scheduled(cron = "${schedules.cron}") | ||
public void removeExpiredTokenPair() { | ||
final Set<String> refreshTokens = inMemoryTokenPairRepository.getTokenPairs().keySet(); | ||
for (String refreshToken : refreshTokens) { | ||
try { | ||
tokenProvider.parseClaims(refreshToken); | ||
} catch (TokenException.ExpiredTokenException e) { | ||
inMemoryTokenPairRepository.delete(refreshToken); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package shook.shook.auth.repository; | ||
|
||
import java.util.Map; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import org.springframework.stereotype.Repository; | ||
import shook.shook.auth.exception.TokenException; | ||
import shook.shook.auth.exception.TokenException.TokenPairNotMatchingException; | ||
|
||
@Repository | ||
public class InMemoryTokenPairRepository { | ||
|
||
private final Map<String, String> tokenPairs = new ConcurrentHashMap<>(); | ||
|
||
Comment on lines
+9
to
+13
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. db에 저장하는 대신 인메모리를 이용한 이유는 다음과 같습니다.
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. 납득완. 좋습니다!!!! |
||
public void validateTokenPair(final String refreshToken, final String accessToken) { | ||
if (!tokenPairs.containsKey(refreshToken)) { | ||
throw new TokenException.RefreshTokenNotFoundException(Map.of("wrongRefreshToken", refreshToken)); | ||
} | ||
if (!tokenPairs.get(refreshToken).equals(accessToken)) { | ||
throw new TokenPairNotMatchingException(Map.of("wrongAccessToken", accessToken)); | ||
} | ||
} | ||
|
||
public void addOrUpdateTokenPair(final String refreshToken, final String accessToken) { | ||
tokenPairs.put(refreshToken, accessToken); | ||
} | ||
|
||
public void delete(final String refreshToken) { | ||
tokenPairs.remove(refreshToken); | ||
} | ||
|
||
public Map<String, String> getTokenPairs() { | ||
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. 먼저 방어적 복사를 이용하지 않았던 이유는 방어적 복사를 이용하게 되서 원본과 복사본의 참조가 끊어서 복사본의 변형이 원본이 반영되지 않게 되는데 저는 현재 이 getTokenPairs()를 scheduler에서 이용해서 외부에 변화가 원본에도 적용이 되어야해서 방어적 복사를 이용하지 않았습니다. |
||
return tokenPairs; | ||
} | ||
|
||
public void clear() { | ||
tokenPairs.clear(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
package shook.shook.auth.ui; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.CookieValue; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestHeader; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import shook.shook.auth.application.AuthService; | ||
import shook.shook.auth.application.dto.ReissueAccessTokenResponse; | ||
|
@@ -16,18 +18,21 @@ public class AccessTokenReissueController implements AccessTokenReissueApi { | |
|
||
private static final String EMPTY_REFRESH_TOKEN = "none"; | ||
private static final String REFRESH_TOKEN_KEY = "refreshToken"; | ||
private static final String TOKEN_PREFIX = "Bearer "; | ||
|
||
private final AuthService authService; | ||
|
||
@GetMapping("/reissue") | ||
@PostMapping("/reissue") | ||
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. reissue 요청이 POST로 바뀐 이유가 무엇인가요? 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. 처음에는 단순하게 accessToken을 가져온다라고 생각을 했는데 이제는 accessToken을 재발급하고 메모리에 저장과정이 발생하여 저장소를 변경하는 작업이 발생합니다. 그래서 get보다는 post가 적절하다고 생각했습니다. 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. 그런데 사실 HTTP 요청을 보내는 입장에서 보면, "내가 제공한 refresh Token 정보를 기반으로 accessToken을 발급 받아온다!" 이기 때문에 서버에서 저장소에 저장을 하고, 말고는 사실 중요한 내용이 아닐 것 같다는 생각도 들어요. HTTP Method를 클라이언트 입장에서 보는지, 서버 입장에서 보는지의 차이인 것 같아요. 더 이야기 해보고 팀 내에서 정해보면 좋을 것 같습니다! :) 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. 저도 바론의 의견에 동의합니다. 물론 서버 입장에서는 저장을 하는 거지만, 클라이언트는 결국 값을 받기만 하는거니까요! |
||
public ResponseEntity<ReissueAccessTokenResponse> reissueAccessToken( | ||
@CookieValue(value = REFRESH_TOKEN_KEY, defaultValue = EMPTY_REFRESH_TOKEN) final String refreshToken | ||
@CookieValue(value = REFRESH_TOKEN_KEY, defaultValue = EMPTY_REFRESH_TOKEN) final String refreshToken, | ||
@RequestHeader(HttpHeaders.AUTHORIZATION) final String authorization | ||
) { | ||
if (refreshToken.equals(EMPTY_REFRESH_TOKEN)) { | ||
throw new AuthorizationException.RefreshTokenNotFoundException(); | ||
} | ||
final String accessToken = authorization.split(TOKEN_PREFIX)[1]; | ||
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. accessToken을 header에서 가져오는 작업이 Controller에서 진행되도록 설정하신 이유가 궁금해요! 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. 좋은 의견이네요 저는 깊게 고민을 하지 못했던 것 같습니다👍 이 부분 |
||
final ReissueAccessTokenResponse response = | ||
authService.reissueAccessTokenByRefreshToken(refreshToken); | ||
authService.reissueAccessTokenByRefreshToken(refreshToken, accessToken); | ||
|
||
return ResponseEntity.ok(response); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,5 +42,6 @@ excel: | |
song-length-suffix: "s" | ||
|
||
schedules: | ||
cron: 0/1 * * * * * | ||
in-memory-song: | ||
cron: "0/1 * * * * *" # 1초 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; | ||
import static org.mockito.ArgumentMatchers.any; | ||
import static org.mockito.Mockito.when; | ||
|
||
|
@@ -17,6 +18,7 @@ | |
import shook.shook.auth.application.dto.ReissueAccessTokenResponse; | ||
import shook.shook.auth.application.dto.TokenPair; | ||
import shook.shook.auth.exception.TokenException; | ||
import shook.shook.auth.repository.InMemoryTokenPairRepository; | ||
import shook.shook.member.application.MemberService; | ||
import shook.shook.member.domain.Member; | ||
import shook.shook.member.domain.repository.MemberRepository; | ||
|
@@ -35,8 +37,15 @@ class AuthServiceTest { | |
@Autowired | ||
private MemberService memberService; | ||
|
||
@Autowired | ||
private InMemoryTokenPairRepository inMemoryTokenPairRepository; | ||
|
||
private TokenProvider tokenProvider; | ||
|
||
private String refreshToken; | ||
|
||
private String accessToken; | ||
|
||
private AuthService authService; | ||
|
||
@BeforeEach | ||
|
@@ -45,8 +54,11 @@ void setUp() { | |
100000L, | ||
1000000L, | ||
"asdfsdsvsdf2esvsdvsdvs23"); | ||
authService = new AuthService(memberService, googleInfoProvider, tokenProvider); | ||
savedMember = memberRepository.save(new Member("[email protected]", "shook")); | ||
refreshToken = tokenProvider.createRefreshToken(savedMember.getId(), savedMember.getNickname()); | ||
accessToken = tokenProvider.createAccessToken(savedMember.getId(), savedMember.getNickname()); | ||
inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken); | ||
authService = new AuthService(memberService, googleInfoProvider, tokenProvider, inMemoryTokenPairRepository); | ||
} | ||
|
||
@AfterEach | ||
|
@@ -80,19 +92,16 @@ void success_login() { | |
|
||
assertThat(result.getAccessToken()).isEqualTo(accessToken); | ||
assertThat(result.getRefreshToken()).isEqualTo(refreshToken); | ||
assertDoesNotThrow(() -> inMemoryTokenPairRepository.validateTokenPair(refreshToken, accessToken)); | ||
} | ||
|
||
@DisplayName("올바른 refresh 토큰이 들어오면 access 토큰을 재발급해준다.") | ||
@DisplayName("올바른 refresh 토큰과 access 토큰이 들어오면 access 토큰을 재발급해준다.") | ||
@Test | ||
void success_reissue() { | ||
//given | ||
final String refreshToken = tokenProvider.createRefreshToken( | ||
savedMember.getId(), | ||
savedMember.getNickname()); | ||
|
||
//when | ||
final ReissueAccessTokenResponse result = authService.reissueAccessTokenByRefreshToken( | ||
refreshToken); | ||
refreshToken, accessToken); | ||
|
||
//then | ||
final String accessToken = tokenProvider.createAccessToken( | ||
|
@@ -111,13 +120,13 @@ void fail_reissue_invalid_refreshToken() { | |
100L, | ||
"asdzzxcwetg2adfvssd3xZcZXCZvzx"); | ||
|
||
final String refreshToken = inValidTokenProvider.createRefreshToken( | ||
final String wrongRefreshToken = inValidTokenProvider.createRefreshToken( | ||
savedMember.getId(), | ||
savedMember.getNickname()); | ||
|
||
//when | ||
//then | ||
assertThatThrownBy(() -> authService.reissueAccessTokenByRefreshToken(refreshToken)) | ||
assertThatThrownBy(() -> authService.reissueAccessTokenByRefreshToken(wrongRefreshToken, accessToken)) | ||
.isInstanceOf(TokenException.NotIssuedTokenException.class); | ||
} | ||
|
||
|
@@ -136,7 +145,7 @@ void fail_reissue_expired_refreshToken() { | |
|
||
//when | ||
//then | ||
assertThatThrownBy(() -> authService.reissueAccessTokenByRefreshToken(refreshToken)) | ||
assertThatThrownBy(() -> authService.reissueAccessTokenByRefreshToken(refreshToken, accessToken)) | ||
.isInstanceOf(TokenException.ExpiredTokenException.class); | ||
} | ||
} |
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.
👍