From 9737e11c7d66ae6f75fd33303857f89bf19e422d Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 23 Jan 2025 23:51:06 +0900 Subject: [PATCH 1/8] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=EB=8B=A4=EC=96=91=ED=95=9C=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=9C=A0=ED=98=95=EC=9D=84=20=EA=B0=80=EC=A7=80=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../siteuser/domain/AuthType.java | 9 +++ .../siteuser/domain/SiteUser.java | 34 +++++++++- ...3__add_auth_type_column_and_unique_key.sql | 13 ++++ .../repository/SiteUserRepositoryTest.java | 62 +++++++++++++++++++ .../support/TestContainerSpringBootTest.java | 2 + 5 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java create mode 100644 src/main/resources/db/migration/V3__add_auth_type_column_and_unique_key.sql create mode 100644 src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java b/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java new file mode 100644 index 000000000..d9d0b582c --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.siteuser.domain; + +public enum AuthType { + + KAKAO, + APPLE, + EMAIL, + ; +} diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index e518a5efb..2c2a5d8be 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -17,6 +17,8 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @@ -32,15 +34,25 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @AllArgsConstructor +@Table(uniqueConstraints = { + @UniqueConstraint( + name = "uk_site_user_email_auth_type", + columnNames = {"email", "auth_type"} + ) +}) public class SiteUser { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false, length = 100) + @Column(name = "email", nullable = false, length = 100) private String email; + @Column(name = "auth_type", nullable = false, length = 100) + @Enumerated(EnumType.STRING) + private AuthType authType; + @Setter @Column(nullable = false, length = 100) private String nickname; @@ -100,5 +112,25 @@ public SiteUser( this.preparationStage = preparationStage; this.role = role; this.gender = gender; + this.authType = AuthType.KAKAO; + } + + public SiteUser( + String email, + String nickname, + String profileImageUrl, + String birth, + PreparationStatus preparationStage, + Role role, + Gender gender, + AuthType authType) { + this.email = email; + this.nickname = nickname; + this.profileImageUrl = profileImageUrl; + this.birth = birth; + this.preparationStage = preparationStage; + this.role = role; + this.gender = gender; + this.authType = authType; } } diff --git a/src/main/resources/db/migration/V3__add_auth_type_column_and_unique_key.sql b/src/main/resources/db/migration/V3__add_auth_type_column_and_unique_key.sql new file mode 100644 index 000000000..e89c4aa1b --- /dev/null +++ b/src/main/resources/db/migration/V3__add_auth_type_column_and_unique_key.sql @@ -0,0 +1,13 @@ +ALTER TABLE site_user +ADD COLUMN auth_type ENUM('KAKAO', 'APPLE', 'EMAIL'); + +UPDATE site_user +SET auth_type = 'KAKAO' +WHERE auth_type IS NULL; + +ALTER TABLE site_user +MODIFY COLUMN auth_type ENUM('KAKAO', 'APPLE', 'EMAIL') NOT NULL; + +ALTER TABLE site_user +ADD CONSTRAINT uk_site_user_email_auth_type +UNIQUE (email, auth_type); diff --git a/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java b/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java new file mode 100644 index 000000000..d3433937a --- /dev/null +++ b/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java @@ -0,0 +1,62 @@ +package com.example.solidconnection.siteuser.repository; + +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.support.TestContainerDataJpaTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; + +import static org.assertj.core.api.Assertions.assertThatCode; + +@TestContainerDataJpaTest +class SiteUserRepositoryTest { + + @Autowired + private SiteUserRepository siteUserRepository; + + @Nested + class 이메일과_인증_유형이_동일한_사용자는_저장할_수_없다 { + + @Test + void 이메일과_인증_유형이_동일한_사용자를_저장하면_예외_응답을_반환한다() { + // given + SiteUser user1 = createSiteUser("email", AuthType.KAKAO); + SiteUser user2 = createSiteUser("email", AuthType.KAKAO); + siteUserRepository.save(user1); + + // when, then + assertThatCode(() -> siteUserRepository.save(user2)) + .isInstanceOf(DataIntegrityViolationException.class); + } + + @Test + void 이메일이_같더라도_인증_유형이_다른_사용자는_정상_저장한다() { + // given + SiteUser user1 = createSiteUser("email", AuthType.KAKAO); + SiteUser user2 = createSiteUser("email", AuthType.APPLE); + siteUserRepository.save(user1); + + // when, then + assertThatCode(() -> siteUserRepository.save(user2)) + .doesNotThrowAnyException(); + } + } + + private SiteUser createSiteUser(String email, AuthType authType) { + return new SiteUser( + email, + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE, + authType + ); + } +} diff --git a/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java b/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java index bcb110c6b..fe9b74f60 100644 --- a/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java +++ b/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.support; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; @@ -11,6 +12,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +@ExtendWith({DatabaseClearExtension.class}) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @ActiveProfiles("test") From d843569dda5979898ed7c12c75d451b546f80429 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 27 Jan 2025 03:25:18 +0900 Subject: [PATCH 2/8] =?UTF-8?q?refactor:=20=ED=95=A8=EC=88=98=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 더 의미를 전달하도록 --- .../auth/service/TokenProvider.java | 6 +-- .../security/JwtAuthenticationFilter.java | 4 +- .../config/security/SignOutCheckFilter.java | 4 +- .../solidconnection/util/JwtUtils.java | 8 ++-- .../solidconnection/util/JwtUtilsTest.java | 44 ++++++++++++++----- 5 files changed, 46 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java index 693a968ea..9cba77c36 100644 --- a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java @@ -12,8 +12,8 @@ import java.util.Date; import java.util.concurrent.TimeUnit; +import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; import static com.example.solidconnection.util.JwtUtils.parseSubject; -import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; @RequiredArgsConstructor @Component @@ -35,7 +35,7 @@ public String generateToken(String email, TokenType tokenType) { } public String saveToken(String token, TokenType tokenType) { - String subject = parseSubjectOrElseThrow(token, jwtProperties.secret()); + String subject = parseSubject(token, jwtProperties.secret()); redisTemplate.opsForValue().set( tokenType.addPrefixToSubject(subject), token, @@ -46,6 +46,6 @@ public String saveToken(String token, TokenType tokenType) { } public String getEmail(String token) { - return parseSubject(token, jwtProperties.secret()); + return parseSubjectIgnoringExpiration(token, jwtProperties.secret()); } } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index e01009be1..72cea43d2 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -16,7 +16,7 @@ import java.io.IOException; -import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; +import static com.example.solidconnection.util.JwtUtils.parseSubject; import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; @Component @@ -40,7 +40,7 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, } try { - String subject = parseSubjectOrElseThrow(token, jwtProperties.secret()); + String subject = parseSubject(token, jwtProperties.secret()); UserDetails userDetails = new JwtUserDetails(subject); Authentication auth = new JwtAuthentication(userDetails, token, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); diff --git a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java index 3c1218d13..3fc8c6a01 100644 --- a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java @@ -16,7 +16,7 @@ import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.auth.service.AuthService.SIGN_OUT_VALUE; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; -import static com.example.solidconnection.util.JwtUtils.parseSubject; +import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; @Component @@ -41,7 +41,7 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, } private boolean isSignOut(String accessToken) { - String subject = parseSubject(accessToken, jwtProperties.secret()); + String subject = parseSubjectIgnoringExpiration(accessToken, jwtProperties.secret()); String refreshToken = REFRESH.addPrefixToSubject(subject); return SIGN_OUT_VALUE.equals(redisTemplate.opsForValue().get(refreshToken)); } diff --git a/src/main/java/com/example/solidconnection/util/JwtUtils.java b/src/main/java/com/example/solidconnection/util/JwtUtils.java index a3775365d..566bd7ff5 100644 --- a/src/main/java/com/example/solidconnection/util/JwtUtils.java +++ b/src/main/java/com/example/solidconnection/util/JwtUtils.java @@ -25,18 +25,20 @@ public static String parseTokenFromRequest(HttpServletRequest request) { return token.substring(TOKEN_PREFIX.length()); } - public static String parseSubject(String token, String secretKey) { + public static String parseSubjectIgnoringExpiration(String token, String secretKey) { try { return extractSubject(token, secretKey); } catch (ExpiredJwtException e) { return e.getClaims().getSubject(); + } catch (Exception e) { + throw new CustomException(INVALID_TOKEN); } } - public static String parseSubjectOrElseThrow(String token, String secretKey) { + public static String parseSubject(String token, String secretKey) { try { return extractSubject(token, secretKey); - } catch (ExpiredJwtException e) { + } catch (Exception e) { throw new CustomException(INVALID_TOKEN); } } diff --git a/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java b/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java index 4dfc11540..7eedd88a6 100644 --- a/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java +++ b/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java @@ -12,7 +12,7 @@ import java.util.Date; import static com.example.solidconnection.util.JwtUtils.parseSubject; -import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; +import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -59,7 +59,7 @@ class 요청으로부터_토큰을_추출한다 { } @Nested - class 토큰으로부터_subject_를_추출한다 { + class 유효한_토큰으로부터_subject_를_추출한다 { @Test void 유효한_토큰의_subject_를_추출한다() { @@ -75,13 +75,29 @@ class 토큰으로부터_subject_를_추출한다 { } @Test - void 유효하지_않은_토큰의_subject_를_추출한다() { + void 유효하지_않은_토큰의_subject_를_추출하면_예외_응답을_반환한다() { + // given + String subject = "subject123"; + String token = createExpiredToken(subject); + + // when + assertThatCode(() -> parseSubject(token, jwtSecretKey)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } + } + + @Nested + class 만료된_토큰으로부터_subject_를_추출한다 { + + @Test + void 만료된_토큰의_subject_를_예외를_발생시키지_않고_추출한다() { // given String subject = "subject999"; - String token = createInvalidToken(subject); + String token = createExpiredToken(subject); // when - String extractedSubject = parseSubject(token, jwtSecretKey); + String extractedSubject = parseSubjectIgnoringExpiration(token, jwtSecretKey); // then assertThat(extractedSubject).isEqualTo(subject); @@ -90,11 +106,10 @@ class 토큰으로부터_subject_를_추출한다 { @Test void 유효하지_않은_토큰의_subject_를_추출하면_예외_응답을_반환한다() { // given - String subject = "subject123"; - String token = createInvalidToken(subject); + String token = createExpiredUnsignedToken("hackers secret key"); - // when - assertThatCode(() -> parseSubjectOrElseThrow(token, jwtSecretKey)) + // when & then + assertThatCode(() -> parseSubjectIgnoringExpiration(token, jwtSecretKey)) .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); } @@ -109,7 +124,7 @@ private String createValidToken(String subject) { .compact(); } - private String createInvalidToken(String subject) { + private String createExpiredToken(String subject) { return Jwts.builder() .setSubject(subject) .setIssuedAt(new Date()) @@ -117,4 +132,13 @@ private String createInvalidToken(String subject) { .signWith(SignatureAlgorithm.HS256, jwtSecretKey) .compact(); } + + private String createExpiredUnsignedToken(String jwtSecretKey) { + return Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtSecretKey) + .compact(); + } } From 72eabba5ed081310697b2016086603ec4e45fbb9 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 27 Jan 2025 03:25:32 +0900 Subject: [PATCH 3/8] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{config/token => auth/service}/TokenProviderTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename src/test/java/com/example/solidconnection/{config/token => auth/service}/TokenProviderTest.java (96%) diff --git a/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java similarity index 96% rename from src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java rename to src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java index d3992a33a..8cc91e2c0 100644 --- a/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java @@ -1,7 +1,6 @@ -package com.example.solidconnection.config.token; +package com.example.solidconnection.auth.service; import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.config.security.JwtProperties; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.exception.ErrorCode; From be5ca5e78336f9f54b32387bbf006836af3a39a1 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 27 Jan 2025 03:46:12 +0900 Subject: [PATCH 4/8] =?UTF-8?q?refactor:=20EntryPoint=20=EB=A7=90=EA=B3=A0?= =?UTF-8?q?=20=ED=95=84=ED=84=B0=EB=A1=9C=20=ED=95=84=ED=84=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20=EB=AA=A8?= =?UTF-8?q?=EB=93=A0=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...Point.java => ExceptionHandlerFilter.java} | 33 ++++--- .../security/JwtAuthenticationFilter.java | 21 +---- .../security/SecurityConfiguration.java | 6 +- .../config/security/SignOutCheckFilter.java | 11 +-- .../security/ExceptionHandlerFilterTest.java | 91 +++++++++++++++++++ .../security/JwtAuthenticationFilterTest.java | 23 +++-- .../security/SignOutCheckFilterTest.java | 12 +-- 7 files changed, 143 insertions(+), 54 deletions(-) rename src/main/java/com/example/solidconnection/config/security/{JwtAuthenticationEntryPoint.java => ExceptionHandlerFilter.java} (63%) create mode 100644 src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java similarity index 63% rename from src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java rename to src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java index 7487858be..59022c198 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java @@ -3,12 +3,15 @@ import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.response.ErrorResponse; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @@ -16,24 +19,32 @@ @Component @RequiredArgsConstructor -public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { +public class ExceptionHandlerFilter extends OncePerRequestFilter { private final ObjectMapper objectMapper; @Override - public void commence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException { - ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, authException.getMessage()); - writeResponse(response, errorResponse); + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (CustomException e) { + customCommence(response, e); + } catch (Exception e) { + generalCommence(response, e); + } } - public void generalCommence(HttpServletResponse response, Exception exception) throws IOException { - ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, exception.getMessage()); + public void customCommence(HttpServletResponse response, CustomException customException) throws IOException { + SecurityContextHolder.clearContext(); + ErrorResponse errorResponse = new ErrorResponse(customException); writeResponse(response, errorResponse); } - public void customCommence(HttpServletResponse response, CustomException customException) throws IOException { - ErrorResponse errorResponse = new ErrorResponse(customException); + public void generalCommence(HttpServletResponse response, Exception exception) throws IOException { + SecurityContextHolder.clearContext(); + ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, exception.getMessage()); writeResponse(response, errorResponse); } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index 72cea43d2..5c7ab9f97 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -1,6 +1,5 @@ package com.example.solidconnection.config.security; -import com.example.solidconnection.custom.exception.CustomException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -8,7 +7,6 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; @@ -27,7 +25,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private static final String REISSUE_METHOD = "post"; private final JwtProperties jwtProperties; - private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @@ -39,19 +36,11 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, return; } - try { - String subject = parseSubject(token, jwtProperties.secret()); - UserDetails userDetails = new JwtUserDetails(subject); - Authentication auth = new JwtAuthentication(userDetails, token, userDetails.getAuthorities()); - SecurityContextHolder.getContext().setAuthentication(auth); - filterChain.doFilter(request, response); - } catch (AuthenticationException e) { - jwtAuthenticationEntryPoint.commence(request, response, e); - } catch (CustomException e) { - jwtAuthenticationEntryPoint.customCommence(response, e); - } catch (Exception e) { - jwtAuthenticationEntryPoint.generalCommence(response, e); - } + String subject = parseSubject(token, jwtProperties.secret()); + UserDetails userDetails = new JwtUserDetails(subject); + Authentication auth = new JwtAuthentication(userDetails, token, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(auth); + filterChain.doFilter(request, response); } private boolean isReissueRequest(HttpServletRequest request) { diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index d28d883ca..3f6307f8f 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -19,6 +19,7 @@ public class SecurityConfiguration { private final CorsProperties corsProperties; + private final ExceptionHandlerFilter exceptionHandlerFilter; private final SignOutCheckFilter signOutCheckFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter; @@ -45,8 +46,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) - .addFilterBefore(this.jwtAuthenticationFilter, BasicAuthenticationFilter.class) - .addFilterBefore(this.signOutCheckFilter, JwtAuthenticationFilter.class) + .addFilterBefore(jwtAuthenticationFilter, BasicAuthenticationFilter.class) + .addFilterBefore(signOutCheckFilter, JwtAuthenticationFilter.class) + .addFilterBefore(exceptionHandlerFilter, SignOutCheckFilter.class) .build(); } } diff --git a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java index 3fc8c6a01..a7509e667 100644 --- a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java @@ -25,22 +25,19 @@ public class SignOutCheckFilter extends OncePerRequestFilter { private final RedisTemplate redisTemplate; private final JwtProperties jwtProperties; - private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { String token = parseTokenFromRequest(request); - if (token == null || !isSignOut(token)) { - filterChain.doFilter(request, response); - return; + if (token != null && hasSignedOut(token)) { + throw new CustomException(USER_ALREADY_SIGN_OUT); } - - jwtAuthenticationEntryPoint.customCommence(response, new CustomException(USER_ALREADY_SIGN_OUT)); + filterChain.doFilter(request, response); } - private boolean isSignOut(String accessToken) { + private boolean hasSignedOut(String accessToken) { String subject = parseSubjectIgnoringExpiration(accessToken, jwtProperties.secret()); String refreshToken = REFRESH.addPrefixToSubject(subject); return SIGN_OUT_VALUE.equals(redisTemplate.opsForValue().get(refreshToken)); diff --git a/src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java b/src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java new file mode 100644 index 000000000..f4e8dc666 --- /dev/null +++ b/src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java @@ -0,0 +1,91 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +@TestContainerSpringBootTest +class ExceptionHandlerFilterTest { + + @Autowired + private ExceptionHandlerFilter exceptionHandlerFilter; + + private HttpServletRequest request; + private HttpServletResponse response; + private FilterChain filterChain; + + @BeforeEach() + void setUp() { + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + filterChain = spy(FilterChain.class); + } + + @Test + void 필터_체인에서_예외가_발생하면_SecurityContext_를_초기화한다() throws Exception { + // given + Authentication authentication = mock(TestingAuthenticationToken.class); + SecurityContextHolder.getContext().setAuthentication(authentication); + willThrow(new RuntimeException()).given(filterChain).doFilter(request, response); + + // when + exceptionHandlerFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + } + + @Test + void 필터_체인에서_예외가_발생하지_않으면_다음_필터로_진행한다() throws Exception { + // given + willDoNothing().given(filterChain).doFilter(request, response); + + // when + exceptionHandlerFilter.doFilterInternal(request, response, filterChain); + + // then + then(filterChain).should().doFilter(request, response); + } + + @ParameterizedTest + @MethodSource("provideException") + void 필터_체인에서_예외가_발생하면_예외_응답을_반환한다(Throwable throwable) throws Exception { + // given + willThrow(throwable).given(filterChain).doFilter(request, response); + + // when + exceptionHandlerFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + + private static Stream provideException() { + return Stream.of( + new RuntimeException(), + new CustomException(ErrorCode.INVALID_TOKEN) + ); + } +} diff --git a/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java b/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java index c0256f75a..16e3639f1 100644 --- a/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java +++ b/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.config.security; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.support.TestContainerSpringBootTest; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -17,7 +18,9 @@ import java.util.Date; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.spy; @@ -89,12 +92,10 @@ class 유효하지_않은_토큰으로_인증하면_예외를_응답한다 { .compact(); request = createRequestWithToken(token); - // when - jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + // when & then + assertThatCode(() -> jwtAuthenticationFilter.doFilterInternal(request, response, filterChain)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_TOKEN.getMessage()); then(filterChain).shouldHaveNoMoreInteractions(); } @@ -109,12 +110,10 @@ class 유효하지_않은_토큰으로_인증하면_예외를_응답한다 { .compact(); request = createRequestWithToken(token); - // when - jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + // when & then + assertThatCode(() -> jwtAuthenticationFilter.doFilterInternal(request, response, filterChain)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_TOKEN.getMessage()); then(filterChain).shouldHaveNoMoreInteractions(); } } diff --git a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java b/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java index 13544152b..6f0ec1d11 100644 --- a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java +++ b/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.config.security; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.support.TestContainerSpringBootTest; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -19,7 +20,7 @@ import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.spy; @@ -59,11 +60,10 @@ void setUp() { String refreshTokenKey = REFRESH.addPrefixToSubject(subject); redisTemplate.opsForValue().set(refreshTokenKey, "signOut"); - // when - signOutCheckFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(response.getStatus()).isEqualTo(USER_ALREADY_SIGN_OUT.getCode()); + // when & then + assertThatCode(() -> signOutCheckFilter.doFilterInternal(request, response, filterChain)) + .isInstanceOf(CustomException.class) + .hasMessage(USER_ALREADY_SIGN_OUT.getMessage()); then(filterChain).shouldHaveNoMoreInteractions(); } From 4676e999e679574273ffad4ee6747132297dc23a Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 27 Jan 2025 04:15:09 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EA=B2=80=EC=82=AC=20=ED=95=A8=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/util/JwtUtils.java | 15 +++++++ .../solidconnection/util/JwtUtilsTest.java | 41 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/main/java/com/example/solidconnection/util/JwtUtils.java b/src/main/java/com/example/solidconnection/util/JwtUtils.java index 566bd7ff5..3a1b58520 100644 --- a/src/main/java/com/example/solidconnection/util/JwtUtils.java +++ b/src/main/java/com/example/solidconnection/util/JwtUtils.java @@ -6,6 +6,8 @@ import jakarta.servlet.http.HttpServletRequest; import org.springframework.stereotype.Component; +import java.util.Date; + import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; @Component @@ -43,6 +45,19 @@ public static String parseSubject(String token, String secretKey) { } } + public static boolean isExpired(String token, String secretKey) { + try { + Date expiration = Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody() + .getExpiration(); + return expiration.before(new Date()); + } catch (Exception e) { + return true; + } + } + private static String extractSubject(String token, String secretKey) throws ExpiredJwtException { return Jwts.parser() .setSigningKey(secretKey) diff --git a/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java b/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java index 7eedd88a6..95bdd5a52 100644 --- a/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java +++ b/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java @@ -115,6 +115,47 @@ class 만료된_토큰으로부터_subject_를_추출한다 { } } + + @Nested + class 토큰이_만료되었는지_확인한다 { + + @Test + void 서명된_토큰의_만료_여부를_반환한다() { + // given + String subject = "subject123"; + String validToken = createValidToken(subject); + String expiredToken = createExpiredToken(subject); + + // when + boolean isExpired1 = JwtUtils.isExpired(validToken, jwtSecretKey); + boolean isExpired2 = JwtUtils.isExpired(expiredToken, jwtSecretKey); + + // then + assertAll( + () -> assertThat(isExpired1).isFalse(), + () -> assertThat(isExpired2).isTrue() + ); + } + + @Test + void 서명되지_않은_토큰의_만료_여부를_반환한다() { + // given + String subject = "subject123"; + String validToken = createValidToken(subject); + String expiredToken = createExpiredToken(subject); + + // when + boolean isExpired1 = JwtUtils.isExpired(validToken, "wrong-secret-key"); + boolean isExpired2 = JwtUtils.isExpired(expiredToken, "wrong-secret-key"); + + // then + assertAll( + () -> assertThat(isExpired1).isTrue(), + () -> assertThat(isExpired2).isTrue() + ); + } + } + private String createValidToken(String subject) { return Jwts.builder() .setSubject(subject) From d87c5ba877351f177dff8133f27472f1c56c4d83 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 27 Jan 2025 04:22:22 +0900 Subject: [PATCH 6/8] =?UTF-8?q?test:=20=EA=B9=A8=EC=A7=80=EB=8A=94=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unit/repository/PostRepositoryTest.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java index 42da9de22..a37a0e6bf 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java @@ -17,7 +17,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; @@ -30,10 +29,13 @@ @TestContainerDataJpaTest @DisplayName("게시글 레포지토리 테스트") class PostRepositoryTest { + @Autowired private PostRepository postRepository; + @Autowired private BoardRepository boardRepository; + @Autowired private SiteUserRepository siteUserRepository; @@ -89,7 +91,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다() { Post foundPost = postRepository.getByIdUsingEntityGraph(post.getId()); foundPost.getPostImageList().size(); // 추가쿼리 발생하지 않는다. @@ -98,7 +99,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다_유효한_게시글이_아니라면_예외_응답을_반환한다() { // given Long invalidId = -1L; @@ -114,7 +114,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글을_조회한다() { Post foundPost = postRepository.getById(post.getId()); @@ -122,7 +121,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글을_조회할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { Long invalidId = -1L; @@ -136,7 +134,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글_좋아요를_등록한다() { // given Long likeCount = post.getLikeCount(); @@ -150,7 +147,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글_좋아요를_삭제한다() { // given Long likeCount = post.getLikeCount(); From d8eac22563a0f6b97f2cc7164856d1193351afb1 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 27 Jan 2025 11:10:51 +0900 Subject: [PATCH 7/8] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/domain/TokenType.java | 4 +++- .../auth/service/AuthService.java | 20 +++++++++---------- .../config/security/SignOutCheckFilter.java | 9 +++------ 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java index 7fa6045f7..ad5607a27 100644 --- a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java +++ b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java @@ -7,7 +7,9 @@ public enum TokenType { ACCESS("ACCESS:", 1000 * 60 * 60), // 1hour REFRESH("REFRESH:", 1000 * 60 * 60 * 24 * 7), // 7days - KAKAO_OAUTH("KAKAO:", 1000 * 60 * 60); // 1hour + KAKAO_OAUTH("KAKAO:", 1000 * 60 * 60), // 1hour + BLACKLIST("BLACKLIST:", ACCESS.expireTime) + ; private final String prefix; private final int expireTime; 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 29fb1b347..e16044e97 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -15,6 +15,7 @@ import java.util.concurrent.TimeUnit; import static com.example.solidconnection.auth.domain.TokenType.ACCESS; +import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; @@ -30,16 +31,13 @@ public class AuthService { /* * 로그아웃 한다. - * - 리프레시 토큰을 무효화하기 위해 리프레시 토큰의 value 를 변경한다. - * - 어떤 사용자가 엑세스 토큰으로 인증이 필요한 기능을 사용하려 할 때, 로그아웃 검증이 진행되는데, - * - 이때 리프레시 토큰의 value 가 SIGN_OUT_VALUE 이면 예외 응답이 반환된다. - * - (TokenValidator.validateNotSignOut() 참고) + * - 엑세스 토큰을 블랙리스트에 추가한다. * */ - public void signOut(String email) { + public void signOut(String accessToken) { redisTemplate.opsForValue().set( - REFRESH.addPrefixToSubject(email), - SIGN_OUT_VALUE, - REFRESH.getExpireTime(), + BLACKLIST.addPrefixToSubject(accessToken), + accessToken, + BLACKLIST.getExpireTime(), TimeUnit.MILLISECONDS ); } @@ -61,15 +59,15 @@ public void quit(String email) { * - 리프레시 토큰이 만료되었거나, 존재하지 않는다면 예외 응답을 반환한다. * - 리프레시 토큰이 존재한다면, 액세스 토큰을 재발급한다. * */ - public ReissueResponse reissue(String email) { + public ReissueResponse reissue(String subject) { // 리프레시 토큰 만료 확인 - String refreshTokenKey = REFRESH.addPrefixToSubject(email); + String refreshTokenKey = REFRESH.addPrefixToSubject(subject); String refreshToken = redisTemplate.opsForValue().get(refreshTokenKey); if (ObjectUtils.isEmpty(refreshToken)) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } // 액세스 토큰 재발급 - String newAccessToken = tokenProvider.generateToken(email, ACCESS); + String newAccessToken = tokenProvider.generateToken(subject, ACCESS); tokenProvider.saveToken(newAccessToken, ACCESS); return new ReissueResponse(newAccessToken); } diff --git a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java index a7509e667..c71252f1f 100644 --- a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java @@ -13,10 +13,8 @@ import java.io.IOException; -import static com.example.solidconnection.auth.domain.TokenType.REFRESH; -import static com.example.solidconnection.auth.service.AuthService.SIGN_OUT_VALUE; +import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; -import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; @Component @@ -38,8 +36,7 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, } private boolean hasSignedOut(String accessToken) { - String subject = parseSubjectIgnoringExpiration(accessToken, jwtProperties.secret()); - String refreshToken = REFRESH.addPrefixToSubject(subject); - return SIGN_OUT_VALUE.equals(redisTemplate.opsForValue().get(refreshToken)); + String blacklistKey = BLACKLIST.addPrefixToSubject(accessToken); + return redisTemplate.opsForValue().get(blacklistKey) != null; } } From 126164b7c37022a47572bf87626367c0ca56ac76 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 27 Jan 2025 11:11:15 +0900 Subject: [PATCH 8/8] =?UTF-8?q?test:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=ED=95=84=ED=84=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/SignOutCheckFilterTest.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java b/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java index 6f0ec1d11..a067bf9d9 100644 --- a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java +++ b/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java @@ -18,7 +18,7 @@ import java.util.Date; import java.util.Objects; -import static com.example.solidconnection.auth.domain.TokenType.REFRESH; +import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.BDDMockito.then; @@ -56,8 +56,9 @@ void setUp() { @Test void 로그아웃한_토큰이면_예외를_응답한다() throws Exception { // given - request = createTokenRequest(subject); - String refreshTokenKey = REFRESH.addPrefixToSubject(subject); + String token = createToken(subject); + request = createRequest(token); + String refreshTokenKey = BLACKLIST.addPrefixToSubject(token); redisTemplate.opsForValue().set(refreshTokenKey, "signOut"); // when & then @@ -82,7 +83,8 @@ void setUp() { @Test void 로그아웃하지_않은_토큰이면_다음_필터로_전달한다() throws Exception { // given - request = createTokenRequest(subject); + String token = createToken(subject); + request = createRequest(token); // when signOutCheckFilter.doFilterInternal(request, response, filterChain); @@ -91,14 +93,16 @@ void setUp() { then(filterChain).should().doFilter(request, response); } - private HttpServletRequest createTokenRequest(String subject) { - String token = Jwts.builder() - .setSubject(subject) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + 1000)) - .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) - .compact(); + private String createToken(String subject) { + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + private HttpServletRequest createRequest(String token) { MockHttpServletRequest request = new MockHttpServletRequest(); request.addHeader("Authorization", "Bearer " + token); return request;