From 66e47edf849ce7a959e36145b7ff8a4e8dee9a83 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 4 May 2025 02:37:54 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/auth/service/AccessToken.java | 11 +++++++++++ .../solidconnection/auth/service/RefreshToken.java | 11 +++++++++++ .../example/solidconnection/auth/service/Subject.java | 6 ++++++ 3 files changed, 28 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/auth/service/AccessToken.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/RefreshToken.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/Subject.java diff --git a/src/main/java/com/example/solidconnection/auth/service/AccessToken.java b/src/main/java/com/example/solidconnection/auth/service/AccessToken.java new file mode 100644 index 000000000..c94e891aa --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/AccessToken.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.auth.service; + +public record AccessToken( + Subject subject, + String token +) { + + public AccessToken(String subject, String token) { + this(new Subject(subject), token); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/RefreshToken.java b/src/main/java/com/example/solidconnection/auth/service/RefreshToken.java new file mode 100644 index 000000000..2aac3ad8c --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/RefreshToken.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.auth.service; + +public record RefreshToken( + Subject subject, + String token +) { + + RefreshToken(String subject, String token) { + this(new Subject(subject), token); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/Subject.java b/src/main/java/com/example/solidconnection/auth/service/Subject.java new file mode 100644 index 000000000..2c03eb013 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/Subject.java @@ -0,0 +1,6 @@ +package com.example.solidconnection.auth.service; + +public record Subject( + String value +) { +} From f27233c0a415439eb846b3bd326c4d6f16f24703 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 4 May 2025 02:41:28 +0900 Subject: [PATCH 02/12] =?UTF-8?q?refactor:=20authTokenProvider=EC=97=90=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EA=B0=9D=EC=B2=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AuthTokenProvider.java | 65 ++++++++++++------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java index b682a4b39..294cd3ff0 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java @@ -3,49 +3,70 @@ import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.config.security.JwtProperties; import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.util.JwtUtils; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; -import java.util.Optional; +import java.util.Objects; @Component -public class AuthTokenProvider extends TokenProvider { +public class AuthTokenProvider extends TokenProvider implements BlacklistChecker { public AuthTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { super(jwtProperties, redisTemplate); } - public String generateAccessToken(SiteUser siteUser) { - String subject = getSubject(siteUser); - return generateToken(subject, TokenType.ACCESS); + public AccessToken generateAccessToken(Subject subject) { + String token = generateToken(subject.value(), TokenType.ACCESS); + return new AccessToken(subject, token); } - public String generateAccessToken(String subject) { - return generateToken(subject, TokenType.ACCESS); + public RefreshToken generateAndSaveRefreshToken(Subject subject) { + String token = generateToken(subject.value(), TokenType.REFRESH); + saveToken(token, TokenType.REFRESH); + return new RefreshToken(subject, token); } - public String generateAndSaveRefreshToken(SiteUser siteUser) { - String subject = getSubject(siteUser); - String refreshToken = generateToken(subject, TokenType.REFRESH); - return saveToken(refreshToken, TokenType.REFRESH); + /* + * 액세스 토큰을 블랙리스트에 저장한다. + * - key = BLACKLIST:{subject} + * - value = {accessToken} + * */ + public void addToBlacklist(AccessToken accessToken) { + saveToken(accessToken.token(), TokenType.BLACKLIST); } - public String generateAndSaveBlackListToken(String accessToken) { - String blackListToken = generateToken(accessToken, TokenType.BLACKLIST); - return saveToken(blackListToken, TokenType.BLACKLIST); - } - - public Optional findRefreshToken(String subject) { + /* + * 유효한 리프레시 토큰인지 확인한다. + * - 요청된 토큰과 같은 subject 의 리프레시 토큰을 조회한다. + * - 조회된 리프레시 토큰과 요청된 토큰이 같은지 비교한다. + * */ + public boolean isValidRefreshToken(String requestedRefreshToken) { + String subject = JwtUtils.parseSubject(requestedRefreshToken, jwtProperties.secret()); String refreshTokenKey = TokenType.REFRESH.addPrefix(subject); - return Optional.ofNullable(redisTemplate.opsForValue().get(refreshTokenKey)); + String foundRefreshToken = redisTemplate.opsForValue().get(refreshTokenKey); + return Objects.equals(requestedRefreshToken, foundRefreshToken); } - public Optional findBlackListToken(String subject) { + /* + * 블랙리스트에 등록된 토큰인지 확인한다. + * - 액세스 토큰의 subject 에 해당하는 블랙리스트 토큰을 조회한다. + * - 조회된 블랙리스트 토큰과 요청된 액세스 토큰이 같은지 비교한다. + */ + @Override + public boolean isTokenBlacklisted(String accessToken) { + String subject = JwtUtils.parseSubject(accessToken, jwtProperties.secret()); String blackListTokenKey = TokenType.BLACKLIST.addPrefix(subject); - return Optional.ofNullable(redisTemplate.opsForValue().get(blackListTokenKey)); + String foundBlackListToken = redisTemplate.opsForValue().get(blackListTokenKey); + return Objects.equals(accessToken, foundBlackListToken); + } + + public Subject parseSubject(String token) { + String subject = JwtUtils.parseSubject(token, jwtProperties.secret()); + return new Subject(subject); } - private String getSubject(SiteUser siteUser) { - return siteUser.getId().toString(); + public Subject toSubject(SiteUser siteUser) { + return new Subject(siteUser.getId().toString()); } } From 71a1ba0767032df04dadbaba4070d8c971d32e02 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 4 May 2025 02:41:50 +0900 Subject: [PATCH 03/12] =?UTF-8?q?refactor:=20authService=EC=97=90=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EA=B0=9D=EC=B2=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 6 ++--- .../auth/dto/ReissueResponse.java | 9 +++++++- .../auth/service/AuthService.java | 23 +++++++------------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 7fe543bd7..056957d21 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -9,6 +9,7 @@ import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; import com.example.solidconnection.auth.dto.oauth.OAuthResponse; +import com.example.solidconnection.auth.service.AccessToken; import com.example.solidconnection.auth.service.AuthService; import com.example.solidconnection.auth.service.CommonSignUpTokenProvider; import com.example.solidconnection.auth.service.EmailSignInService; @@ -97,11 +98,10 @@ public ResponseEntity signUp( public ResponseEntity signOut( Authentication authentication ) { - String token = authentication.getCredentials().toString(); - if (token == null) { + if (!(authentication.getCredentials() instanceof AccessToken accessToken)) { // null or AccessToken 로 형변환 실패 throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다."); } - authService.signOut(token); + authService.signOut(accessToken); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java b/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java index 48b55e6cb..972470cca 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java @@ -1,5 +1,12 @@ package com.example.solidconnection.auth.dto; +import com.example.solidconnection.auth.service.AccessToken; + public record ReissueResponse( - String accessToken) { + String accessToken +) { + + public static ReissueResponse from(AccessToken accessToken) { + return new ReissueResponse(accessToken.token()); + } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index 289893dbf..86bb9045a 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -1,9 +1,7 @@ package com.example.solidconnection.auth.service; - import com.example.solidconnection.auth.dto.ReissueRequest; import com.example.solidconnection.auth.dto.ReissueResponse; -import com.example.solidconnection.config.security.JwtProperties; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import lombok.RequiredArgsConstructor; @@ -11,25 +9,21 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; -import java.util.Objects; -import java.util.Optional; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; -import static com.example.solidconnection.util.JwtUtils.parseSubject; @RequiredArgsConstructor @Service public class AuthService { private final AuthTokenProvider authTokenProvider; - private final JwtProperties jwtProperties; /* * 로그아웃 한다. * - 엑세스 토큰을 블랙리스트에 추가한다. * */ - public void signOut(String accessToken) { - authTokenProvider.generateAndSaveBlackListToken(accessToken); + public void signOut(AccessToken accessToken) { + authTokenProvider.addToBlacklist(accessToken); } /* @@ -45,19 +39,18 @@ public void quit(SiteUser siteUser) { /* * 액세스 토큰을 재발급한다. - * - 요청된 리프레시 토큰과 동일한 subject 의 토큰이 저장되어 있으며 값이 일치할 경우, 액세스 토큰을 재발급한다. - * - 그렇지 않으면 예외를 반환한다. + * - 유효한 리프레시토큰이면, 액세스 토큰을 재발급한다. + * - 그렇지 않으면 예외를 발생시킨다. * */ public ReissueResponse reissue(ReissueRequest reissueRequest) { // 리프레시 토큰 확인 String requestedRefreshToken = reissueRequest.refreshToken(); - String subject = parseSubject(requestedRefreshToken, jwtProperties.secret()); - Optional savedRefreshToken = authTokenProvider.findRefreshToken(subject); - if (!Objects.equals(requestedRefreshToken, savedRefreshToken.orElse(null))) { + if (!authTokenProvider.isValidRefreshToken(requestedRefreshToken)) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } // 액세스 토큰 재발급 - String newAccessToken = authTokenProvider.generateAccessToken(subject); - return new ReissueResponse(newAccessToken); + Subject subject = authTokenProvider.parseSubject(requestedRefreshToken); + AccessToken newAccessToken = authTokenProvider.generateAccessToken(subject); + return ReissueResponse.from(newAccessToken); } } From af06df832e49040eaad08f293693d1decff6df3d Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 4 May 2025 02:42:08 +0900 Subject: [PATCH 04/12] =?UTF-8?q?refactor:=20signInService=EC=97=90=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EA=B0=9D=EC=B2=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/solidconnection/auth/dto/SignInResponse.java | 7 +++++++ .../solidconnection/auth/service/SignInService.java | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java index a4ae442e2..b01fdd369 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java @@ -1,7 +1,14 @@ package com.example.solidconnection.auth.dto; +import com.example.solidconnection.auth.service.AccessToken; +import com.example.solidconnection.auth.service.RefreshToken; + public record SignInResponse( String accessToken, String refreshToken ) { + + public static SignInResponse of(AccessToken accessToken, RefreshToken refreshToken) { + return new SignInResponse(accessToken.token(), refreshToken.token()); + } } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/SignInService.java index 820d2e573..c2b129214 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignInService.java @@ -15,9 +15,10 @@ public class SignInService { @Transactional public SignInResponse signIn(SiteUser siteUser) { resetQuitedAt(siteUser); - String accessToken = authTokenProvider.generateAccessToken(siteUser); - String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); - return new SignInResponse(accessToken, refreshToken); + Subject subject = authTokenProvider.toSubject(siteUser); + AccessToken accessToken = authTokenProvider.generateAccessToken(subject); + RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(subject); + return SignInResponse.of(accessToken, refreshToken); } private void resetQuitedAt(SiteUser siteUser) { From 3f94b96d280e2c3d279175c605b6a3a035eb4392 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 4 May 2025 03:38:01 +0900 Subject: [PATCH 05/12] =?UTF-8?q?test:=20AuthTokenProvider=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B3=B4=EC=B6=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AuthTokenProviderTest.java | 167 ++++++------------ 1 file changed, 53 insertions(+), 114 deletions(-) diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java index 57a9ea789..e3589cc37 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java @@ -1,14 +1,7 @@ package com.example.solidconnection.auth.service; import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.config.security.JwtProperties; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.TestContainerSpringBootTest; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import com.example.solidconnection.util.JwtUtils; -import io.jsonwebtoken.Jwts; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -16,8 +9,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; -import java.util.Optional; - import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -28,157 +19,105 @@ class AuthTokenProviderTest { @Autowired private AuthTokenProvider authTokenProvider; - @Autowired - private SiteUserRepository siteUserRepository; - @Autowired private RedisTemplate redisTemplate; - @Autowired - private JwtProperties jwtProperties; - - private SiteUser siteUser; - private String subject; + private Subject subject; @BeforeEach void setUp() { - siteUser = createSiteUser(); - siteUserRepository.save(siteUser); - subject = siteUser.getId().toString(); + subject = new Subject("subject123"); } - @Nested - class 액세스_토큰을_제공한다 { - - @Test - void SiteUser_로_액세스_토큰을_생성한다() { - // when - String token = authTokenProvider.generateAccessToken(siteUser); - - // then - String actualSubject = JwtUtils.parseSubject(token, jwtProperties.secret()); - assertThat(actualSubject).isEqualTo(subject); - } - - @Test - void subject_로_액세스_토큰을_생성한다() { - // given - String subject = "subject123"; - - // when - String token = authTokenProvider.generateAccessToken(subject); + @Test + void 액세스_토큰을_생성한다() { + // when + AccessToken accessToken = authTokenProvider.generateAccessToken(subject); - // then - String actualSubject = JwtUtils.parseSubject(token, jwtProperties.secret()); - assertThat(actualSubject).isEqualTo(subject); - } + // then + String actualSubject = authTokenProvider.parseSubject(accessToken.token()).value(); + assertThat(actualSubject).isEqualTo(subject.value()); } @Nested class 리프레시_토큰을_제공한다 { @Test - void SiteUser_로_리프레시_토큰을_생성하고_저장한다() { + void 리프레시_토큰을_생성하고_저장한다() { // when - String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); + RefreshToken actualRefreshToken = authTokenProvider.generateAndSaveRefreshToken(subject); // then - String actualSubject = JwtUtils.parseSubject(refreshToken, jwtProperties.secret()); - String refreshTokenKey = TokenType.REFRESH.addPrefix(subject); + String actualSubject = authTokenProvider.parseSubject(actualRefreshToken.token()).value(); + String refreshTokenKey = TokenType.REFRESH.addPrefix(subject.value()); + String expectedRefreshToken = redisTemplate.opsForValue().get(refreshTokenKey); assertAll( - () -> assertThat(actualSubject).isEqualTo(subject), - () -> assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isEqualTo(refreshToken) + () -> assertThat(actualSubject).isEqualTo(subject.value()), + () -> assertThat(actualRefreshToken.token()).isEqualTo(expectedRefreshToken) ); } @Test - void 저장된_리프레시_토큰을_조회한다() { + void 유효한_리프레시_토큰인지_확인한다() { // given - String refreshToken = "refreshToken"; - redisTemplate.opsForValue().set(TokenType.REFRESH.addPrefix(subject), refreshToken); + RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(subject); + AccessToken fakeRefreshToken = authTokenProvider.generateAccessToken(subject); // todo: issue#296 - // when - Optional optionalRefreshToken = authTokenProvider.findRefreshToken(subject); - - // then - assertThat(optionalRefreshToken.get()).isEqualTo(refreshToken); - } - - @Test - void 저장되지_않은_리프레시_토큰을_조회한다() { - // when - Optional optionalRefreshToken = authTokenProvider.findRefreshToken(subject); - - // then - assertThat(optionalRefreshToken).isEmpty(); + // when, then + assertAll( + () -> assertThat(authTokenProvider.isValidRefreshToken(refreshToken.token())).isTrue(), + () -> assertThat(authTokenProvider.isValidRefreshToken(fakeRefreshToken.token())).isFalse() + ); } } @Nested - class 블랙리스트_토큰을_제공한다 { - - @Test - void 엑세스_토큰으로_블랙리스트_토큰을_생성하고_저장한다() { - // when - String accessToken = "accessToken"; - String blackListToken = authTokenProvider.generateAndSaveBlackListToken(accessToken); - - // then - String actualSubject = JwtUtils.parseSubject(blackListToken, jwtProperties.secret()); - String blackListTokenKey = TokenType.BLACKLIST.addPrefix(accessToken); - assertAll( - () -> assertThat(actualSubject).isEqualTo(accessToken), - () -> assertThat(redisTemplate.opsForValue().get(blackListTokenKey)).isEqualTo(blackListToken) - ); - } + class 블랙리스트를_관리한다 { @Test - void 저장된_블랙리스트_토큰을_조회한다() { + void 액세스_토큰을_블랙리스트에_추가한다() { // given - String accessToken = "accessToken"; - String blackListToken = "token"; - redisTemplate.opsForValue().set(TokenType.BLACKLIST.addPrefix(accessToken), blackListToken); + AccessToken accessToken = authTokenProvider.generateAccessToken(subject); // todo: issue#296 // when - Optional optionalBlackListToken = authTokenProvider.findBlackListToken(accessToken); + authTokenProvider.addToBlacklist(accessToken); // then - assertThat(optionalBlackListToken).hasValue(blackListToken); + String blackListTokenKey = TokenType.BLACKLIST.addPrefix(subject.value()); + String foundBlackListToken = redisTemplate.opsForValue().get(blackListTokenKey); + assertThat(foundBlackListToken).isEqualTo(accessToken.token()); } + /* + * todo: JwtUtils 나 TokenProvider 를 스프링 빈으로 주입받도록 변경한다. (issue#296) + * - 아래 테스트 코드에서는, 내부적으로 JwtUtils.parseSubject() 메서드가 호출될 때 발생하는 예외를 피하기 위해 jwt토큰을 생성한다. + * - 테스트 작성자는 예외 발생을 피하기 위해 "제대로된 jwt 토큰 생성이 필요하다"는 것을 몰라야한다. + * - 따라서, JwtUtils 나 TokenProvider 를 스프링 빈으로 주입받도록 변경하고, 테스트에서 mock 을 사용하여 의존성을 끊을 필요가 있다. + */ @Test - void 저장되지_않은_블랙리스트_토큰을_조회한다() { - // when - Optional optionalBlackListToken = authTokenProvider.findBlackListToken("accessToken"); + void 블랙리스트에_있는_토큰인지_확인한다() { + // given + AccessToken accessToken = authTokenProvider.generateAccessToken(subject); + authTokenProvider.addToBlacklist(accessToken); + AccessToken notRegisteredAccessToken = authTokenProvider.generateAccessToken(new Subject("!")); - // then - assertThat(optionalBlackListToken).isEmpty(); + // when, then + assertAll( + () -> assertThat(authTokenProvider.isTokenBlacklisted(accessToken.token())).isTrue(), + () -> assertThat(authTokenProvider.isTokenBlacklisted(notRegisteredAccessToken.token())).isFalse() + ); } } @Test - void 토큰을_생성한다() { + void 토큰으로부터_Subject_를_추출한다() { + // given + String accessToken = authTokenProvider.generateAccessToken(subject).token(); + // when - String subject = "subject123"; - String token = authTokenProvider.generateToken(subject, TokenType.ACCESS); + Subject actualSubject = authTokenProvider.parseSubject(accessToken); // then - String extractedSubject = Jwts.parser() - .setSigningKey(jwtProperties.secret()) - .parseClaimsJws(token) - .getBody() - .getSubject(); - assertThat(subject).isEqualTo(extractedSubject); - } - - private SiteUser createSiteUser() { - SiteUser siteUser = new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - PreparationStatus.CONSIDERING, - Role.MENTEE - ); - return siteUserRepository.save(siteUser); + assertThat(actualSubject.value()).isEqualTo(subject.value()); } } From d33933e10015c4fbb3b3b5127e17bc2631a91042 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 4 May 2025 03:50:36 +0900 Subject: [PATCH 06/12] =?UTF-8?q?test:=20AuthService=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=B4=EC=B6=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AuthServiceTest.java | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java index 0030e45e2..206b43872 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java @@ -1,16 +1,13 @@ package com.example.solidconnection.auth.service; -import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.auth.dto.ReissueRequest; import com.example.solidconnection.auth.dto.ReissueResponse; -import com.example.solidconnection.config.security.JwtProperties; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; -import com.example.solidconnection.util.JwtUtils; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -35,19 +32,16 @@ class AuthServiceTest { @Autowired private SiteUserRepository siteUserRepository; - @Autowired - private JwtProperties jwtProperties; - @Test void 로그아웃한다() { // given - String accessToken = "accessToken"; + AccessToken accessToken = new AccessToken("subject", "accessToken"); // when authService.signOut(accessToken); // then - assertThat(authTokenProvider.findBlackListToken(accessToken)).isNotNull(); + assertThat(authTokenProvider.isTokenBlacklisted(accessToken.token())).isTrue(); } @Test @@ -67,26 +61,25 @@ class AuthServiceTest { class 토큰을_재발급한다 { @Test - void 요청의_리프레시_토큰이_저장되어_있고_값이_일치면_액세스_토큰을_재발급한다() { + void 요청의_리프레시_토큰이_저장되어_있으면_액세스_토큰을_재발급한다() { // given - SiteUser siteUser = createSiteUser(); - String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); - ReissueRequest reissueRequest = new ReissueRequest(refreshToken); + RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(new Subject("subject")); + ReissueRequest reissueRequest = new ReissueRequest(refreshToken.token()); // when ReissueResponse reissuedAccessToken = authService.reissue(reissueRequest); - // then - String actualSubject = JwtUtils.parseSubject(reissuedAccessToken.accessToken(), jwtProperties.secret()); - String expectedSubject = JwtUtils.parseSubject(refreshToken, jwtProperties.secret()); + // then - 요청의 리프레시 토큰과 재발급한 액세스 토큰의 subject 가 동일해야 한다. + Subject expectedSubject = authTokenProvider.parseSubject(refreshToken.token()); + Subject actualSubject = authTokenProvider.parseSubject(reissuedAccessToken.accessToken()); assertThat(actualSubject).isEqualTo(expectedSubject); } @Test void 요청의_리프레시_토큰이_저장되어있지_않다면_예외_응답을_반환한다() { // given - String refreshToken = authTokenProvider.generateToken("subject", TokenType.REFRESH); - ReissueRequest reissueRequest = new ReissueRequest(refreshToken); + String invalidRefreshToken = authTokenProvider.generateAccessToken(new Subject("subject")).token(); + ReissueRequest reissueRequest = new ReissueRequest(invalidRefreshToken); // when, then assertThatCode(() -> authService.reissue(reissueRequest)) From dbf5d72a1a9e7952fd293be395b39338c6cb6ae7 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 5 May 2025 05:03:19 +0900 Subject: [PATCH 07/12] =?UTF-8?q?refactor:=20=EC=9D=B8=ED=84=B0=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=EB=A1=9C=20DIP=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/auth/service/BlacklistChecker.java | 6 ++++++ .../custom/security/filter/SignOutCheckFilter.java | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/auth/service/BlacklistChecker.java diff --git a/src/main/java/com/example/solidconnection/auth/service/BlacklistChecker.java b/src/main/java/com/example/solidconnection/auth/service/BlacklistChecker.java new file mode 100644 index 000000000..b4e174906 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/BlacklistChecker.java @@ -0,0 +1,6 @@ +package com.example.solidconnection.auth.service; + +public interface BlacklistChecker { + + boolean isTokenBlacklisted(String token); +} diff --git a/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java b/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java index 2cef8d1ac..a111a7292 100644 --- a/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java +++ b/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java @@ -1,6 +1,6 @@ package com.example.solidconnection.custom.security.filter; -import com.example.solidconnection.auth.service.AuthTokenProvider; +import com.example.solidconnection.auth.service.BlacklistChecker; import com.example.solidconnection.custom.exception.CustomException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -20,7 +20,7 @@ @RequiredArgsConstructor public class SignOutCheckFilter extends OncePerRequestFilter { - private final AuthTokenProvider authTokenProvider; + private final BlacklistChecker tokenBlacklistChecker; @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @@ -34,6 +34,6 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, } private boolean hasSignedOut(String accessToken) { - return authTokenProvider.findBlackListToken(accessToken).isPresent(); + return tokenBlacklistChecker.isTokenBlacklisted(accessToken); } } From b09c5841cd67993f6f3d378e46ea0ef516a1b81c Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 5 May 2025 05:11:39 +0900 Subject: [PATCH 08/12] =?UTF-8?q?test:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=ED=86=A0=ED=81=B0=20=EC=A1=B0=ED=9A=8C=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/SignInServiceTest.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java index 6136bbee2..e40f20b7a 100644 --- a/src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.auth.service; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.config.security.JwtProperties; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -12,9 +13,9 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; import java.time.LocalDate; -import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -35,6 +36,9 @@ class SignInServiceTest { @Autowired private SiteUserRepository siteUserRepository; + @Autowired + private RedisTemplate redisTemplate; + private SiteUser siteUser; private String subject; @@ -53,11 +57,11 @@ void setUp() { // then String accessTokenSubject = JwtUtils.parseSubject(signInResponse.accessToken(), jwtProperties.secret()); String refreshTokenSubject = JwtUtils.parseSubject(signInResponse.refreshToken(), jwtProperties.secret()); - Optional savedRefreshToken = authTokenProvider.findRefreshToken(subject); + String savedRefreshToken = redisTemplate.opsForValue().get(TokenType.REFRESH.addPrefix(refreshTokenSubject)); assertAll( () -> assertThat(accessTokenSubject).isEqualTo(subject), () -> assertThat(refreshTokenSubject).isEqualTo(subject), - () -> assertThat(savedRefreshToken).hasValue(signInResponse.refreshToken())); + () -> assertThat(savedRefreshToken).isEqualTo(signInResponse.refreshToken())); } @Test From dd915374eba91fe3a6de74aa99a6996462b1bb41 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 5 May 2025 22:13:29 +0900 Subject: [PATCH 09/12] =?UTF-8?q?test:=20=EB=B8=94=EB=9E=99=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A0=80=EC=9E=A5=20=EB=B0=A9=EB=B2=95=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../custom/security/filter/SignOutCheckFilterTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java b/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java index a11d8d28a..9278fec5c 100644 --- a/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java +++ b/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java @@ -59,8 +59,8 @@ void setUp() { // given String token = createToken(subject); request = createRequest(token); - String refreshTokenKey = BLACKLIST.addPrefix(token); - redisTemplate.opsForValue().set(refreshTokenKey, "signOut"); + String refreshTokenKey = BLACKLIST.addPrefix(subject); + redisTemplate.opsForValue().set(refreshTokenKey, token); // when & then assertThatCode(() -> signOutCheckFilter.doFilterInternal(request, response, filterChain)) From 29ffd67a55010d1201a65c27abd8c302f44a916c Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 5 May 2025 22:17:26 +0900 Subject: [PATCH 10/12] =?UTF-8?q?test:=20=EA=B9=A8=EC=A7=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/solidconnection/auth/service/AuthServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java index 206b43872..537a2bf61 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java @@ -35,7 +35,7 @@ class AuthServiceTest { @Test void 로그아웃한다() { // given - AccessToken accessToken = new AccessToken("subject", "accessToken"); + AccessToken accessToken = authTokenProvider.generateAccessToken(new Subject("subject")); // todo: #296 // when authService.signOut(accessToken); From afd7db5cfa1f47494d6bc1c9fe04baa7a505f70d Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 7 May 2025 02:59:04 +0900 Subject: [PATCH 11/12] =?UTF-8?q?refactor:=20=EC=97=91=EC=84=B8=EC=8A=A4?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=20=EC=9E=90=EC=B2=B4=EB=A5=BC=20=EB=B8=94?= =?UTF-8?q?=EB=9E=99=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A1=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=ED=95=98=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AuthTokenProvider.java | 18 ++++++------------ .../auth/service/AuthTokenProviderTest.java | 4 ++-- .../filter/SignOutCheckFilterTest.java | 4 ++-- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java index 294cd3ff0..032b2c537 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java @@ -29,11 +29,12 @@ public RefreshToken generateAndSaveRefreshToken(Subject subject) { /* * 액세스 토큰을 블랙리스트에 저장한다. - * - key = BLACKLIST:{subject} - * - value = {accessToken} + * - key = BLACKLIST:{accessToken} + * - value = "signOut" -> key 의 존재만 확인하므로, value 에는 무슨 값이 들어가도 상관없다. * */ public void addToBlacklist(AccessToken accessToken) { - saveToken(accessToken.token(), TokenType.BLACKLIST); + String blackListKey = TokenType.BLACKLIST.addPrefix(accessToken.token()); + redisTemplate.opsForValue().set(blackListKey, "signOut"); } /* @@ -48,17 +49,10 @@ public boolean isValidRefreshToken(String requestedRefreshToken) { return Objects.equals(requestedRefreshToken, foundRefreshToken); } - /* - * 블랙리스트에 등록된 토큰인지 확인한다. - * - 액세스 토큰의 subject 에 해당하는 블랙리스트 토큰을 조회한다. - * - 조회된 블랙리스트 토큰과 요청된 액세스 토큰이 같은지 비교한다. - */ @Override public boolean isTokenBlacklisted(String accessToken) { - String subject = JwtUtils.parseSubject(accessToken, jwtProperties.secret()); - String blackListTokenKey = TokenType.BLACKLIST.addPrefix(subject); - String foundBlackListToken = redisTemplate.opsForValue().get(blackListTokenKey); - return Objects.equals(accessToken, foundBlackListToken); + String blackListTokenKey = TokenType.BLACKLIST.addPrefix(accessToken); + return redisTemplate.hasKey(blackListTokenKey); } public Subject parseSubject(String token) { diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java index e3589cc37..2a0bd9b73 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java @@ -83,9 +83,9 @@ class 블랙리스트를_관리한다 { authTokenProvider.addToBlacklist(accessToken); // then - String blackListTokenKey = TokenType.BLACKLIST.addPrefix(subject.value()); + String blackListTokenKey = TokenType.BLACKLIST.addPrefix(accessToken.token()); String foundBlackListToken = redisTemplate.opsForValue().get(blackListTokenKey); - assertThat(foundBlackListToken).isEqualTo(accessToken.token()); + assertThat(foundBlackListToken).isNotNull(); } /* diff --git a/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java b/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java index 9278fec5c..e76a01c75 100644 --- a/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java +++ b/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java @@ -55,11 +55,11 @@ void setUp() { } @Test - void 로그아웃한_토큰이면_예외를_응답한다() throws Exception { + void 로그아웃한_토큰이면_예외를_응답한다() { // given String token = createToken(subject); request = createRequest(token); - String refreshTokenKey = BLACKLIST.addPrefix(subject); + String refreshTokenKey = BLACKLIST.addPrefix(token); redisTemplate.opsForValue().set(refreshTokenKey, token); // when & then From 07948783159f8f3e8e4f8b6ddcb33a3d21a21c62 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 7 May 2025 21:16:42 +0900 Subject: [PATCH 12/12] =?UTF-8?q?refactor:=20AuthController=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9D=B8=EC=A6=9D=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?String=EC=9C=BC=EB=A1=9C=20=EB=B0=9B=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/auth/controller/AuthController.java | 4 ++-- .../com/example/solidconnection/auth/service/AuthService.java | 3 ++- .../solidconnection/auth/service/AuthTokenProvider.java | 4 ++++ .../example/solidconnection/auth/service/AuthServiceTest.java | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 056957d21..c4b9127d5 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -9,7 +9,6 @@ import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; import com.example.solidconnection.auth.dto.oauth.OAuthResponse; -import com.example.solidconnection.auth.service.AccessToken; import com.example.solidconnection.auth.service.AuthService; import com.example.solidconnection.auth.service.CommonSignUpTokenProvider; import com.example.solidconnection.auth.service.EmailSignInService; @@ -98,7 +97,8 @@ public ResponseEntity signUp( public ResponseEntity signOut( Authentication authentication ) { - if (!(authentication.getCredentials() instanceof AccessToken accessToken)) { // null or AccessToken 로 형변환 실패 + String accessToken = (String) authentication.getCredentials(); + if (accessToken == null || accessToken.isBlank()) { throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다."); } authService.signOut(accessToken); diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index 86bb9045a..86220d863 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -22,7 +22,8 @@ public class AuthService { * 로그아웃 한다. * - 엑세스 토큰을 블랙리스트에 추가한다. * */ - public void signOut(AccessToken accessToken) { + public void signOut(String token) { + AccessToken accessToken = authTokenProvider.toAccessToken(token); authTokenProvider.addToBlacklist(accessToken); } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java index 032b2c537..d44f3bc8f 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java @@ -63,4 +63,8 @@ public Subject parseSubject(String token) { public Subject toSubject(SiteUser siteUser) { return new Subject(siteUser.getId().toString()); } + + public AccessToken toAccessToken(String token) { + return new AccessToken(parseSubject(token), token); + } } diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java index 537a2bf61..759c90d88 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java @@ -38,7 +38,7 @@ class AuthServiceTest { AccessToken accessToken = authTokenProvider.generateAccessToken(new Subject("subject")); // todo: #296 // when - authService.signOut(accessToken); + authService.signOut(accessToken.token()); // then assertThat(authTokenProvider.isTokenBlacklisted(accessToken.token())).isTrue();