-
Notifications
You must be signed in to change notification settings - Fork 1
Refactor/jira kan 71 token 쿠키 저장 및 redis aof rdb 고려 #85
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
The head ref may contain hidden characters: "Refactor/JIRA-kan-71-refactor-token-\uCFE0\uD0A4-\uC800\uC7A5-\uBC0F-redis-aof-rdb-\uACE0\uB824"
Changes from 33 commits
6f9ccf6
f10fa0a
53c468b
494ed54
fb0c5c7
b6571c2
8b40fe5
863e48b
4c23f33
ac134d5
bf21c86
5d40a4e
0020ee0
e33e228
c46f601
f52f1b1
1f57e96
8507afb
b1c2267
8f1d222
6b755c4
3a03bd4
a36e277
e3bd0b6
2cf67f4
f23cb1d
409ad86
eece890
e1fc08a
c042020
dfde3ec
5594ef4
aed8718
f335bb2
fcb8e97
1b96ef8
b9c9393
723313e
fdbaf57
d578af0
fe8ae75
7c282d5
449f2a3
7367188
29115a5
2991170
5399d58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| import bumblebee.xchangepass.global.security.jwt.JwtProvider; | ||
| import bumblebee.xchangepass.domain.refresh.dto.RefreshTokenResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseCookie; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
|
|
@@ -27,10 +28,18 @@ public RefreshTokenResponse refreshToken(final String refreshToken) { | |
| // Redis에서 사용자 ID 조회 | ||
| Long userId = refreshTokenRepository.getUserIdFromRefreshToken(refreshToken); | ||
|
|
||
| // Redis에 저장된 토큰과 비교 → 재사용 감지 | ||
| String storedToken = refreshTokenRepository.getRefreshToken(userId); | ||
|
Collaborator
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. 이 로직으로 볼 때 Redis에 전에 사용했던 refreshToken을 누적시키는 것 처럼 보이는데 redis의 용량은 한정적인데 방안이 있을까요?
Collaborator
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. RefreshToken Rotate방식을 적용해서 RefreshToken을 사용하면 레디스에서 삭제후 재발급하는 방식을 사용하고있습니다. if (storedToken == null || !storedToken.equals(refreshToken)) { } |
||
|
|
||
| if (storedToken == null || !storedToken.equals(refreshToken)) { | ||
| // 이미 사용된 토큰 or 위조된 토큰 → 재사용 시도 | ||
| refreshTokenRepository.deleteRefreshToken(userId); // Redis 강제 삭제 | ||
|
|
||
| throw ErrorCode.REFRESH_TOKEN_INVALID.commonException(); | ||
| } | ||
|
|
||
| // 새로운 Access Token 생성 | ||
| String newAccessToken = jwtProvider.generateAccessToken(userId); | ||
|
|
||
| // 새로운 Refresh Token 생성 후 Redis에 저장 (기존 것은 자동 삭제됨) | ||
| String newRefreshToken = jwtProvider.generateRefreshToken(userId); | ||
| refreshTokenRepository.saveRefreshToken(newRefreshToken, userId); | ||
|
|
||
|
|
@@ -40,6 +49,16 @@ public RefreshTokenResponse refreshToken(final String refreshToken) { | |
| .build(); | ||
| } | ||
|
|
||
| public ResponseCookie saveRefreshToken(RefreshTokenResponse response) { | ||
| return ResponseCookie.from("refreshToken", response.refreshToken()) | ||
| .httpOnly(true) | ||
| .secure(true) | ||
| .path("/") | ||
| .maxAge(60 * 60 * 24 * 7) | ||
| .sameSite("Strict") | ||
|
Collaborator
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. sameStite 설정의 경우 https 도메인이 아니면 의미가 없는데 대체 방안이 있을까요?
Collaborator
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. 금융서비스 특징상 https를 사용할 것이라 생각해서 만들었습니다만 만약 아니라면 |
||
| .build(); | ||
| } | ||
|
|
||
| /** | ||
| * refresh token 검증 | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| package bumblebee.xchangepass.domain.user.login; | ||
|
|
||
| import bumblebee.xchangepass.domain.refresh.dto.RefreshTokenResponse; | ||
| import bumblebee.xchangepass.domain.user.login.dto.request.LoginRequest; | ||
| import bumblebee.xchangepass.domain.user.login.dto.response.LoginResponse; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.media.Content; | ||
| import io.swagger.v3.oas.annotations.media.ExampleObject; | ||
|
|
@@ -11,10 +11,14 @@ | |
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseCookie; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.ErrorResponse; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| @Slf4j | ||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @Tag(name = "Login", description = "Login API") | ||
|
|
@@ -40,8 +44,16 @@ public class LoginController { | |
| }) | ||
| @ResponseStatus(HttpStatus.OK) | ||
| @PostMapping("/login") | ||
| public LoginResponse memberLogin(@RequestBody @Valid LoginRequest loginRequest) { | ||
| return loginService.login(loginRequest); | ||
| public ResponseEntity<RefreshTokenResponse> memberLogin(@RequestBody @Valid LoginRequest loginRequest) { | ||
| RefreshTokenResponse response = loginService.login(loginRequest); | ||
|
|
||
| ResponseCookie accessCookie = loginService.saveAccessToken(response.accessToken()); | ||
| ResponseCookie refreshCookie = loginService.saveRefreshToken(response); | ||
|
|
||
| return ResponseEntity.ok() | ||
| .header("Set-Cookie", accessCookie.toString()) | ||
|
Collaborator
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. access 토큰도 쿠키로 반환하는 것일까요?
Collaborator
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. 네 accessToken을 localStorage에 저장하게 되면 XSS 공격 시 토큰이 노출될 수 있는데 |
||
| .header("Set-Cookie", refreshCookie.toString()) | ||
|
Comment on lines
+54
to
+55
Collaborator
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. 대체적으로 access 로컬 / refresh 쿠키로 저장하는게 일반적인데 access도 쿠키로 저장하는 이유가 있을까요? |
||
| .build(); | ||
| } | ||
|
|
||
| @Operation(summary = "로그아웃", description = "로그아웃합니다.") | ||
|
|
@@ -55,8 +67,20 @@ public LoginResponse memberLogin(@RequestBody @Valid LoginRequest loginRequest) | |
| }) | ||
| @ResponseStatus(HttpStatus.OK) | ||
| @PostMapping("/logout") | ||
| public void logout(@RequestHeader("Authorization") String refreshToken) { | ||
| public ResponseEntity<RefreshTokenResponse> logout(@RequestHeader("Authorization") String refreshToken) { | ||
| if (refreshToken.startsWith("Bearer ")) { | ||
| refreshToken = refreshToken.substring(7); | ||
| } | ||
|
|
||
| // Refresh Token 삭제 | ||
| loginService.logout(refreshToken); | ||
|
|
||
| ResponseCookie expiredRefresh = loginService.deleteRefreshToken(); | ||
| ResponseCookie expiredAccess = loginService.deleteAccessToken(); | ||
|
|
||
| return ResponseEntity.ok() | ||
| .header("Set-Cookie", expiredRefresh.toString()) | ||
| .header("Set-Cookie", expiredAccess.toString()) | ||
| .build(); | ||
| } | ||
| } | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,8 @@ | |
|
|
||
| import bumblebee.xchangepass.domain.wallet.balance.entity.WalletBalance; | ||
| import bumblebee.xchangepass.domain.wallet.balance.repository.WalletBalanceRepository; | ||
| import bumblebee.xchangepass.domain.wallet.fraud.service.FraudDetectEvent; | ||
| import bumblebee.xchangepass.domain.wallet.fraud.service.FraudDetectionService; | ||
| import bumblebee.xchangepass.domain.wallet.transaction.entity.WalletTransactionType; | ||
| import bumblebee.xchangepass.domain.wallet.transaction.service.WalletTransactionService; | ||
| import bumblebee.xchangepass.domain.wallet.wallet.entity.Wallet; | ||
|
|
@@ -11,6 +13,7 @@ | |
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.time.LocalDateTime; | ||
| import java.util.Currency; | ||
| import java.util.List; | ||
|
|
||
|
|
@@ -20,6 +23,7 @@ public class WalletBalanceService { | |
|
|
||
| private final WalletBalanceRepository balanceRepository; | ||
| private final WalletTransactionService transactionService; | ||
| private final FraudDetectionService fraudDetectionService; | ||
|
|
||
| public void createBalance(Wallet wallet, Currency currency) { | ||
| if (balanceRepository.existsByCurrency(wallet.getWalletId(), currency)) { | ||
|
|
@@ -75,6 +79,13 @@ public void withdrawBalance(WalletBalance balance, BigDecimal amount) { | |
| } | ||
|
|
||
|
Collaborator
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. walletbalance 서버스도 다른 서버스와 같이 간단하게 주석을 추가해주실 수 있을까요?
Collaborator
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. 넵! |
||
| public void transferBalance(WalletBalance fromBalance, WalletBalance toBalance, BigDecimal amount) { | ||
| fraudDetectionService.detect(new FraudDetectEvent( | ||
| fromBalance.getWallet().getUser().getUserId(), | ||
| amount, | ||
| LocalDateTime.now(), | ||
| null | ||
| )); | ||
|
|
||
| fromBalance.subtractBalance(amount); | ||
| toBalance.addBalance(amount); | ||
| balanceRepository.save(fromBalance); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package bumblebee.xchangepass.domain.wallet.fraud.service; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.time.LocalDateTime; | ||
|
|
||
| public record FraudDetectEvent( | ||
| Long userId, | ||
| BigDecimal amount, | ||
| LocalDateTime timestamp, | ||
| String detail | ||
| ) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package bumblebee.xchangepass.domain.wallet.fraud.service; | ||
|
|
||
| import bumblebee.xchangepass.domain.wallet.transaction.consumer.SlackNotifier; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class FraudDetectionService { | ||
|
|
||
| private final RedisFraudStorageService redisFraudStorage; | ||
| private final FraudRuleEvaluator ruleEvaluator; | ||
| private final SlackNotifier slackNotifier; | ||
|
|
||
| public void detect(FraudDetectEvent event) { | ||
| redisFraudStorage.store(event.userId(), event.amount()); | ||
|
|
||
| boolean isFraud = ruleEvaluator.isSuspicious(event.userId(), event.amount()); | ||
|
|
||
| if (isFraud) { | ||
| String reason = ruleEvaluator.getLastDetectedReason(); | ||
| slackNotifier.notifyFraud(new FraudDetectEvent( | ||
| event.userId(), | ||
| event.amount(), | ||
| event.timestamp(), | ||
| reason | ||
| )); | ||
| } | ||
| } | ||
| } |
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.
일관성을 위해서 주석을 추가해주시면 감사하겠습니다