From 3d44031ea3762216015aa92d10e981410e5e2e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 17:43:21 +0900 Subject: [PATCH 01/27] refactor : Move file's directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth 디렉토리를 도메인에 추가합니다. - common/jwt/dto를 auth 도메인에 포함되도록합니다. --- .../java/com/tasksprints/auction/common/jwt/JwtProvider.java | 2 +- .../{common/jwt => domain/auth}/dto/response/JwtResponse.java | 2 +- .../com/tasksprints/auction/common/jwt/JwtProviderTest.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/main/java/com/tasksprints/auction/{common/jwt => domain/auth}/dto/response/JwtResponse.java (86%) diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java index 033e8ead..08c2f728 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java @@ -2,7 +2,7 @@ import static com.tasksprints.auction.common.util.TimeUtil.*; -import com.tasksprints.auction.common.jwt.dto.response.JwtResponse; +import com.tasksprints.auction.domain.auth.dto.response.JwtResponse; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; diff --git a/src/main/java/com/tasksprints/auction/common/jwt/dto/response/JwtResponse.java b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/JwtResponse.java similarity index 86% rename from src/main/java/com/tasksprints/auction/common/jwt/dto/response/JwtResponse.java rename to src/main/java/com/tasksprints/auction/domain/auth/dto/response/JwtResponse.java index 1d209eca..c53b08fa 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/dto/response/JwtResponse.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/JwtResponse.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.common.jwt.dto.response; +package com.tasksprints.auction.domain.auth.dto.response; import lombok.*; diff --git a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java index ea1bafc5..ab76b217 100644 --- a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java +++ b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java @@ -1,6 +1,6 @@ package com.tasksprints.auction.common.jwt; -import com.tasksprints.auction.common.jwt.dto.response.JwtResponse; +import com.tasksprints.auction.domain.auth.dto.response.JwtResponse; import io.jsonwebtoken.ExpiredJwtException; import java.time.Clock; import java.time.Instant; From 522328ff5bcd6bd11e9a237176343a4335884029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 17:46:36 +0900 Subject: [PATCH 02/27] refactor : Refactor JwtConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이름은 변경한다 - header와 prefix는 클래스에서 지정하도록 한다. --- .../jwt/{JwtProperties.java => JwtConfig.java} | 7 +------ .../auction/common/jwt/JwtProvider.java | 16 ++++++++-------- .../auction/common/jwt/JwtProviderTest.java | 10 +++++----- 3 files changed, 14 insertions(+), 19 deletions(-) rename src/main/java/com/tasksprints/auction/common/jwt/{JwtProperties.java => JwtConfig.java} (75%) diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProperties.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java similarity index 75% rename from src/main/java/com/tasksprints/auction/common/jwt/JwtProperties.java rename to src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java index e87ce1d1..676468b9 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtProperties.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java @@ -6,12 +6,7 @@ @Component @Getter -public class JwtProperties { - @Value("${jwt.header}") - private String header; - - @Value("${jwt.prefix}") - private String prefix; +public class JwtConfig { @Value("${jwt.expire-ms}") private Long expireMs; diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java index 08c2f728..d645bd37 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java @@ -17,7 +17,7 @@ @RequiredArgsConstructor public class JwtProvider { - private final JwtProperties jwtProperties; + private final JwtConfig jwtConfig; private final Clock clock; public JwtResponse generateToken(Long userId, String userRole) { @@ -28,18 +28,18 @@ public String createAccessToken(Long userId, String userRole) { Date now = localDateTimeToDate(LocalDateTime.now(clock)); - return Jwts.builder().setIssuer(jwtProperties.getIssuer()).claim("userId", userId).claim("userRole", userRole) - .setIssuedAt(now).setExpiration(new Date(now.getTime() + jwtProperties.getExpireMs())) - .signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtProperties.getSecretKey())).compact(); + return Jwts.builder().setIssuer(jwtConfig.getIssuer()).claim("userId", userId).claim("userRole", userRole) + .setIssuedAt(now).setExpiration(new Date(now.getTime() + jwtConfig.getExpireMs())) + .signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtConfig.getSecretKey())).compact(); } public String createRefreshToken() { Date now = localDateTimeToDate(LocalDateTime.now(clock)); - return Jwts.builder().setIssuer(jwtProperties.getIssuer()).setIssuedAt(now) - .setExpiration(new Date(now.getTime() + jwtProperties.getRefreshExpireMs())) - .signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtProperties.getSecretKey())).compact(); + return Jwts.builder().setIssuer(jwtConfig.getIssuer()).setIssuedAt(now) + .setExpiration(new Date(now.getTime() + jwtConfig.getRefreshExpireMs())) + .signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtConfig.getSecretKey())).compact(); } public boolean verifyToken(String token) { @@ -52,7 +52,7 @@ public boolean verifyToken(String token) { } public Claims getClaims(String token) { - return Jwts.parser().setSigningKey(JwtUtil.encodeSecretKey(jwtProperties.getSecretKey())) + return Jwts.parser().setSigningKey(JwtUtil.encodeSecretKey(jwtConfig.getSecretKey())) .parseClaimsJws(token) .getBody(); } diff --git a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java index ab76b217..f1c6a24b 100644 --- a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java +++ b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java @@ -21,7 +21,7 @@ @ExtendWith(MockitoExtension.class) class JwtProviderTest { @Mock - private JwtProperties jwtProperties; + private JwtConfig jwtConfig; @Mock private Clock clock; @InjectMocks @@ -38,16 +38,16 @@ class JwtProviderTest { public void setUp() { when(clock.instant()).thenReturn(Instant.now()); when(clock.getZone()).thenReturn(ZONE_ID); - when(jwtProperties.getIssuer()).thenReturn(ISSUER); - when(jwtProperties.getSecretKey()).thenReturn(SECRET_KEY); + when(jwtConfig.getIssuer()).thenReturn(ISSUER); + when(jwtConfig.getSecretKey()).thenReturn(SECRET_KEY); } private void stubAccessTokenExpiration(Long expireMs) { - when(jwtProperties.getExpireMs()).thenReturn(expireMs); + when(jwtConfig.getExpireMs()).thenReturn(expireMs); } private void stubRefreshTokenExpiration(Long expireMs) { - when(jwtProperties.getRefreshExpireMs()).thenReturn(expireMs); + when(jwtConfig.getRefreshExpireMs()).thenReturn(expireMs); } @Test From 110f022d6ed1a47dbcc819fe5d182fbe31d3b1fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 17:52:03 +0900 Subject: [PATCH 03/27] refactor : Rename JwtResponse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - jwtResponse -> UserTokens 변경한다. - Data 대신 Getter을 대신해서 필요한 기능만 사용하게 한다. - NoArgsConstructor을 삭제해서, 필요한 어노테이션만 있게하도록 한다. --- .../com/tasksprints/auction/common/jwt/JwtProvider.java | 7 +++---- .../dto/response/{JwtResponse.java => UserTokens.java} | 9 ++++----- .../tasksprints/auction/common/jwt/JwtProviderTest.java | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) rename src/main/java/com/tasksprints/auction/domain/auth/dto/response/{JwtResponse.java => UserTokens.java} (62%) diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java index d645bd37..2e2fdff4 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java @@ -2,7 +2,7 @@ import static com.tasksprints.auction.common.util.TimeUtil.*; -import com.tasksprints.auction.domain.auth.dto.response.JwtResponse; +import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -16,12 +16,11 @@ @Component @RequiredArgsConstructor public class JwtProvider { - private final JwtConfig jwtConfig; private final Clock clock; - public JwtResponse generateToken(Long userId, String userRole) { - return JwtResponse.of(createAccessToken(userId, userRole), createRefreshToken()); + public UserTokens generateToken(Long userId, String userRole) { + return UserTokens.of(createAccessToken(userId, userRole), createRefreshToken()); } public String createAccessToken(Long userId, String userRole) { diff --git a/src/main/java/com/tasksprints/auction/domain/auth/dto/response/JwtResponse.java b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/UserTokens.java similarity index 62% rename from src/main/java/com/tasksprints/auction/domain/auth/dto/response/JwtResponse.java rename to src/main/java/com/tasksprints/auction/domain/auth/dto/response/UserTokens.java index c53b08fa..ad91a135 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/dto/response/JwtResponse.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/UserTokens.java @@ -2,16 +2,15 @@ import lombok.*; -@Data +@Getter @Builder -@NoArgsConstructor @AllArgsConstructor -public class JwtResponse { +public class UserTokens { private String refreshToken; private String accessToken; - public static JwtResponse of(String accessToken, String refreshToken) { - return JwtResponse.builder() + public static UserTokens of(String accessToken, String refreshToken) { + return UserTokens.builder() .accessToken(accessToken) .refreshToken(refreshToken) .build(); diff --git a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java index f1c6a24b..69ec9588 100644 --- a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java +++ b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java @@ -1,6 +1,6 @@ package com.tasksprints.auction.common.jwt; -import com.tasksprints.auction.domain.auth.dto.response.JwtResponse; +import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import io.jsonwebtoken.ExpiredJwtException; import java.time.Clock; import java.time.Instant; @@ -53,7 +53,7 @@ private void stubRefreshTokenExpiration(Long expireMs) { @Test @DisplayName("token generator 을 통한 access token, refresh token 발급 테스트") void generateToken() { - JwtResponse jwtResponse = jwtProvider.generateToken(1L, "admin"); + UserTokens jwtResponse = jwtProvider.generateToken(1L, "admin"); assertNotNull(jwtResponse.getAccessToken(), "access token 이 발급되어야 합니다."); assertNotNull(jwtResponse.getRefreshToken(), "refresh token 이 발급되어야 합니다."); From 2a5a279fb2d7ae981eedec181f7e0441ead31bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 18:09:34 +0900 Subject: [PATCH 04/27] refactor : Refactor JwtProvider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 토큰 검증 과정이 겹친다. 따라서 parseToken에서 토큰을 검증하는 기능을 맡게한다. - getSubject는 파싱한 토큰에서 정보를 빼는 메서드의 역할을 부여한다. --- .../auction/common/jwt/JwtConfig.java | 2 +- .../auction/common/jwt/JwtProvider.java | 57 ++++++++------- .../auction/common/jwt/JwtProviderTest.java | 72 ++++++++++--------- 3 files changed, 69 insertions(+), 62 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java index 676468b9..29ef63cd 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java @@ -9,7 +9,7 @@ public class JwtConfig { @Value("${jwt.expire-ms}") - private Long expireMs; + private Long accessExpireMs; @Value("${jwt.expire-ms}") private Long refreshExpireMs; diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java index 2e2fdff4..0bd26023 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java @@ -4,6 +4,8 @@ import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.time.Clock; @@ -16,43 +18,46 @@ @Component @RequiredArgsConstructor public class JwtProvider { + private static final String EMPTY_SUBJECT = ""; private final JwtConfig jwtConfig; private final Clock clock; - public UserTokens generateToken(Long userId, String userRole) { - return UserTokens.of(createAccessToken(userId, userRole), createRefreshToken()); + public UserTokens generateToken(String subject) { + return UserTokens.of( + createToken(subject, jwtConfig.getAccessExpireMs()), + createToken(EMPTY_SUBJECT, jwtConfig.getRefreshExpireMs()) + ); } - public String createAccessToken(Long userId, String userRole) { - + private String createToken(String subject, Long expiredMs) { + byte[] secretKey = JwtUtil.encodeSecretKey(jwtConfig.getSecretKey()); Date now = localDateTimeToDate(LocalDateTime.now(clock)); - - return Jwts.builder().setIssuer(jwtConfig.getIssuer()).claim("userId", userId).claim("userRole", userRole) - .setIssuedAt(now).setExpiration(new Date(now.getTime() + jwtConfig.getExpireMs())) - .signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtConfig.getSecretKey())).compact(); + Date expirationTime = new Date(now.getTime() + expiredMs); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setIssuer(jwtConfig.getIssuer()) + .setSubject(subject) + .setIssuedAt(now) + .setExpiration(expirationTime) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); } - public String createRefreshToken() { - - Date now = localDateTimeToDate(LocalDateTime.now(clock)); - - return Jwts.builder().setIssuer(jwtConfig.getIssuer()).setIssuedAt(now) - .setExpiration(new Date(now.getTime() + jwtConfig.getRefreshExpireMs())) - .signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtConfig.getSecretKey())).compact(); + public void validateToken(String token) { + parseToken(token); } - public boolean verifyToken(String token) { - - Date now = localDateTimeToDate(LocalDateTime.now(clock)); - - Claims claims = getClaims(token); - - return !claims.getExpiration().before(now); + public String getSubject(String token) { + return parseToken(token) + .getBody() + .getSubject(); } - public Claims getClaims(String token) { - return Jwts.parser().setSigningKey(JwtUtil.encodeSecretKey(jwtConfig.getSecretKey())) - .parseClaimsJws(token) - .getBody(); + private Jws parseToken(String token) { + byte[] secretKey = JwtUtil.encodeSecretKey(jwtConfig.getSecretKey()); + return Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token); } } diff --git a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java index 69ec9588..09b9e9d4 100644 --- a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java +++ b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java @@ -43,7 +43,7 @@ public void setUp() { } private void stubAccessTokenExpiration(Long expireMs) { - when(jwtConfig.getExpireMs()).thenReturn(expireMs); + when(jwtConfig.getAccessExpireMs()).thenReturn(expireMs); } private void stubRefreshTokenExpiration(Long expireMs) { @@ -51,64 +51,66 @@ private void stubRefreshTokenExpiration(Long expireMs) { } @Test - @DisplayName("token generator 을 통한 access token, refresh token 발급 테스트") + @DisplayName("accessToken과 refreshToken을 발급해야한다.") void generateToken() { - UserTokens jwtResponse = jwtProvider.generateToken(1L, "admin"); + // when + UserTokens userTokens = jwtProvider.generateToken("1L"); - assertNotNull(jwtResponse.getAccessToken(), "access token 이 발급되어야 합니다."); - assertNotNull(jwtResponse.getRefreshToken(), "refresh token 이 발급되어야 합니다."); + // then + assertNotNull(userTokens.getAccessToken(), "access token 이 발급되어야 합니다."); + assertNotNull(userTokens.getRefreshToken(), "refresh token 이 발급되어야 합니다."); } @Test - @DisplayName("access token 발급 테스트") - void createAccessToken() { - stubAccessTokenExpiration(VALID_EXPIRE_MS); - - String token = jwtProvider.createAccessToken(1L, "admin"); - - assertNotNull(token, "access token 이 발급되어야 합니다."); - } - - @Test - @DisplayName("refresh token 발급 테스트") - void createRefreshToken() { - stubRefreshTokenExpiration(REFRESH_EXPIRE_MS); - String token = jwtProvider.createRefreshToken(); - assertNotNull(token, "refresh token 이 발급되어야 합니다."); - } - - @Test - @DisplayName("유효한 토큰 테스트") + @DisplayName("유효기간에는 토큰이 유효해야한다.") void verifyToken_valid() { + // given stubAccessTokenExpiration(VALID_EXPIRE_MS); + stubRefreshTokenExpiration(REFRESH_EXPIRE_MS); - String token = jwtProvider.createAccessToken(1L, "admin"); + // when + UserTokens userTokens = jwtProvider.generateToken("1L"); - Assertions.assertTrue(jwtProvider.verifyToken(token)); + // then + Assertions.assertDoesNotThrow(() -> { + jwtProvider.validateToken(userTokens.getAccessToken()); + }); + Assertions.assertDoesNotThrow(() -> { + jwtProvider.validateToken(userTokens.getRefreshToken()); + }); } @Test - @DisplayName("만료된 토큰 테스트") + @DisplayName("유효기간이 지나면, 토큰 만료 예외를 반환해야한다") void verifyToken_expired() { + // given stubAccessTokenExpiration(EXPIRED_EXPIRE_MS); + stubRefreshTokenExpiration(EXPIRED_EXPIRE_MS); + + // when + UserTokens userTokens = jwtProvider.generateToken("1L"); - String token = jwtProvider.createAccessToken(1L, "admin"); + // then + Assertions.assertThrows(ExpiredJwtException.class, () -> { + jwtProvider.validateToken(userTokens.getRefreshToken()); + }, "리프레시토큰이 즉시 만료되어야 합니다."); Assertions.assertThrows(ExpiredJwtException.class, () -> { - jwtProvider.verifyToken(token); - }, "토큰이 즉시 만료되어야 합니다."); + jwtProvider.validateToken(userTokens.getAccessToken()); + }, "액세스토큰이 즉시 만료되어야 합니다."); } @Test @DisplayName("디코딩 된 페이로드 정확성 테스트") void getClaims() { + // given stubAccessTokenExpiration(VALID_EXPIRE_MS); + UserTokens userTokens = jwtProvider.generateToken("1L"); - String token = jwtProvider.createAccessToken(1L, "admin"); - Long decodedUserId = jwtProvider.getClaims(token).get("userId", Long.class); - String decodedUserRole = jwtProvider.getClaims(token).get("userRole", String.class); + // when + String decodedUserId = jwtProvider.getSubject(userTokens.getAccessToken()); - assertThat(decodedUserId).isEqualTo(1L); - assertThat(decodedUserRole).isEqualTo("admin"); + // then + assertThat(decodedUserId).isEqualTo("1L"); } } From 62b57dec8c396a222386a3d528e057f7c8771297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 18:11:44 +0900 Subject: [PATCH 05/27] refactor : Move JwtConfig's directory path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - common/config에 포함되도록 합니다. --- .../tasksprints/auction/common/{jwt => config}/JwtConfig.java | 2 +- .../java/com/tasksprints/auction/common/jwt/JwtProvider.java | 1 + .../com/tasksprints/auction/common/jwt/JwtProviderTest.java | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) rename src/main/java/com/tasksprints/auction/common/{jwt => config}/JwtConfig.java (90%) diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java b/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java similarity index 90% rename from src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java rename to src/main/java/com/tasksprints/auction/common/config/JwtConfig.java index 29ef63cd..5abef1db 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtConfig.java +++ b/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.common.jwt; +package com.tasksprints.auction.common.config; import lombok.Getter; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java index 0bd26023..e6ab226e 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java @@ -2,6 +2,7 @@ import static com.tasksprints.auction.common.util.TimeUtil.*; +import com.tasksprints.auction.common.config.JwtConfig; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Header; diff --git a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java index 09b9e9d4..afbfa45d 100644 --- a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java +++ b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java @@ -1,5 +1,6 @@ package com.tasksprints.auction.common.jwt; +import com.tasksprints.auction.common.config.JwtConfig; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import io.jsonwebtoken.ExpiredJwtException; import java.time.Clock; From c902cd73f46cde37e5ea64606213ab4ec4b77af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:11:40 +0900 Subject: [PATCH 06/27] feat : Add refresh token model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 리프레시 토큰 모델을 생성합니다. - id로 refreshtoken 값을 갖습니다. - 유저의 id를 열로 갖습니다. --- .../domain/auth/model/RefreshToken.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java new file mode 100644 index 00000000..a0395f9d --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java @@ -0,0 +1,23 @@ +package com.tasksprints.auction.domain.auth.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RefreshToken { + @Id + private String id; + + @Column + private Long memberId; +} From fbee8cd05cd1362e4458596286f6e6355581de31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:18:10 +0900 Subject: [PATCH 07/27] feat : Add refresh token repsitory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 리프레시 토큰 레파지토리를 생성합니다. - refreshToken 레파지토리에 refreshToken을 저장하고, findById와 existById가 실행되는지 확인합니다. --- .../repository/RefreshTokenRepository.java | 7 +++ .../RefreshTokenRepositoryTest.java | 60 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepository.java create mode 100644 src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java diff --git a/src/main/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepository.java b/src/main/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepository.java new file mode 100644 index 00000000..7259764b --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepository.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.repository; + +import com.tasksprints.auction.domain.auth.model.RefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RefreshTokenRepository extends JpaRepository { +} diff --git a/src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java b/src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java new file mode 100644 index 00000000..f4f491da --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java @@ -0,0 +1,60 @@ +package com.tasksprints.auction.domain.auth.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import com.tasksprints.auction.common.config.QueryDslConfig; +import com.tasksprints.auction.domain.auth.model.RefreshToken; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + + +@DataJpaTest +@Import(QueryDslConfig.class) +class RefreshTokenRepositoryTest { + + @Autowired + private RefreshTokenRepository refreshTokenRepository; + + private RefreshToken refreshToken; + + @BeforeEach + void setUp() { + refreshToken = RefreshToken.builder() + .id("testId") + .memberId(1L) + .build(); + } + + @Test + @DisplayName("refresh token ID로 refresh token 조회") + void testFindById() { + // given + refreshTokenRepository.save(refreshToken); + + // when + Optional resultRefreshToken = refreshTokenRepository.findById(refreshToken.getId()); + + // then + assertTrue(resultRefreshToken.isPresent()); + assertThat(refreshToken.getId()).isEqualTo(resultRefreshToken.get().getId()); + } + + @Test + @DisplayName("refresh token ID로 refresh token이 존재하는지 확인합니다.") + void testExistById() { + // given + refreshTokenRepository.save(refreshToken); + + // when + boolean resultRefreshToken = refreshTokenRepository.existsById(refreshToken.getId()); + + // then + assertTrue(resultRefreshToken); + } +} From ea7968efd31bc48136227829add7428bb3363a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:19:12 +0900 Subject: [PATCH 08/27] feat : Add Role enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자의 권한을 다양하게 부여하기 위해, role enum을 추가합니다. --- .../com/tasksprints/auction/domain/auth/model/Role.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/model/Role.java diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/Role.java b/src/main/java/com/tasksprints/auction/domain/auth/model/Role.java new file mode 100644 index 00000000..5591e894 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/Role.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.model; + +public enum Role { + USER, + ADMIN, + GUEST +} From a810d0affd21971afe62c3ff4047bba86ee87c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:20:43 +0900 Subject: [PATCH 09/27] refactor : Delete userRole class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자의 권한 부여에 대한 것을 auth 도메인에서 관리합니다. --- .../auction/domain/user/model/UserRole.java | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/main/java/com/tasksprints/auction/domain/user/model/UserRole.java diff --git a/src/main/java/com/tasksprints/auction/domain/user/model/UserRole.java b/src/main/java/com/tasksprints/auction/domain/user/model/UserRole.java deleted file mode 100644 index 13d8e420..00000000 --- a/src/main/java/com/tasksprints/auction/domain/user/model/UserRole.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.tasksprints.auction.domain.user.model; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum UserRole { - ADMIN("admin"), - USER("user"); - - private final String userRole; -} From dc488ddd4ae956df6e1cc4f23fec4709614f77fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:23:39 +0900 Subject: [PATCH 10/27] refactor : Add Accessor domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자가 접속했을 때, 권한을 부여해 줄 모델을 생성합니다. --- .../auction/domain/auth/model/Accessor.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java b/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java new file mode 100644 index 00000000..d9ad8150 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java @@ -0,0 +1,34 @@ +package com.tasksprints.auction.domain.auth.model; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Builder +@Getter +@RequiredArgsConstructor +public class Accessor { + + private static final Long GUEST_USER_ID = 0L; + private final Long userId; + private final Role role; + + public static Accessor guest() { + return Accessor.of(GUEST_USER_ID, Role.GUEST); + } + public static Accessor user(Long userId) { + return Accessor.of(userId, Role.USER); + } + public static Accessor admin(Long userId) { + return Accessor.of(userId, Role.ADMIN); + } + + private static Accessor of(Long userId, Role role) { + return Accessor.builder() + .userId(userId) + .role(role) + .build(); + } +} + + From d90a93fe3aa179813e436b079fa5c776e31eaa62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:26:15 +0900 Subject: [PATCH 11/27] refactor : Add Auth annotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인증인가를 필요로 하는 엔드포인트를 식별하기 위한 어노테이션을 생성합니다. --- .../java/com/tasksprints/auction/common/jwt/Auth.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/common/jwt/Auth.java diff --git a/src/main/java/com/tasksprints/auction/common/jwt/Auth.java b/src/main/java/com/tasksprints/auction/common/jwt/Auth.java new file mode 100644 index 00000000..2061c8ab --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/jwt/Auth.java @@ -0,0 +1,11 @@ +package com.tasksprints.auction.common.jwt; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Auth { +} From a9ee79fa5d6f15d7c58e65141ba627cf378401dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:29:16 +0900 Subject: [PATCH 12/27] refactor : Add AuthException class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인증인가시 발생하는 예외를 처리하기 위한 클래스를 생성합니다. --- .../domain/auth/exception/AccessTokenException.java | 7 +++++++ .../auction/domain/auth/exception/AuthException.java | 7 +++++++ .../domain/auth/exception/RefreshTokenException.java | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/exception/AccessTokenException.java create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/exception/AuthException.java create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/exception/RefreshTokenException.java diff --git a/src/main/java/com/tasksprints/auction/domain/auth/exception/AccessTokenException.java b/src/main/java/com/tasksprints/auction/domain/auth/exception/AccessTokenException.java new file mode 100644 index 00000000..c4acac9f --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/exception/AccessTokenException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.exception; + +public class AccessTokenException extends AuthException { + public AccessTokenException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/exception/AuthException.java b/src/main/java/com/tasksprints/auction/domain/auth/exception/AuthException.java new file mode 100644 index 00000000..ce842a7e --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/exception/AuthException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.exception; + +public class AuthException extends RuntimeException { + public AuthException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/exception/RefreshTokenException.java b/src/main/java/com/tasksprints/auction/domain/auth/exception/RefreshTokenException.java new file mode 100644 index 00000000..c0c1d255 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/exception/RefreshTokenException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.exception; + +public class RefreshTokenException extends AuthException { + public RefreshTokenException(String message) { + super(message); + } +} From b92dda38ac76bbaffa1eac305221ee8db36ef3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 19:52:08 +0900 Subject: [PATCH 13/27] refactor : Add TokenExtractor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 토큰을 추출하는 인터페이스를 생성합니다. - accessToken을 추출하는 클래스를, 인터페이스를 통해 구현합니다. --- .../common/constant/ApiResponseMessages.java | 3 ++ .../domain/auth/AccessTokenExtractor.java | 18 ++++++++ .../auction/domain/auth/TokenExtractor.java | 5 +++ .../domain/auth/AccessTokenExtractorTest.java | 43 +++++++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java create mode 100644 src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java diff --git a/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java b/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java index a8963f05..eb2d9770 100644 --- a/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java +++ b/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java @@ -30,5 +30,8 @@ public class ApiResponseMessages { public static final String REVIEWS_RETRIEVED = "Reviews successfully retrieved"; public static final String REVIEW_RETRIEVED = "Review successfully retrieved"; + // AUTH + public static final String ACCESS_TOKEN_NOT_FOUND = "Access token Not found"; + // Additional messages can be defined as needed } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java new file mode 100644 index 00000000..3d56d7e4 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java @@ -0,0 +1,18 @@ +package com.tasksprints.auction.domain.auth; + +import static com.tasksprints.auction.common.constant.ApiResponseMessages.ACCESS_TOKEN_NOT_FOUND; + +import com.tasksprints.auction.domain.auth.exception.AccessTokenException; +import org.springframework.stereotype.Component; + +@Component +public class AccessTokenExtractor implements TokenExtractor { + private static final String TYPE = "Bearer "; + + public String extractToken(String header) { + if(header != null && header.startsWith(TYPE)) { + return header.substring(TYPE.length()); + } + throw new AccessTokenException(ACCESS_TOKEN_NOT_FOUND); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java new file mode 100644 index 00000000..824a961d --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java @@ -0,0 +1,5 @@ +package com.tasksprints.auction.domain.auth; + +public interface TokenExtractor { + String extractToken(String value); +} diff --git a/src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java b/src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java new file mode 100644 index 00000000..35b38058 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java @@ -0,0 +1,43 @@ +package com.tasksprints.auction.domain.auth; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.tasksprints.auction.domain.auth.exception.AccessTokenException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class AccessTokenExtractorTest { + private TokenExtractor tokenExtractor; + + @BeforeEach + void setUp() { + tokenExtractor = new AccessTokenExtractor(); + } + + @Test + @DisplayName("헤더에서 access token 을 꺼낸다") + void testExtractToken_success() { + // given + String header = "Bearer token"; + + // when + String accessToken = tokenExtractor.extractToken(header); + + // then + assertThat(accessToken).isEqualTo("token"); + } + + @Test + @DisplayName("access token 이 존재하지 않으면 예외를 반환한다") + void testExtractToken_fail() { + // given + String header = "no token"; + + // when, then + Assertions.assertThrows(AccessTokenException.class, () -> { + tokenExtractor.extractToken(header); + }); + } +} From cc4468e08ea57b54c4bfb8315d862935b32aaa66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 20:04:20 +0900 Subject: [PATCH 14/27] refactor : Refactor TokenExtractor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 토큰을 추출하는 인터페이스의 매개변수를 HttpServletRequest로 변경합니다. --- .../auction/domain/auth/AccessTokenExtractor.java | 6 +++++- .../auction/domain/auth/TokenExtractor.java | 4 +++- .../domain/auth/AccessTokenExtractorTest.java | 13 +++++++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java index 3d56d7e4..2b36ea99 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java @@ -3,13 +3,17 @@ import static com.tasksprints.auction.common.constant.ApiResponseMessages.ACCESS_TOKEN_NOT_FOUND; import com.tasksprints.auction.domain.auth.exception.AccessTokenException; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.stereotype.Component; @Component + public class AccessTokenExtractor implements TokenExtractor { private static final String TYPE = "Bearer "; + private static final String HEADER = "Authorization"; - public String extractToken(String header) { + public String extractToken(HttpServletRequest request) { + String header = request.getHeader(HEADER); if(header != null && header.startsWith(TYPE)) { return header.substring(TYPE.length()); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java index 824a961d..6dc30cfb 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/TokenExtractor.java @@ -1,5 +1,7 @@ package com.tasksprints.auction.domain.auth; +import jakarta.servlet.http.HttpServletRequest; + public interface TokenExtractor { - String extractToken(String value); + String extractToken(HttpServletRequest request); } diff --git a/src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java b/src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java index 35b38058..3efe9fc7 100644 --- a/src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java +++ b/src/test/java/com/tasksprints/auction/domain/auth/AccessTokenExtractorTest.java @@ -1,8 +1,11 @@ package com.tasksprints.auction.domain.auth; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.tasksprints.auction.domain.auth.exception.AccessTokenException; +import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -20,10 +23,11 @@ void setUp() { @DisplayName("헤더에서 access token 을 꺼낸다") void testExtractToken_success() { // given - String header = "Bearer token"; + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("Authorization")).thenReturn("Bearer token"); // when - String accessToken = tokenExtractor.extractToken(header); + String accessToken = tokenExtractor.extractToken(request); // then assertThat(accessToken).isEqualTo("token"); @@ -33,11 +37,12 @@ void testExtractToken_success() { @DisplayName("access token 이 존재하지 않으면 예외를 반환한다") void testExtractToken_fail() { // given - String header = "no token"; + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("Authorization")).thenReturn("notoken"); // when, then Assertions.assertThrows(AccessTokenException.class, () -> { - tokenExtractor.extractToken(header); + tokenExtractor.extractToken(request); }); } } From dcc0fe04996aae8cdb0f42efd998855e4a0e0bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Fri, 1 Nov 2024 20:19:55 +0900 Subject: [PATCH 15/27] feat : Add RefreshTokenExtractor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 쿠키에서 토큰을 추출하고 반환합니다. --- .../common/constant/ApiResponseMessages.java | 1 + .../domain/auth/RefreshTokenExtractor.java | 33 ++++++++++ .../auth/RefreshTokenExtractorTest.java | 65 +++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java create mode 100644 src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java diff --git a/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java b/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java index eb2d9770..9b097fea 100644 --- a/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java +++ b/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java @@ -32,6 +32,7 @@ public class ApiResponseMessages { // AUTH public static final String ACCESS_TOKEN_NOT_FOUND = "Access token Not found"; + public static final String REFRESH_TOKEN_NOT_FOUND = "Refresh token not found"; // Additional messages can be defined as needed } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java new file mode 100644 index 00000000..4ca8ef98 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java @@ -0,0 +1,33 @@ +package com.tasksprints.auction.domain.auth; + +import static com.tasksprints.auction.common.constant.ApiResponseMessages.REFRESH_TOKEN_NOT_FOUND; + +import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; +import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RefreshTokenExtractor implements TokenExtractor { + private static final String COOKIE_NAME = "refresh-token"; + + private final RefreshTokenRepository refreshTokenRepository; + @Override + public String extractToken(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + + if (cookies == null) { + throw new RefreshTokenException(REFRESH_TOKEN_NOT_FOUND); + } + return Arrays.stream(cookies) + .filter(cookie -> cookie.getName().equals(COOKIE_NAME)) + .filter(cookie -> refreshTokenRepository.existsById(cookie.getValue())) + .findFirst() + .orElseThrow(() -> new RefreshTokenException(REFRESH_TOKEN_NOT_FOUND)) + .getValue(); + } +} diff --git a/src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java b/src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java new file mode 100644 index 00000000..e267f204 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java @@ -0,0 +1,65 @@ +package com.tasksprints.auction.domain.auth; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; +import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +class RefreshTokenExtractorTest { + @Mock + private RefreshTokenRepository refreshTokenRepository; + + @InjectMocks + private RefreshTokenExtractor tokenExtractor; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("쿠키에서 refresh token 을 꺼내서 반환한다.") + void testExtractToken_success() { + // given + Cookie[] cookies = { + new Cookie("nothing", "token"), + new Cookie("refresh-token", "tokenName"), + }; + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getCookies()).thenReturn(cookies); + when(refreshTokenRepository.existsById("tokenName")).thenReturn(true); + + // when + String resultValue = tokenExtractor.extractToken(request); + + // then + assertThat(resultValue).isEqualTo("tokenName"); + } + + @Test + @DisplayName("쿠키에 refresh token이 존재하지 않으면 예외를 반환한다.") + void testExtractToken_fail() { + // given + Cookie[] cookies = { + new Cookie("nothing", "token"), + }; + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getCookies()).thenReturn(cookies); + + // when, then + Assertions.assertThrows(RefreshTokenException.class, ()-> { + tokenExtractor.extractToken(request); + }, "리프레시토큰이 쿠키에 존재해야 합니다."); + } +} From 5ae66f275db75ac1facb87d867225a9e1e38ed4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Sat, 16 Nov 2024 23:42:13 +0900 Subject: [PATCH 16/27] feat : Add AuthenticationResolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auth 어노테이션을 가진 파라미터 대상으로 resolver가 작동합니다 - RefreshToken 관련 오류가 발생하지 않는경우 member 권한으로 접근합니다 - 그렇지 않는 경우 guest 권한으로 접근합니다 - 컨트롤러에서 의존성 문제로 발생하는 오류를 목킹을 해결합니다 --- .../auction/api/auth/AuthController.java | 18 +++++++ .../auction/common/config/JwtConfig.java | 10 ++-- .../auction/common/config/WebConfig.java | 7 ++- .../resolver/AuthenticationResolver.java | 53 +++++++++++++++++++ .../domain/auth/AccessTokenExtractor.java | 3 +- .../domain/auth/RefreshTokenExtractor.java | 2 + .../auction/domain/auth/model/Accessor.java | 2 - .../auction/api/AuctionControllerTest.java | 5 +- .../auction/api/BaseControllerTest.java | 10 ++++ .../auction/api/ProductControllerTest.java | 2 +- .../auction/api/UserControllerTest.java | 2 +- .../auction/common/config/TestAuthConfig.java | 25 +++++++++ 12 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/tasksprints/auction/api/auth/AuthController.java create mode 100644 src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java create mode 100644 src/test/java/com/tasksprints/auction/api/BaseControllerTest.java create mode 100644 src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java diff --git a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java new file mode 100644 index 00000000..e4a50046 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java @@ -0,0 +1,18 @@ +package com.tasksprints.auction.api.auth; + +import com.tasksprints.auction.common.jwt.Auth; +import com.tasksprints.auction.domain.auth.model.Accessor; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequiredArgsConstructor +@RequestMapping("/api/v1/auth") +public class AuthController { + @GetMapping("/access") + public Boolean access(@Auth Accessor accessor) { + return true; + } +} diff --git a/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java b/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java index 5abef1db..f272e578 100644 --- a/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java +++ b/src/main/java/com/tasksprints/auction/common/config/JwtConfig.java @@ -1,22 +1,24 @@ package com.tasksprints.auction.common.config; import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component @Getter +@RequiredArgsConstructor public class JwtConfig { @Value("${jwt.expire-ms}") - private Long accessExpireMs; + private final Long accessExpireMs; @Value("${jwt.expire-ms}") - private Long refreshExpireMs; + private final Long refreshExpireMs; @Value("${jwt.issuer}") - private String issuer; + private final String issuer; @Value("${jwt.secret}") - private String secretKey; + private final String secretKey; } diff --git a/src/main/java/com/tasksprints/auction/common/config/WebConfig.java b/src/main/java/com/tasksprints/auction/common/config/WebConfig.java index 0f7bb22a..b895df7c 100644 --- a/src/main/java/com/tasksprints/auction/common/config/WebConfig.java +++ b/src/main/java/com/tasksprints/auction/common/config/WebConfig.java @@ -1,7 +1,7 @@ package com.tasksprints.auction.common.config; +import com.tasksprints.auction.common.resolver.AuthenticationResolver; import com.tasksprints.auction.common.resolver.SearchConditionResolver; -import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; @@ -12,14 +12,17 @@ @Configuration public class WebConfig implements WebMvcConfigurer { private final SearchConditionResolver searchConditionResolver; + private final AuthenticationResolver authenticationResolver; - public WebConfig(SearchConditionResolver searchConditionResolver) { + public WebConfig(SearchConditionResolver searchConditionResolver, AuthenticationResolver authenticationResolver) { this.searchConditionResolver = searchConditionResolver; + this.authenticationResolver = authenticationResolver; } @Override public void addArgumentResolvers(List resolvers) { resolvers.add(searchConditionResolver); + resolvers.add(authenticationResolver); } @Override diff --git a/src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java b/src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java new file mode 100644 index 00000000..56064687 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java @@ -0,0 +1,53 @@ +package com.tasksprints.auction.common.resolver; + +import com.tasksprints.auction.common.jwt.Auth; +import com.tasksprints.auction.common.jwt.JwtProvider; +import com.tasksprints.auction.domain.auth.TokenExtractor; +import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; +import com.tasksprints.auction.domain.auth.model.Accessor; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class AuthenticationResolver implements HandlerMethodArgumentResolver { + private final JwtProvider jwtProvider; + private final TokenExtractor refreshTokenExtractor; + private final TokenExtractor accessTokenExtractor; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter + .hasParameterAnnotation(Auth.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) { + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + + if (request == null) { + throw new IllegalArgumentException(); + } + try { + String refreshToken = refreshTokenExtractor.extractToken(request); + String accessToken = accessTokenExtractor.extractToken(request); + + jwtProvider.validateToken(accessToken); + jwtProvider.validateToken(refreshToken); + + Long userId = Long.valueOf(jwtProvider.getSubject(refreshToken)); + return Accessor.user(userId); + } catch (RefreshTokenException e) { + return Accessor.guest(); + } + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java index 2b36ea99..cd2e0179 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/AccessTokenExtractor.java @@ -4,10 +4,11 @@ import com.tasksprints.auction.domain.auth.exception.AccessTokenException; import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @Component - +@Qualifier("accessTokenExtractor") public class AccessTokenExtractor implements TokenExtractor { private static final String TYPE = "Bearer "; private static final String HEADER = "Authorization"; diff --git a/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java index 4ca8ef98..f512be34 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java @@ -8,9 +8,11 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @Component +@Qualifier("refreshTokenExtractor") @RequiredArgsConstructor public class RefreshTokenExtractor implements TokenExtractor { private static final String COOKIE_NAME = "refresh-token"; diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java b/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java index d9ad8150..03060d48 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java @@ -30,5 +30,3 @@ private static Accessor of(Long userId, Role role) { .build(); } } - - diff --git a/src/test/java/com/tasksprints/auction/api/AuctionControllerTest.java b/src/test/java/com/tasksprints/auction/api/AuctionControllerTest.java index e21acad2..6d9cb5ee 100644 --- a/src/test/java/com/tasksprints/auction/api/AuctionControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/AuctionControllerTest.java @@ -19,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; @@ -40,10 +41,10 @@ @WebMvcTest(AuctionController.class) @MockBean(JpaMetamodelMappingContext.class) -public class AuctionControllerTest { +public class AuctionControllerTest extends BaseControllerTest { @Autowired - private MockMvc mockMvc; + protected MockMvc mockMvc; @MockBean private AuctionService auctionService; diff --git a/src/test/java/com/tasksprints/auction/api/BaseControllerTest.java b/src/test/java/com/tasksprints/auction/api/BaseControllerTest.java new file mode 100644 index 00000000..58a4aba1 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/api/BaseControllerTest.java @@ -0,0 +1,10 @@ +package com.tasksprints.auction.api; + +import com.tasksprints.auction.common.config.TestAuthConfig; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; + +@WebMvcTest +@Import(TestAuthConfig.class) +public abstract class BaseControllerTest { +} diff --git a/src/test/java/com/tasksprints/auction/api/ProductControllerTest.java b/src/test/java/com/tasksprints/auction/api/ProductControllerTest.java index 54212bc5..6d56ebc6 100644 --- a/src/test/java/com/tasksprints/auction/api/ProductControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/ProductControllerTest.java @@ -28,7 +28,7 @@ @WebMvcTest(ProductController.class) @MockBean(JpaMetamodelMappingContext.class) -public class ProductControllerTest { +public class ProductControllerTest extends BaseControllerTest { @Autowired private MockMvc mockMvc; diff --git a/src/test/java/com/tasksprints/auction/api/UserControllerTest.java b/src/test/java/com/tasksprints/auction/api/UserControllerTest.java index 091985ab..5cb0c775 100644 --- a/src/test/java/com/tasksprints/auction/api/UserControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/UserControllerTest.java @@ -30,7 +30,7 @@ @WebMvcTest(UserController.class) @MockBean(JpaMetamodelMappingContext.class) -class UserControllerTest { +class UserControllerTest extends BaseControllerTest { @Autowired private MockMvc mockMvc; diff --git a/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java b/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java new file mode 100644 index 00000000..ef4c6be9 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java @@ -0,0 +1,25 @@ +package com.tasksprints.auction.common.config; + +import com.tasksprints.auction.common.jwt.JwtProvider; +import com.tasksprints.auction.domain.auth.TokenExtractor; +import org.mockito.Mockito; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class TestAuthConfig { + @Bean + public JwtProvider jwtProvider() { + return Mockito.mock(JwtProvider.class); + } + + @Bean + public TokenExtractor refreshTokenExtractor() { + return Mockito.mock(TokenExtractor.class); + } + + @Bean + public TokenExtractor accessTokenExtractor() { + return Mockito.mock(TokenExtractor.class); + } +} From 1197c69650ece144457797a60580ff3e95576b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Mon, 18 Nov 2024 13:26:18 +0900 Subject: [PATCH 17/27] feat : Add find user by email method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 레파지토리에서 user를 email로 찾는다 --- .../user/repository/UserRepository.java | 2 ++ .../domain/user/UserRepositoryTest.java | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/main/java/com/tasksprints/auction/domain/user/repository/UserRepository.java b/src/main/java/com/tasksprints/auction/domain/user/repository/UserRepository.java index 007cf110..ff883b64 100644 --- a/src/main/java/com/tasksprints/auction/domain/user/repository/UserRepository.java +++ b/src/main/java/com/tasksprints/auction/domain/user/repository/UserRepository.java @@ -1,7 +1,9 @@ package com.tasksprints.auction.domain.user.repository; import com.tasksprints.auction.domain.user.model.User; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); } diff --git a/src/test/java/com/tasksprints/auction/domain/user/UserRepositoryTest.java b/src/test/java/com/tasksprints/auction/domain/user/UserRepositoryTest.java index 45f64ea9..960400d1 100644 --- a/src/test/java/com/tasksprints/auction/domain/user/UserRepositoryTest.java +++ b/src/test/java/com/tasksprints/auction/domain/user/UserRepositoryTest.java @@ -95,4 +95,25 @@ void createUser() { log.info("Created User: {}", createdUser); } } + + + @DisplayName("find User By Email") + @Test + void findUserByEmail() { + // given + User createdUser = userRepository.save(user); + String findEmail = "test@example.com"; + + // when + User foundUser = userRepository.findByEmail(findEmail).orElse(null); + + // then + Assertions.assertNotNull(foundUser); + Assertions.assertEquals(createdUser.getId(), foundUser.getId()); + Assertions.assertEquals("testUser", foundUser.getName()); + Assertions.assertEquals("testNick", foundUser.getNickName()); + Assertions.assertEquals("test@example.com", foundUser.getEmail()); + + log.info("Found User: {}", foundUser); + } } From 09874d95c966dc3b391b2ef2cbc6f699e1c9dc90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Mon, 18 Nov 2024 13:28:35 +0900 Subject: [PATCH 18/27] feat : Add get user detail method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserService와 UserServiceImpl에 메서드를 추가하고 테스트 코드를 작성한다 - 유저를 찾을 경우 유저의 상세 정보를 return 한다 - 유저를 찾지 못하는 경우, 예외를 반환한다 --- .../domain/user/service/UserService.java | 2 + .../domain/user/service/UserServiceImpl.java | 6 +++ .../domain/user/UserServiceImplTest.java | 39 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java b/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java index 63755627..73646ad5 100644 --- a/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java +++ b/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java @@ -16,4 +16,6 @@ public interface UserService { UserDetailResponse updateUser(Long id, UserRequest.Update user); void deleteUser(Long id); + + UserDetailResponse getUserDetailByEmail(String email); } diff --git a/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java index ad9d0b6b..dc4e8779 100644 --- a/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java @@ -59,4 +59,10 @@ public void deleteUser(Long id) { userRepository.save(user); // 상태 업데이트를 저장 } + @Override + public UserDetailResponse getUserDetailByEmail(String email) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UserNotFoundException("User not found with email " + email)); + return UserDetailResponse.of(user); + } } diff --git a/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java index 95c78348..a4aacf07 100644 --- a/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java @@ -171,4 +171,43 @@ void shouldThrowExceptionWhenUserNotFound() { verify(userRepository, never()).delete(any(User.class)); } } + + @Nested + @DisplayName("Get User By Email") + class GetUserByEmailTests { + + @Test + @DisplayName("Get user's detail information by email") + void shouldReturnUserWhenFound() { + // given + String findEmail = "test@example.com"; + when(userRepository.findByEmail(any())).thenReturn(Optional.ofNullable(existingUser)); + + // when + UserDetailResponse user = userService.getUserDetailByEmail(findEmail); + + // then + Assertions.assertNotNull(user); + Assertions.assertEquals(existingUser.getId(), user.getId()); + Assertions.assertEquals(existingUser.getName(), user.getName()); + verify(userRepository, times(1)).findByEmail(findEmail); + } + + @Test + @DisplayName("Should throw an exception if the user cannot be found by email") + void shouldThrowExceptionWhenUserNotFound() { + // given + String findEmail = "different@example.com"; + when(userRepository.findByEmail(any())).thenReturn(Optional.empty()); + + // when + UserNotFoundException exception = Assertions.assertThrows(UserNotFoundException.class, () -> { + userService.getUserDetailByEmail(findEmail); + }); + + // then + Assertions.assertEquals("User not found with email " + findEmail, exception.getMessage()); + verify(userRepository, times(1)).findByEmail(findEmail); + } + } } From 72eaf1fcf0ed0b10ae3c4d34a21554ecd59f0311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Mon, 18 Nov 2024 13:38:05 +0900 Subject: [PATCH 19/27] feat : Add login method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그인 정보가 올바를 경우 토큰을 반환한다 - 로그인 정보가 올바르지 않을 경우 예외를 반환한다 --- .../domain/auth/model/RefreshToken.java | 7 ++ .../domain/auth/service/AuthService.java | 7 ++ .../domain/auth/service/AuthServiceImpl.java | 37 ++++++++ .../auth/service/AuthServiceImplTest.java | 91 +++++++++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java create mode 100644 src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java index a0395f9d..0a3e22f9 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java @@ -20,4 +20,11 @@ public class RefreshToken { @Column private Long memberId; + + public static RefreshToken create(String id, Long memberId) { + return RefreshToken.builder() + .id(id) + .memberId(memberId) + .build(); + } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java new file mode 100644 index 00000000..3ecebf36 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.service; + +import com.tasksprints.auction.domain.auth.dto.response.UserTokens; + +public interface AuthService { + UserTokens login(String email, String password); +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java new file mode 100644 index 00000000..a4c88056 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java @@ -0,0 +1,37 @@ +package com.tasksprints.auction.domain.auth.service; + +import com.tasksprints.auction.common.jwt.JwtProvider; +import com.tasksprints.auction.domain.auth.dto.response.UserTokens; +import com.tasksprints.auction.domain.auth.exception.AuthException; +import com.tasksprints.auction.domain.auth.model.RefreshToken; +import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; +import com.tasksprints.auction.domain.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + + +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class AuthServiceImpl implements AuthService { + private final UserService userService; + private final JwtProvider jwtProvider; + private final RefreshTokenRepository refreshTokenRepository; + + @Transactional + @Override + public UserTokens login(String email, String password) { + UserDetailResponse userDetail = userService.getUserDetailByEmail(email); + + if (!password.equals(userDetail.getPassword())) { + throw new AuthException("password is not correct"); + } + + UserTokens userTokens = jwtProvider.generateToken(userDetail.getId().toString()); + RefreshToken refreshToken = RefreshToken.create(userTokens.getRefreshToken(), userDetail.getId()); + refreshTokenRepository.save(refreshToken); + return userTokens; + } +} diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java new file mode 100644 index 00000000..72e263a7 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java @@ -0,0 +1,91 @@ +package com.tasksprints.auction.domain.auth.service; + + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.tasksprints.auction.common.jwt.JwtProvider; +import com.tasksprints.auction.domain.auth.dto.response.UserTokens; +import com.tasksprints.auction.domain.auth.exception.AuthException; +import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; +import com.tasksprints.auction.domain.user.model.User; +import com.tasksprints.auction.domain.user.service.UserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + + +@ExtendWith(MockitoExtension.class) +class AuthServiceImplTest { + + @Mock + UserService userService; + + @Mock + JwtProvider jwtProvider; + @Mock + RefreshTokenRepository refreshTokenRepository; + @InjectMocks + private AuthServiceImpl authService; + + private UserDetailResponse userDetail; + + @BeforeEach + void setUp() { + User existingUser = User.builder() + .id(1L) + .email("user@exapmle.com") + .password("password") + .nickName("testUser") + .name("realName") + .build(); + + userDetail = UserDetailResponse.of(existingUser); + } + + @Nested + @DisplayName("Login success when password is correct") + class testLogin { + @Test + @DisplayName("Return tokens when password is correct") + void ReturnTokensWhenPasswordIsCorrect() { + // given + String email = "user@exapmle.com"; + String password = "password"; + UserTokens expected = UserTokens.of("accessToken", "refreshToken"); + when(userService.getUserDetailByEmail(any())).thenReturn(userDetail); + when(jwtProvider.generateToken(any())).thenReturn(expected); + when(refreshTokenRepository.save(any())).thenReturn(any()); + + // when + UserTokens actual = authService.login(email, password); + + // then + assertEquals(expected.getRefreshToken(), actual.getRefreshToken()); + assertEquals(expected.getAccessToken(), actual.getAccessToken()); + } + + @Test + @DisplayName("should throw exception when password is different") + void shouldReturnExceptionWhenPasswordIsDifferent() { + // given + String email = "user@exapmle.com"; + String password = "differentPassword"; + when(userService.getUserDetailByEmail(any())).thenReturn(userDetail); + + // when + AuthException exception = assertThrows(AuthException.class, () -> { + authService.login(email, password); + }); + + // then + assertEquals("password is not correct", exception.getMessage()); + } + } +} From 2558555785f98f33ffde83a434029dad04c26a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Mon, 18 Nov 2024 16:01:04 +0900 Subject: [PATCH 20/27] feat : Add login controller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그인 정보가 올바를 경우 쿠키로 refresh token을 반환한다 - 로그인 정보가 올바를 경우 body에 access token을 반환한다 - 로그인 정보가 올바르지 않을 경우 예외를 반환한다 --- .../auction/api/auth/AuthController.java | 38 +++++- .../common/constant/ApiResponseMessages.java | 1 + .../handler/GlobalExceptionHandler.java | 6 + .../domain/auth/dto/request/LoginRequest.java | 16 +++ .../domain/auth/dto/response/AccessToken.java | 7 ++ .../auction/api/auth/AuthControllerTest.java | 114 ++++++++++++++++++ 6 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/dto/request/LoginRequest.java create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/dto/response/AccessToken.java create mode 100644 src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java diff --git a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java index e4a50046..9a29e866 100644 --- a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java +++ b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java @@ -1,18 +1,44 @@ package com.tasksprints.auction.api.auth; -import com.tasksprints.auction.common.jwt.Auth; -import com.tasksprints.auction.domain.auth.model.Accessor; +import static org.springframework.http.HttpHeaders.SET_COOKIE; + +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.common.response.ApiResult; +import com.tasksprints.auction.domain.auth.dto.request.LoginRequest; +import com.tasksprints.auction.domain.auth.dto.response.AccessToken; +import com.tasksprints.auction.domain.auth.dto.response.UserTokens; +import com.tasksprints.auction.domain.auth.service.AuthService; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequiredArgsConstructor @RequestMapping("/api/v1/auth") public class AuthController { - @GetMapping("/access") - public Boolean access(@Auth Accessor accessor) { - return true; + + private final AuthService authService; + private static final Integer COOKIE_AGE_SECONDS = 1209600; + + @PostMapping("/login") + public ResponseEntity> login(@RequestBody LoginRequest.Login login) { + UserTokens tokens = authService.login(login.email(), login.password()); + AccessToken accessToken = AccessToken.of(tokens.getAccessToken()); + + ResponseCookie cookie = ResponseCookie.from("refresh-token", tokens.getRefreshToken()) + .maxAge(COOKIE_AGE_SECONDS) + .secure(true) + .httpOnly(true) + .sameSite("None") + .path("/") + .build(); + + return ResponseEntity.ok() + .header(SET_COOKIE, cookie.toString()) + .body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, accessToken)); } } diff --git a/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java b/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java index 9b097fea..244215c3 100644 --- a/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java +++ b/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java @@ -33,6 +33,7 @@ public class ApiResponseMessages { // AUTH public static final String ACCESS_TOKEN_NOT_FOUND = "Access token Not found"; public static final String REFRESH_TOKEN_NOT_FOUND = "Refresh token not found"; + public static final String LOGIN_SUCCESS = "Login Success"; // Additional messages can be defined as needed } diff --git a/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java b/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java index 4b8a90aa..423cccbf 100644 --- a/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java @@ -6,6 +6,7 @@ import com.tasksprints.auction.domain.auction.exception.AuctionEndedException; import com.tasksprints.auction.domain.auction.exception.AuctionNotFoundException; import com.tasksprints.auction.domain.auction.exception.InvalidAuctionTimeException; +import com.tasksprints.auction.domain.auth.exception.AuthException; import com.tasksprints.auction.domain.bid.exception.BidNotFoundException; import com.tasksprints.auction.domain.bid.exception.InvalidBidAmountException; import com.tasksprints.auction.domain.product.exception.ProductNotFoundException; @@ -75,4 +76,9 @@ public ResponseEntity> handleIllegalStateException(IllegalStat public ResponseEntity> handleRuntimeException(RuntimeException ex) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ApiResult.failure(ex.getMessage())); } + + @ExceptionHandler(AuthException.class) + public ResponseEntity> handleAuthException(AuthException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResult.failure(ApiResponseMessages.USER_NOT_FOUND)); + } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/dto/request/LoginRequest.java b/src/main/java/com/tasksprints/auction/domain/auth/dto/request/LoginRequest.java new file mode 100644 index 00000000..8f87b031 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/dto/request/LoginRequest.java @@ -0,0 +1,16 @@ +package com.tasksprints.auction.domain.auth.dto.request; + +import lombok.Builder; + + +public class LoginRequest { + @Builder + public record Login(String email, String password) { + public static Login of(String email, String password) { + return Login.builder() + .email(email) + .password(password) + .build(); + } + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/dto/response/AccessToken.java b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/AccessToken.java new file mode 100644 index 00000000..9f0dc450 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/AccessToken.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.dto.response; + +public record AccessToken(String accessToken) { + public static AccessToken of(String accessToken) { + return new AccessToken(accessToken); + } +} diff --git a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java new file mode 100644 index 00000000..4177844e --- /dev/null +++ b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java @@ -0,0 +1,114 @@ +package com.tasksprints.auction.api.auth; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tasksprints.auction.api.BaseControllerTest; +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.domain.auth.dto.request.LoginRequest; +import com.tasksprints.auction.domain.auth.dto.request.LoginRequest.Login; +import com.tasksprints.auction.domain.auth.dto.response.UserTokens; +import com.tasksprints.auction.domain.auth.exception.AuthException; +import com.tasksprints.auction.domain.auth.service.AuthService; +import com.tasksprints.auction.domain.user.exception.UserNotFoundException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + + +@WebMvcTest(AuthController.class) +@MockBean(JpaMetamodelMappingContext.class) +class AuthControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private AuthService authService; + + @Autowired + private ObjectMapper objectMapper; + + + @Nested + @DisplayName("test login") + class LoginTest { + @Test + @DisplayName("Return refresh and access token, when login success") + void login_success() throws Exception { + // given + UserTokens tokens = UserTokens.of("accessTokenValue", "refreshTokenValue"); + LoginRequest.Login request = new Login("example@email.com", "password"); + when(authService.login(any(), any())).thenReturn(tokens); + + // when + ResultActions resultActions = mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + // then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.accessToken").value("accessTokenValue")) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.LOGIN_SUCCESS)) + .andExpect(header().string("set-cookie", containsString("refresh-token=refreshTokenValue"))) + .andExpect(header().string("set-cookie", containsString("Max-Age=1209600"))) + .andExpect(header().string("set-cookie", containsString("Secure"))) + .andExpect(header().string("set-cookie", containsString("HttpOnly"))) + .andExpect(header().string("set-cookie", containsString("SameSite=None"))); + } + + @Test + @DisplayName("Throw Exception, when password is different") + void loginFailWhenPasswordIsDifferent() throws Exception { + // given + LoginRequest.Login request = new Login("example@email.com", "password"); + when(authService.login(any(), any())).thenThrow(AuthException.class); + + // when + ResultActions resultActions = mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + // then + resultActions + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.USER_NOT_FOUND)); + } + + @Test + @DisplayName("Throw Exception, when email is different") + void loginFailWhenEmailIsDifferent() throws Exception { + // given + LoginRequest.Login request = new Login("example@email.com", "password"); + when(authService.login(any(), any())).thenThrow(UserNotFoundException.class); + + // when + ResultActions resultActions = mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + // then + resultActions + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.USER_NOT_FOUND)); + } + } +} + From f13cc9eeca504b125a70813b55bf951ca97cf373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Tue, 19 Nov 2024 01:58:31 +0900 Subject: [PATCH 21/27] refactor : Refactor auth domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthServiceImpl에 치중된 책임을 RefreshTokenServiceImpl을 통해 덜어낸다 - cookie 생성 로직을 여러번 사용하기 위해, service 레이어에 작성한다 - UserTokens라는 DTO가 AccessToken DTO를 가질 수 있게한다 --- .../auction/api/auth/AuthController.java | 14 +----- .../auction/common/jwt/JwtProvider.java | 10 ++++- .../domain/auth/dto/response/UserTokens.java | 4 +- .../domain/auth/model/RefreshToken.java | 2 +- .../domain/auth/service/AuthService.java | 3 ++ .../domain/auth/service/AuthServiceImpl.java | 25 ++++++++--- .../auth/service/RefreshTokenService.java | 7 +++ .../auth/service/RefreshTokenServiceImpl.java | 21 +++++++++ .../auction/api/auth/AuthControllerTest.java | 21 ++++++--- .../auction/common/jwt/JwtProviderTest.java | 6 +-- .../auth/service/AuthServiceImplTest.java | 43 ++++++++++++++---- .../service/RefreshTokenServiceImplTest.java | 45 +++++++++++++++++++ 12 files changed, 160 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java create mode 100644 src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java diff --git a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java index 9a29e866..a58f6c39 100644 --- a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java +++ b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java @@ -20,25 +20,15 @@ @RequiredArgsConstructor @RequestMapping("/api/v1/auth") public class AuthController { - private final AuthService authService; - private static final Integer COOKIE_AGE_SECONDS = 1209600; @PostMapping("/login") public ResponseEntity> login(@RequestBody LoginRequest.Login login) { UserTokens tokens = authService.login(login.email(), login.password()); - AccessToken accessToken = AccessToken.of(tokens.getAccessToken()); - - ResponseCookie cookie = ResponseCookie.from("refresh-token", tokens.getRefreshToken()) - .maxAge(COOKIE_AGE_SECONDS) - .secure(true) - .httpOnly(true) - .sameSite("None") - .path("/") - .build(); + ResponseCookie cookie = authService.getResponseCookie(tokens.getRefreshToken()); return ResponseEntity.ok() .header(SET_COOKIE, cookie.toString()) - .body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, accessToken)); + .body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, tokens.getAccessToken())); } } diff --git a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java index e6ab226e..aaf55948 100644 --- a/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java +++ b/src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java @@ -3,6 +3,7 @@ import static com.tasksprints.auction.common.util.TimeUtil.*; import com.tasksprints.auction.common.config.JwtConfig; +import com.tasksprints.auction.domain.auth.dto.response.AccessToken; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Header; @@ -24,9 +25,14 @@ public class JwtProvider { private final Clock clock; public UserTokens generateToken(String subject) { + String accessTokenValue = createToken(subject, jwtConfig.getAccessExpireMs()); + AccessToken accessToken = AccessToken.of(accessTokenValue); + + String refreshToken = createToken(EMPTY_SUBJECT, jwtConfig.getRefreshExpireMs()); + return UserTokens.of( - createToken(subject, jwtConfig.getAccessExpireMs()), - createToken(EMPTY_SUBJECT, jwtConfig.getRefreshExpireMs()) + accessToken, + refreshToken ); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/dto/response/UserTokens.java b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/UserTokens.java index ad91a135..985065af 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/dto/response/UserTokens.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/UserTokens.java @@ -7,9 +7,9 @@ @AllArgsConstructor public class UserTokens { private String refreshToken; - private String accessToken; + private AccessToken accessToken; - public static UserTokens of(String accessToken, String refreshToken) { + public static UserTokens of(AccessToken accessToken, String refreshToken) { return UserTokens.builder() .accessToken(accessToken) .refreshToken(refreshToken) diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java index 0a3e22f9..a68c0911 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java @@ -21,7 +21,7 @@ public class RefreshToken { @Column private Long memberId; - public static RefreshToken create(String id, Long memberId) { + public static RefreshToken of(String id, Long memberId) { return RefreshToken.builder() .id(id) .memberId(memberId) diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java index 3ecebf36..59673e99 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java @@ -1,7 +1,10 @@ package com.tasksprints.auction.domain.auth.service; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; +import org.springframework.http.ResponseCookie; public interface AuthService { UserTokens login(String email, String password); + + ResponseCookie getResponseCookie(String refreshToken); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java index a4c88056..51754048 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java @@ -3,22 +3,22 @@ import com.tasksprints.auction.common.jwt.JwtProvider; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; -import com.tasksprints.auction.domain.auth.model.RefreshToken; -import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; import com.tasksprints.auction.domain.user.service.UserService; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - -@RequiredArgsConstructor @Service +@RequiredArgsConstructor @Transactional(readOnly = true) public class AuthServiceImpl implements AuthService { private final UserService userService; private final JwtProvider jwtProvider; - private final RefreshTokenRepository refreshTokenRepository; + private final RefreshTokenService refreshTokenService; + + private static final Integer COOKIE_AGE_SECONDS = 1209600; @Transactional @Override @@ -30,8 +30,19 @@ public UserTokens login(String email, String password) { } UserTokens userTokens = jwtProvider.generateToken(userDetail.getId().toString()); - RefreshToken refreshToken = RefreshToken.create(userTokens.getRefreshToken(), userDetail.getId()); - refreshTokenRepository.save(refreshToken); + refreshTokenService.saveRefreshToken(userTokens.getRefreshToken(), userDetail.getId()); + return userTokens; } + + @Override + public ResponseCookie getResponseCookie(String refreshToken) { + return ResponseCookie.from("refresh-token", refreshToken) + .maxAge(COOKIE_AGE_SECONDS) + .secure(true) + .httpOnly(true) + .sameSite("None") + .path("/") + .build(); + } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java new file mode 100644 index 00000000..ce648085 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.auth.service; + +import com.tasksprints.auction.domain.auth.model.RefreshToken; + +public interface RefreshTokenService { + RefreshToken saveRefreshToken(String refreshTokenValue, Long userId); +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java new file mode 100644 index 00000000..d5893f3e --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java @@ -0,0 +1,21 @@ +package com.tasksprints.auction.domain.auth.service; + +import com.tasksprints.auction.domain.auth.model.RefreshToken; +import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class RefreshTokenServiceImpl implements RefreshTokenService { + private final RefreshTokenRepository refreshTokenRepository; + + @Override + @Transactional + public RefreshToken saveRefreshToken(String refreshTokenValue, Long userId) { + RefreshToken refreshToken = RefreshToken.of(refreshTokenValue, userId); + return refreshTokenRepository.save(refreshToken); + } +} diff --git a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java index 4177844e..09cf76ad 100644 --- a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java @@ -10,6 +10,7 @@ import com.tasksprints.auction.common.constant.ApiResponseMessages; import com.tasksprints.auction.domain.auth.dto.request.LoginRequest; import com.tasksprints.auction.domain.auth.dto.request.LoginRequest.Login; +import com.tasksprints.auction.domain.auth.dto.response.AccessToken; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; import com.tasksprints.auction.domain.auth.service.AuthService; @@ -22,16 +23,14 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.http.MediaType; +import org.springframework.http.ResponseCookie; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; -import org.springframework.test.web.servlet.result.MockMvcResultMatchers; @WebMvcTest(AuthController.class) @MockBean(JpaMetamodelMappingContext.class) class AuthControllerTest extends BaseControllerTest { - @Autowired private MockMvc mockMvc; @@ -45,13 +44,25 @@ class AuthControllerTest extends BaseControllerTest { @Nested @DisplayName("test login") class LoginTest { + + private final ResponseCookie responseCookie = ResponseCookie.from("refresh-token", "refreshTokenValue") + .maxAge(3600) + .secure(true) + .httpOnly(true) + .sameSite("None") + .path("/") + .build(); + + @Test @DisplayName("Return refresh and access token, when login success") void login_success() throws Exception { // given - UserTokens tokens = UserTokens.of("accessTokenValue", "refreshTokenValue"); + AccessToken accessToken = AccessToken.of("accessTokenValue"); + UserTokens tokens = UserTokens.of(accessToken, "refreshTokenValue"); LoginRequest.Login request = new Login("example@email.com", "password"); when(authService.login(any(), any())).thenReturn(tokens); + when(authService.getResponseCookie(any())).thenReturn(responseCookie); // when ResultActions resultActions = mockMvc.perform(post("/api/v1/auth/login") @@ -66,7 +77,7 @@ void login_success() throws Exception { .andExpect(jsonPath("$.data.accessToken").value("accessTokenValue")) .andExpect(jsonPath("$.message").value(ApiResponseMessages.LOGIN_SUCCESS)) .andExpect(header().string("set-cookie", containsString("refresh-token=refreshTokenValue"))) - .andExpect(header().string("set-cookie", containsString("Max-Age=1209600"))) + .andExpect(header().string("set-cookie", containsString("Max-Age=3600"))) .andExpect(header().string("set-cookie", containsString("Secure"))) .andExpect(header().string("set-cookie", containsString("HttpOnly"))) .andExpect(header().string("set-cookie", containsString("SameSite=None"))); diff --git a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java index afbfa45d..96860980 100644 --- a/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java +++ b/src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java @@ -74,7 +74,7 @@ void verifyToken_valid() { // then Assertions.assertDoesNotThrow(() -> { - jwtProvider.validateToken(userTokens.getAccessToken()); + jwtProvider.validateToken(userTokens.getAccessToken().accessToken()); }); Assertions.assertDoesNotThrow(() -> { jwtProvider.validateToken(userTokens.getRefreshToken()); @@ -97,7 +97,7 @@ void verifyToken_expired() { }, "리프레시토큰이 즉시 만료되어야 합니다."); Assertions.assertThrows(ExpiredJwtException.class, () -> { - jwtProvider.validateToken(userTokens.getAccessToken()); + jwtProvider.validateToken(userTokens.getAccessToken().accessToken()); }, "액세스토큰이 즉시 만료되어야 합니다."); } @@ -109,7 +109,7 @@ void getClaims() { UserTokens userTokens = jwtProvider.generateToken("1L"); // when - String decodedUserId = jwtProvider.getSubject(userTokens.getAccessToken()); + String decodedUserId = jwtProvider.getSubject(userTokens.getAccessToken().accessToken()); // then assertThat(decodedUserId).isEqualTo("1L"); diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java index 72e263a7..66f47521 100644 --- a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java @@ -5,12 +5,14 @@ import static org.mockito.Mockito.*; import com.tasksprints.auction.common.jwt.JwtProvider; +import com.tasksprints.auction.domain.auth.dto.response.AccessToken; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; -import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; import com.tasksprints.auction.domain.user.model.User; import com.tasksprints.auction.domain.user.service.UserService; +import java.time.Duration; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -19,6 +21,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseCookie; @ExtendWith(MockitoExtension.class) @@ -26,11 +29,10 @@ class AuthServiceImplTest { @Mock UserService userService; - @Mock JwtProvider jwtProvider; @Mock - RefreshTokenRepository refreshTokenRepository; + RefreshTokenService refreshTokenService; @InjectMocks private AuthServiceImpl authService; @@ -50,18 +52,19 @@ void setUp() { } @Nested - @DisplayName("Login success when password is correct") - class testLogin { + @DisplayName("Login success test") + class TestLogin { @Test @DisplayName("Return tokens when password is correct") - void ReturnTokensWhenPasswordIsCorrect() { + void returnTokensWhenPasswordIsCorrect() { // given String email = "user@exapmle.com"; String password = "password"; - UserTokens expected = UserTokens.of("accessToken", "refreshToken"); + AccessToken accessToken = AccessToken.of("accessToken"); + UserTokens expected = UserTokens.of(accessToken, "refreshToken"); when(userService.getUserDetailByEmail(any())).thenReturn(userDetail); when(jwtProvider.generateToken(any())).thenReturn(expected); - when(refreshTokenRepository.save(any())).thenReturn(any()); + when(refreshTokenService.saveRefreshToken(any(), any())).thenReturn(any()); // when UserTokens actual = authService.login(email, password); @@ -72,7 +75,7 @@ void ReturnTokensWhenPasswordIsCorrect() { } @Test - @DisplayName("should throw exception when password is different") + @DisplayName("Should throw exception when password is different") void shouldReturnExceptionWhenPasswordIsDifferent() { // given String email = "user@exapmle.com"; @@ -88,4 +91,26 @@ void shouldReturnExceptionWhenPasswordIsDifferent() { assertEquals("password is not correct", exception.getMessage()); } } + + @Nested + @DisplayName("Get response cookie test") + class TestResponseCookie { + + @Test + @DisplayName("Return ResponseCookie, when creating the cookie successfully") + void returnResponseCookie_success() { + // given + String refreshToken = "refreshTokenValue"; + + // when + ResponseCookie responseCookie = authService.getResponseCookie(refreshToken); + + // then + Assertions.assertEquals(1209600, responseCookie.getMaxAge().toSeconds()); + Assertions.assertTrue(responseCookie.isSecure()); + Assertions.assertTrue(responseCookie.isHttpOnly()); + Assertions.assertEquals("None", responseCookie.getSameSite()); + Assertions.assertEquals("/", responseCookie.getPath()); + } + } } diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java new file mode 100644 index 00000000..5aa3abb0 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java @@ -0,0 +1,45 @@ +package com.tasksprints.auction.domain.auth.service; + +import static org.mockito.Mockito.*; + +import com.tasksprints.auction.domain.auth.model.RefreshToken; +import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class RefreshTokenServiceImplTest { + + @Mock + RefreshTokenRepository refreshTokenRepository; + + @InjectMocks + RefreshTokenServiceImpl refreshTokenService; + + @Nested + @DisplayName("Save RefreshToken test") + class saveRefreshTokenTest { + @Test + @DisplayName("should return RefreshToken, when the test is success") + void testSaveRefreshToken_success() { + // given + String refreshTokenValue = "refreshToken"; + Long userId = 1L; + RefreshToken expectedRefreshToken = RefreshToken.of(refreshTokenValue, userId); + when(refreshTokenRepository.save(any())).thenReturn(expectedRefreshToken); + + // when + RefreshToken actualRefreshToken = refreshTokenService.saveRefreshToken(refreshTokenValue, userId); + + // then + Assertions.assertEquals(expectedRefreshToken.getMemberId(), actualRefreshToken.getMemberId()); + Assertions.assertEquals(expectedRefreshToken.getId(), actualRefreshToken.getId()); + } + } +} From b6ee0b33c51a992e1dd36bcb3bb68830edea8955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Tue, 19 Nov 2024 02:52:25 +0900 Subject: [PATCH 22/27] refactor : Refactor auth service layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthServiceImpl의 login 메서드 하나에 존재했던 책임들을 여러 메서드로 분리한다 --- .../auction/api/auth/AuthController.java | 3 +- .../domain/auth/service/AuthService.java | 4 +- .../domain/auth/service/AuthServiceImpl.java | 15 +++-- .../auction/api/auth/AuthControllerTest.java | 10 ++-- .../auth/service/AuthServiceImplTest.java | 60 ++++++++++++------- 5 files changed, 59 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java index a58f6c39..016e471b 100644 --- a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java +++ b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java @@ -24,7 +24,8 @@ public class AuthController { @PostMapping("/login") public ResponseEntity> login(@RequestBody LoginRequest.Login login) { - UserTokens tokens = authService.login(login.email(), login.password()); + Long userId = authService.validateLogin(login.email(), login.password()); + UserTokens tokens = authService.issueTokens(userId); ResponseCookie cookie = authService.getResponseCookie(tokens.getRefreshToken()); return ResponseEntity.ok() diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java index 59673e99..edc08d46 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java @@ -4,7 +4,9 @@ import org.springframework.http.ResponseCookie; public interface AuthService { - UserTokens login(String email, String password); + Long validateLogin(String email, String password); ResponseCookie getResponseCookie(String refreshToken); + + UserTokens issueTokens(Long userId); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java index 51754048..8dd37d19 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java @@ -20,19 +20,22 @@ public class AuthServiceImpl implements AuthService { private static final Integer COOKIE_AGE_SECONDS = 1209600; - @Transactional @Override - public UserTokens login(String email, String password) { + public Long validateLogin(String email, String password) { UserDetailResponse userDetail = userService.getUserDetailByEmail(email); if (!password.equals(userDetail.getPassword())) { throw new AuthException("password is not correct"); } + return userDetail.getId(); + } - UserTokens userTokens = jwtProvider.generateToken(userDetail.getId().toString()); - refreshTokenService.saveRefreshToken(userTokens.getRefreshToken(), userDetail.getId()); - - return userTokens; + @Transactional + @Override + public UserTokens issueTokens(Long userId) { + UserTokens tokens = jwtProvider.generateToken(userId.toString()); + refreshTokenService.saveRefreshToken(tokens.getRefreshToken(), userId); + return tokens; } @Override diff --git a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java index 09cf76ad..0ae55fd5 100644 --- a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java @@ -42,9 +42,8 @@ class AuthControllerTest extends BaseControllerTest { @Nested - @DisplayName("test login") + @DisplayName("Test login") class LoginTest { - private final ResponseCookie responseCookie = ResponseCookie.from("refresh-token", "refreshTokenValue") .maxAge(3600) .secure(true) @@ -61,7 +60,8 @@ void login_success() throws Exception { AccessToken accessToken = AccessToken.of("accessTokenValue"); UserTokens tokens = UserTokens.of(accessToken, "refreshTokenValue"); LoginRequest.Login request = new Login("example@email.com", "password"); - when(authService.login(any(), any())).thenReturn(tokens); + when(authService.validateLogin(any(), any())).thenReturn(1L); + when(authService.issueTokens(any())).thenReturn(tokens); when(authService.getResponseCookie(any())).thenReturn(responseCookie); // when @@ -88,7 +88,7 @@ void login_success() throws Exception { void loginFailWhenPasswordIsDifferent() throws Exception { // given LoginRequest.Login request = new Login("example@email.com", "password"); - when(authService.login(any(), any())).thenThrow(AuthException.class); + when(authService.validateLogin(any(), any())).thenThrow(AuthException.class); // when ResultActions resultActions = mockMvc.perform(post("/api/v1/auth/login") @@ -107,7 +107,7 @@ void loginFailWhenPasswordIsDifferent() throws Exception { void loginFailWhenEmailIsDifferent() throws Exception { // given LoginRequest.Login request = new Login("example@email.com", "password"); - when(authService.login(any(), any())).thenThrow(UserNotFoundException.class); + when(authService.validateLogin(any(), any())).thenThrow(UserNotFoundException.class); // when ResultActions resultActions = mockMvc.perform(post("/api/v1/auth/login") diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java index 66f47521..9e9f514b 100644 --- a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java @@ -9,9 +9,9 @@ import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; +import com.tasksprints.auction.domain.user.exception.UserNotFoundException; import com.tasksprints.auction.domain.user.model.User; import com.tasksprints.auction.domain.user.service.UserService; -import java.time.Duration; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -38,8 +38,11 @@ class AuthServiceImplTest { private UserDetailResponse userDetail; + private String loginEmail; + @BeforeEach void setUp() { + loginEmail = "user@exapmle.com"; User existingUser = User.builder() .id(1L) .email("user@exapmle.com") @@ -52,39 +55,32 @@ void setUp() { } @Nested - @DisplayName("Login success test") + @DisplayName("Validate login information test") class TestLogin { @Test - @DisplayName("Return tokens when password is correct") - void returnTokensWhenPasswordIsCorrect() { + @DisplayName("Return userDetails, when password same") + void validateLogin_success() { // given - String email = "user@exapmle.com"; String password = "password"; - AccessToken accessToken = AccessToken.of("accessToken"); - UserTokens expected = UserTokens.of(accessToken, "refreshToken"); when(userService.getUserDetailByEmail(any())).thenReturn(userDetail); - when(jwtProvider.generateToken(any())).thenReturn(expected); - when(refreshTokenService.saveRefreshToken(any(), any())).thenReturn(any()); // when - UserTokens actual = authService.login(email, password); + Long actualUserId = authService.validateLogin(loginEmail, password); // then - assertEquals(expected.getRefreshToken(), actual.getRefreshToken()); - assertEquals(expected.getAccessToken(), actual.getAccessToken()); + assertEquals(1L, actualUserId); } @Test @DisplayName("Should throw exception when password is different") - void shouldReturnExceptionWhenPasswordIsDifferent() { + void validateLoginDifferentPassword() { // given - String email = "user@exapmle.com"; String password = "differentPassword"; when(userService.getUserDetailByEmail(any())).thenReturn(userDetail); // when AuthException exception = assertThrows(AuthException.class, () -> { - authService.login(email, password); + authService.validateLogin(loginEmail, password); }); // then @@ -92,6 +88,30 @@ void shouldReturnExceptionWhenPasswordIsDifferent() { } } + @Nested + @DisplayName("Issue tokens test") + class TestIssueTokens { + + @Test + @DisplayName("Return tokens, when issue tokens successfully") + void returnTokens_success() { + // given + Long userId = 1L; + AccessToken accessToken = AccessToken.of("accessTokenValue"); + String refreshToken = "refreshTokenValue"; + UserTokens generated = UserTokens.of(accessToken, refreshToken); + when(jwtProvider.generateToken(any())).thenReturn(generated); + when(refreshTokenService.saveRefreshToken(any(), any())).thenReturn(any()); + + // when + UserTokens issued = authService.issueTokens(userId); + + // then + assertEquals(generated.getAccessToken().accessToken(), issued.getAccessToken().accessToken()); + assertEquals(refreshToken, issued.getRefreshToken()); + } + } + @Nested @DisplayName("Get response cookie test") class TestResponseCookie { @@ -106,11 +126,11 @@ void returnResponseCookie_success() { ResponseCookie responseCookie = authService.getResponseCookie(refreshToken); // then - Assertions.assertEquals(1209600, responseCookie.getMaxAge().toSeconds()); - Assertions.assertTrue(responseCookie.isSecure()); - Assertions.assertTrue(responseCookie.isHttpOnly()); - Assertions.assertEquals("None", responseCookie.getSameSite()); - Assertions.assertEquals("/", responseCookie.getPath()); + assertEquals(1209600, responseCookie.getMaxAge().toSeconds()); + assertTrue(responseCookie.isSecure()); + assertTrue(responseCookie.isHttpOnly()); + assertEquals("None", responseCookie.getSameSite()); + assertEquals("/", responseCookie.getPath()); } } } From 9d1bfaa94157fb1a351d158799ac265714593c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Wed, 20 Nov 2024 02:14:53 +0900 Subject: [PATCH 23/27] refactor : Refactor refresh token from cookie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - refreshToken을 쿠키에서 추출하고, 쿠키 설정까지의 과정을 하나의 클래스로 묶는다 - 그로인해 변경되는 코드를 변경한다 - 후에, 토큰들을 한번에 발급하고 반환하는 과정이 중복 될 것이기 때문에, 그 부분을 issueResponseTokens 메서드로 통합하여 사용성을 높인다 --- .../auction/api/auth/AuthController.java | 10 +- .../resolver/AuthenticationResolver.java | 5 +- .../auth/dto/response/ResponseTokens.java | 14 +++ .../domain/auth/service/AuthService.java | 7 +- .../domain/auth/service/AuthServiceImpl.java | 18 +--- .../RefreshTokenCookieManager.java} | 22 +++-- .../auth/service/RefreshTokenService.java | 3 + .../auth/service/RefreshTokenServiceImpl.java | 7 ++ .../auction/api/auth/AuthControllerTest.java | 5 +- .../auction/common/config/TestAuthConfig.java | 5 +- .../auth/RefreshTokenExtractorTest.java | 65 ------------- .../auth/service/AuthServiceImplTest.java | 63 ++++++------ .../RefreshTokenCookieManagerTest.java | 97 +++++++++++++++++++ 13 files changed, 188 insertions(+), 133 deletions(-) create mode 100644 src/main/java/com/tasksprints/auction/domain/auth/dto/response/ResponseTokens.java rename src/main/java/com/tasksprints/auction/domain/auth/{RefreshTokenExtractor.java => service/RefreshTokenCookieManager.java} (65%) delete mode 100644 src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java create mode 100644 src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenCookieManagerTest.java diff --git a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java index 016e471b..1446913e 100644 --- a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java +++ b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java @@ -6,10 +6,9 @@ import com.tasksprints.auction.common.response.ApiResult; import com.tasksprints.auction.domain.auth.dto.request.LoginRequest; import com.tasksprints.auction.domain.auth.dto.response.AccessToken; -import com.tasksprints.auction.domain.auth.dto.response.UserTokens; +import com.tasksprints.auction.domain.auth.dto.response.ResponseTokens; import com.tasksprints.auction.domain.auth.service.AuthService; import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; @@ -25,11 +24,10 @@ public class AuthController { @PostMapping("/login") public ResponseEntity> login(@RequestBody LoginRequest.Login login) { Long userId = authService.validateLogin(login.email(), login.password()); - UserTokens tokens = authService.issueTokens(userId); - ResponseCookie cookie = authService.getResponseCookie(tokens.getRefreshToken()); + ResponseTokens responseTokens = authService.issueResponseTokens(userId); return ResponseEntity.ok() - .header(SET_COOKIE, cookie.toString()) - .body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, tokens.getAccessToken())); + .header(SET_COOKIE, responseTokens.refreshToken().toString()) + .body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, responseTokens.accessToken())); } } diff --git a/src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java b/src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java index 56064687..6715a407 100644 --- a/src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java +++ b/src/main/java/com/tasksprints/auction/common/resolver/AuthenticationResolver.java @@ -5,6 +5,7 @@ import com.tasksprints.auction.domain.auth.TokenExtractor; import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; import com.tasksprints.auction.domain.auth.model.Accessor; +import com.tasksprints.auction.domain.auth.service.RefreshTokenCookieManager; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; @@ -18,7 +19,7 @@ @RequiredArgsConstructor public class AuthenticationResolver implements HandlerMethodArgumentResolver { private final JwtProvider jwtProvider; - private final TokenExtractor refreshTokenExtractor; + private final RefreshTokenCookieManager refreshTokenCookieManager; private final TokenExtractor accessTokenExtractor; @Override @@ -38,7 +39,7 @@ public Object resolveArgument(MethodParameter parameter, throw new IllegalArgumentException(); } try { - String refreshToken = refreshTokenExtractor.extractToken(request); + String refreshToken = refreshTokenCookieManager.extractRefreshToken(request); String accessToken = accessTokenExtractor.extractToken(request); jwtProvider.validateToken(accessToken); diff --git a/src/main/java/com/tasksprints/auction/domain/auth/dto/response/ResponseTokens.java b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/ResponseTokens.java new file mode 100644 index 00000000..919123b1 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/auth/dto/response/ResponseTokens.java @@ -0,0 +1,14 @@ +package com.tasksprints.auction.domain.auth.dto.response; + +import lombok.Builder; +import org.springframework.http.ResponseCookie; + +@Builder +public record ResponseTokens(AccessToken accessToken, ResponseCookie refreshToken) { + public static ResponseTokens of(AccessToken accessToken, ResponseCookie refreshToken) { + return ResponseTokens.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java index edc08d46..cd371509 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java @@ -1,12 +1,9 @@ package com.tasksprints.auction.domain.auth.service; -import com.tasksprints.auction.domain.auth.dto.response.UserTokens; -import org.springframework.http.ResponseCookie; +import com.tasksprints.auction.domain.auth.dto.response.ResponseTokens; public interface AuthService { Long validateLogin(String email, String password); - ResponseCookie getResponseCookie(String refreshToken); - - UserTokens issueTokens(Long userId); + ResponseTokens issueResponseTokens(Long userId); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java index 8dd37d19..0204334b 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java @@ -1,6 +1,7 @@ package com.tasksprints.auction.domain.auth.service; import com.tasksprints.auction.common.jwt.JwtProvider; +import com.tasksprints.auction.domain.auth.dto.response.ResponseTokens; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; @@ -18,8 +19,6 @@ public class AuthServiceImpl implements AuthService { private final JwtProvider jwtProvider; private final RefreshTokenService refreshTokenService; - private static final Integer COOKIE_AGE_SECONDS = 1209600; - @Override public Long validateLogin(String email, String password) { UserDetailResponse userDetail = userService.getUserDetailByEmail(email); @@ -32,20 +31,11 @@ public Long validateLogin(String email, String password) { @Transactional @Override - public UserTokens issueTokens(Long userId) { + public ResponseTokens issueResponseTokens(Long userId) { UserTokens tokens = jwtProvider.generateToken(userId.toString()); refreshTokenService.saveRefreshToken(tokens.getRefreshToken(), userId); - return tokens; - } + ResponseCookie refreshToken = refreshTokenService.getResponseRefreshToken(tokens.getRefreshToken()); - @Override - public ResponseCookie getResponseCookie(String refreshToken) { - return ResponseCookie.from("refresh-token", refreshToken) - .maxAge(COOKIE_AGE_SECONDS) - .secure(true) - .httpOnly(true) - .sameSite("None") - .path("/") - .build(); + return ResponseTokens.of(tokens.getAccessToken(), refreshToken); } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenCookieManager.java similarity index 65% rename from src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java rename to src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenCookieManager.java index f512be34..d84f41f9 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractor.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenCookieManager.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.domain.auth; +package com.tasksprints.auction.domain.auth.service; import static com.tasksprints.auction.common.constant.ApiResponseMessages.REFRESH_TOKEN_NOT_FOUND; @@ -8,18 +8,18 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; @Component -@Qualifier("refreshTokenExtractor") @RequiredArgsConstructor -public class RefreshTokenExtractor implements TokenExtractor { +public class RefreshTokenCookieManager { + private static final Integer COOKIE_AGE_SECONDS = 1209600; private static final String COOKIE_NAME = "refresh-token"; private final RefreshTokenRepository refreshTokenRepository; - @Override - public String extractToken(HttpServletRequest request) { + + public String extractRefreshToken(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies == null) { @@ -32,4 +32,14 @@ public String extractToken(HttpServletRequest request) { .orElseThrow(() -> new RefreshTokenException(REFRESH_TOKEN_NOT_FOUND)) .getValue(); } + + public ResponseCookie createResponseCookie(String refreshToken) { + return ResponseCookie.from(COOKIE_NAME, refreshToken) + .maxAge(COOKIE_AGE_SECONDS) + .secure(true) + .httpOnly(true) + .sameSite("None") + .path("/") + .build(); + } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java index ce648085..e9d1ebc0 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java @@ -1,7 +1,10 @@ package com.tasksprints.auction.domain.auth.service; import com.tasksprints.auction.domain.auth.model.RefreshToken; +import org.springframework.http.ResponseCookie; public interface RefreshTokenService { RefreshToken saveRefreshToken(String refreshTokenValue, Long userId); + + ResponseCookie getResponseRefreshToken(String refresh); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java index d5893f3e..e5657a99 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java @@ -3,6 +3,7 @@ import com.tasksprints.auction.domain.auth.model.RefreshToken; import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -11,6 +12,7 @@ @Transactional(readOnly = true) public class RefreshTokenServiceImpl implements RefreshTokenService { private final RefreshTokenRepository refreshTokenRepository; + private final RefreshTokenCookieManager cookieManager; @Override @Transactional @@ -18,4 +20,9 @@ public RefreshToken saveRefreshToken(String refreshTokenValue, Long userId) { RefreshToken refreshToken = RefreshToken.of(refreshTokenValue, userId); return refreshTokenRepository.save(refreshToken); } + + @Override + public ResponseCookie getResponseRefreshToken(String refreshToken) { + return cookieManager.createResponseCookie(refreshToken); + } } diff --git a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java index 0ae55fd5..7c910141 100644 --- a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java @@ -11,6 +11,7 @@ import com.tasksprints.auction.domain.auth.dto.request.LoginRequest; import com.tasksprints.auction.domain.auth.dto.request.LoginRequest.Login; import com.tasksprints.auction.domain.auth.dto.response.AccessToken; +import com.tasksprints.auction.domain.auth.dto.response.ResponseTokens; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; import com.tasksprints.auction.domain.auth.service.AuthService; @@ -60,9 +61,9 @@ void login_success() throws Exception { AccessToken accessToken = AccessToken.of("accessTokenValue"); UserTokens tokens = UserTokens.of(accessToken, "refreshTokenValue"); LoginRequest.Login request = new Login("example@email.com", "password"); + ResponseTokens responseTokens = ResponseTokens.of(accessToken, responseCookie); when(authService.validateLogin(any(), any())).thenReturn(1L); - when(authService.issueTokens(any())).thenReturn(tokens); - when(authService.getResponseCookie(any())).thenReturn(responseCookie); + when(authService.issueResponseTokens(any())).thenReturn(responseTokens); // when ResultActions resultActions = mockMvc.perform(post("/api/v1/auth/login") diff --git a/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java b/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java index ef4c6be9..b2741959 100644 --- a/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java +++ b/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java @@ -2,6 +2,7 @@ import com.tasksprints.auction.common.jwt.JwtProvider; import com.tasksprints.auction.domain.auth.TokenExtractor; +import com.tasksprints.auction.domain.auth.service.RefreshTokenCookieManager; import org.mockito.Mockito; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; @@ -14,8 +15,8 @@ public JwtProvider jwtProvider() { } @Bean - public TokenExtractor refreshTokenExtractor() { - return Mockito.mock(TokenExtractor.class); + public RefreshTokenCookieManager refreshTokenCookieManager() { + return Mockito.mock(RefreshTokenCookieManager.class); } @Bean diff --git a/src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java b/src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java deleted file mode 100644 index e267f204..00000000 --- a/src/test/java/com/tasksprints/auction/domain/auth/RefreshTokenExtractorTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.tasksprints.auction.domain.auth; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; -import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -class RefreshTokenExtractorTest { - @Mock - private RefreshTokenRepository refreshTokenRepository; - - @InjectMocks - private RefreshTokenExtractor tokenExtractor; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } - - @Test - @DisplayName("쿠키에서 refresh token 을 꺼내서 반환한다.") - void testExtractToken_success() { - // given - Cookie[] cookies = { - new Cookie("nothing", "token"), - new Cookie("refresh-token", "tokenName"), - }; - HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getCookies()).thenReturn(cookies); - when(refreshTokenRepository.existsById("tokenName")).thenReturn(true); - - // when - String resultValue = tokenExtractor.extractToken(request); - - // then - assertThat(resultValue).isEqualTo("tokenName"); - } - - @Test - @DisplayName("쿠키에 refresh token이 존재하지 않으면 예외를 반환한다.") - void testExtractToken_fail() { - // given - Cookie[] cookies = { - new Cookie("nothing", "token"), - }; - HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getCookies()).thenReturn(cookies); - - // when, then - Assertions.assertThrows(RefreshTokenException.class, ()-> { - tokenExtractor.extractToken(request); - }, "리프레시토큰이 쿠키에 존재해야 합니다."); - } -} diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java index 9e9f514b..b06ec2f2 100644 --- a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java @@ -6,10 +6,11 @@ import com.tasksprints.auction.common.jwt.JwtProvider; import com.tasksprints.auction.domain.auth.dto.response.AccessToken; +import com.tasksprints.auction.domain.auth.dto.response.ResponseTokens; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; +import com.tasksprints.auction.domain.auth.model.RefreshToken; import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; -import com.tasksprints.auction.domain.user.exception.UserNotFoundException; import com.tasksprints.auction.domain.user.model.User; import com.tasksprints.auction.domain.user.service.UserService; import org.junit.jupiter.api.Assertions; @@ -89,48 +90,48 @@ void validateLoginDifferentPassword() { } @Nested - @DisplayName("Issue tokens test") + @DisplayName("Issue response tokens test") class TestIssueTokens { - @Test - @DisplayName("Return tokens, when issue tokens successfully") - void returnTokens_success() { - // given - Long userId = 1L; - AccessToken accessToken = AccessToken.of("accessTokenValue"); - String refreshToken = "refreshTokenValue"; - UserTokens generated = UserTokens.of(accessToken, refreshToken); - when(jwtProvider.generateToken(any())).thenReturn(generated); - when(refreshTokenService.saveRefreshToken(any(), any())).thenReturn(any()); - - // when - UserTokens issued = authService.issueTokens(userId); + public static ResponseCookie createResponseCookie(String value) { + return ResponseCookie.from("refresh-token", value) + .maxAge(3600) + .secure(true) + .httpOnly(true) + .sameSite("None") + .path("/") + .build(); + } - // then - assertEquals(generated.getAccessToken().accessToken(), issued.getAccessToken().accessToken()); - assertEquals(refreshToken, issued.getRefreshToken()); + public static AccessToken createAccessToken(String value) { + return new AccessToken(value); } - } - @Nested - @DisplayName("Get response cookie test") - class TestResponseCookie { + public static UserTokens createUserTokens(String accessTokenValue, String refreshTokenValue) { + return UserTokens.of(createAccessToken(accessTokenValue), refreshTokenValue); + } @Test - @DisplayName("Return ResponseCookie, when creating the cookie successfully") - void returnResponseCookie_success() { + @DisplayName("Return tokens, when issue tokens successfully") + void returnResponseTokens_success() { // given - String refreshToken = "refreshTokenValue"; + String refreshTokenValue = "refreshTokenValue"; + String accessTokenValue = "accessTokenValue"; + UserTokens generatedTokens = createUserTokens(accessTokenValue, refreshTokenValue); + Long userId = 1L; + RefreshToken refreshToken = new RefreshToken(refreshTokenValue, userId); + + when(jwtProvider.generateToken(any())).thenReturn(generatedTokens); + when(refreshTokenService.saveRefreshToken(any(), any())).thenReturn(refreshToken); + when(refreshTokenService.getResponseRefreshToken(any())).thenReturn(createResponseCookie(refreshTokenValue)); // when - ResponseCookie responseCookie = authService.getResponseCookie(refreshToken); + ResponseTokens responseTokens = authService.issueResponseTokens(userId); // then - assertEquals(1209600, responseCookie.getMaxAge().toSeconds()); - assertTrue(responseCookie.isSecure()); - assertTrue(responseCookie.isHttpOnly()); - assertEquals("None", responseCookie.getSameSite()); - assertEquals("/", responseCookie.getPath()); + assertNotNull(responseTokens); + assertNotNull(responseTokens.accessToken()); + assertNotNull(responseTokens.refreshToken()); } } } diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenCookieManagerTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenCookieManagerTest.java new file mode 100644 index 00000000..20710fe6 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenCookieManagerTest.java @@ -0,0 +1,97 @@ +package com.tasksprints.auction.domain.auth.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; +import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.ResponseCookie; + +class RefreshTokenCookieManagerTest { + @Mock + private RefreshTokenRepository refreshTokenRepository; + + @InjectMocks + private RefreshTokenCookieManager cookieManager; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Nested + @DisplayName("") + class TestExtractToken { + @Test + @DisplayName("Return refresh token, after extracting refresh token") + void extractRefreshToken_success() { + // given + Cookie[] cookies = { + new Cookie("nothing", "token"), + new Cookie("refresh-token", "tokenName"), + }; + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getCookies()).thenReturn(cookies); + when(refreshTokenRepository.existsById("tokenName")).thenReturn(true); + + // when + String resultValue = cookieManager.extractRefreshToken(request); + + // then + assertThat(resultValue).isEqualTo("tokenName"); + } + + @Test + @DisplayName("Should throw exception, when refresh token doesn't exist in cookie") + void extractRefreshToken_fail() { + // given + Cookie[] cookies = { + new Cookie("nothing", "token"), + }; + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getCookies()).thenReturn(cookies); + + // when, then + Assertions.assertThrows(RefreshTokenException.class, () -> { + cookieManager.extractRefreshToken(request); + }, "리프레시토큰이 쿠키에 존재해야 합니다."); + } + + } + + + @Nested + @DisplayName("Get response cookie test") + class TestResponseCookie { + + @Test + @DisplayName("Return ResponseCookie, when creating the cookie successfully") + void returnResponseCookie_success() { + // given + String refreshToken = "refreshTokenValue"; + + // when + ResponseCookie responseCookie = cookieManager.createResponseCookie(refreshToken); + + // then + assertEquals(1209600, responseCookie.getMaxAge().toSeconds()); + assertTrue(responseCookie.isSecure()); + assertTrue(responseCookie.isHttpOnly()); + assertEquals("None", responseCookie.getSameSite()); + assertEquals("/", responseCookie.getPath()); + } + } +} From 6bb43a31e938798d8b02e3575b05876f9a42e06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Wed, 20 Nov 2024 22:26:58 +0900 Subject: [PATCH 24/27] feat : Add reissueResponseTokens api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - access token이 만료될 경우, 클라이언트는, 해당 api로 토큰 재발급 요청을 보낸다 - 토큰이 repository에 존재할 경우, accesstoken과 refreshtoken을 재발급한다 - refreshtoken을 검증하고, DB에서 지우는 로직을 추가로 구현할 예정이다 --- .../auction/api/auth/AuthController.java | 11 +++++ .../domain/auth/model/RefreshToken.java | 6 +-- .../domain/auth/service/AuthService.java | 2 + .../domain/auth/service/AuthServiceImpl.java | 7 +++ .../auth/service/RefreshTokenService.java | 2 + .../auth/service/RefreshTokenServiceImpl.java | 7 +++ .../auction/api/auth/AuthControllerTest.java | 46 +++++++++++++++++++ .../auction/common/config/TestAuthConfig.java | 21 +++------ .../RefreshTokenRepositoryTest.java | 2 +- .../auth/service/AuthServiceImplTest.java | 21 ++++----- .../service/RefreshTokenServiceImplTest.java | 39 ++++++++++++++-- 11 files changed, 132 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java index 1446913e..b2714d2b 100644 --- a/src/main/java/com/tasksprints/auction/api/auth/AuthController.java +++ b/src/main/java/com/tasksprints/auction/api/auth/AuthController.java @@ -11,6 +11,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -30,4 +32,13 @@ public ResponseEntity> login(@RequestBody LoginRequest.Lo .header(SET_COOKIE, responseTokens.refreshToken().toString()) .body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, responseTokens.accessToken())); } + + @GetMapping("/reissue") + public ResponseEntity> reissueTokens(@CookieValue("refresh-token") String refreshToken) { + ResponseTokens responseTokens = authService.reissueResponseTokens(refreshToken); + + return ResponseEntity.ok() + .header(SET_COOKIE, responseTokens.refreshToken().toString()) + .body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, responseTokens.accessToken())); + } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java index a68c0911..edac44b6 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/RefreshToken.java @@ -19,12 +19,12 @@ public class RefreshToken { private String id; @Column - private Long memberId; + private Long userId; - public static RefreshToken of(String id, Long memberId) { + public static RefreshToken of(String id, Long userId) { return RefreshToken.builder() .id(id) - .memberId(memberId) + .userId(userId) .build(); } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java index cd371509..30533708 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthService.java @@ -6,4 +6,6 @@ public interface AuthService { Long validateLogin(String email, String password); ResponseTokens issueResponseTokens(Long userId); + + ResponseTokens reissueResponseTokens(String refreshToken); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java index 0204334b..f4fced71 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/AuthServiceImpl.java @@ -38,4 +38,11 @@ public ResponseTokens issueResponseTokens(Long userId) { return ResponseTokens.of(tokens.getAccessToken(), refreshToken); } + + @Transactional + @Override + public ResponseTokens reissueResponseTokens(String refreshToken) { + Long userId = refreshTokenService.findRefreshTokenById(refreshToken).getUserId(); + return issueResponseTokens(userId); + } } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java index e9d1ebc0..30e8e1c8 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenService.java @@ -7,4 +7,6 @@ public interface RefreshTokenService { RefreshToken saveRefreshToken(String refreshTokenValue, Long userId); ResponseCookie getResponseRefreshToken(String refresh); + + RefreshToken findRefreshTokenById(String refreshToken); } diff --git a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java index e5657a99..b23fb021 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImpl.java @@ -1,5 +1,6 @@ package com.tasksprints.auction.domain.auth.service; +import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; import com.tasksprints.auction.domain.auth.model.RefreshToken; import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; import lombok.RequiredArgsConstructor; @@ -25,4 +26,10 @@ public RefreshToken saveRefreshToken(String refreshTokenValue, Long userId) { public ResponseCookie getResponseRefreshToken(String refreshToken) { return cookieManager.createResponseCookie(refreshToken); } + + @Override + public RefreshToken findRefreshTokenById(String refreshToken) { + return refreshTokenRepository.findById(refreshToken) + .orElseThrow(() -> new RefreshTokenException("Invalid refresh token")); + } } diff --git a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java index 7c910141..c414961a 100644 --- a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java @@ -14,8 +14,10 @@ import com.tasksprints.auction.domain.auth.dto.response.ResponseTokens; import com.tasksprints.auction.domain.auth.dto.response.UserTokens; import com.tasksprints.auction.domain.auth.exception.AuthException; +import com.tasksprints.auction.domain.auth.model.Accessor; import com.tasksprints.auction.domain.auth.service.AuthService; import com.tasksprints.auction.domain.user.exception.UserNotFoundException; +import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -41,6 +43,16 @@ class AuthControllerTest extends BaseControllerTest { @Autowired private ObjectMapper objectMapper; + static ResponseCookie createResponseCookie() { + return ResponseCookie.from("refresh-token", "refreshTokenValue") + .maxAge(3600) + .secure(true) + .httpOnly(true) + .sameSite("None") + .path("/") + .build(); + } + @Nested @DisplayName("Test login") @@ -122,5 +134,39 @@ void loginFailWhenEmailIsDifferent() throws Exception { .andExpect(jsonPath("$.message").value(ApiResponseMessages.USER_NOT_FOUND)); } } + + @Nested + @DisplayName("Test reissueToken") + class ReissueTest { + // given + Long userId = 1L; + AccessToken accessToken = AccessToken.of("accessTokenValue"); + ResponseTokens responseTokens = ResponseTokens.of(accessToken, createResponseCookie()); + Cookie cookie = new Cookie("refresh-token", "refreshToken"); + @Test + @DisplayName("If refresh token exist in repository, return refresh token") + void testReissue_success() throws Exception { + // given + when(authService.reissueResponseTokens(any())).thenReturn(responseTokens); + + // when + ResultActions resultActions = mockMvc.perform(get("/api/v1/auth/reissue") + .contentType(MediaType.APPLICATION_JSON) + .cookie(cookie) + ); + + // then + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.accessToken").value("accessTokenValue")) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.LOGIN_SUCCESS)) + .andExpect(header().string("set-cookie", containsString("refresh-token=refreshTokenValue"))) + .andExpect(header().string("set-cookie", containsString("Max-Age=3600"))) + .andExpect(header().string("set-cookie", containsString("Secure"))) + .andExpect(header().string("set-cookie", containsString("HttpOnly"))) + .andExpect(header().string("set-cookie", containsString("SameSite=None"))); + } + } } diff --git a/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java b/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java index b2741959..e1261044 100644 --- a/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java +++ b/src/test/java/com/tasksprints/auction/common/config/TestAuthConfig.java @@ -3,24 +3,17 @@ import com.tasksprints.auction.common.jwt.JwtProvider; import com.tasksprints.auction.domain.auth.TokenExtractor; import com.tasksprints.auction.domain.auth.service.RefreshTokenCookieManager; -import org.mockito.Mockito; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; +import org.springframework.boot.test.mock.mockito.MockBean; @TestConfiguration public class TestAuthConfig { - @Bean - public JwtProvider jwtProvider() { - return Mockito.mock(JwtProvider.class); - } + @MockBean + public JwtProvider jwtProvider; - @Bean - public RefreshTokenCookieManager refreshTokenCookieManager() { - return Mockito.mock(RefreshTokenCookieManager.class); - } + @MockBean + public RefreshTokenCookieManager refreshTokenCookieManager; - @Bean - public TokenExtractor accessTokenExtractor() { - return Mockito.mock(TokenExtractor.class); - } + @MockBean + public TokenExtractor accessTokenExtractor; } diff --git a/src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java b/src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java index f4f491da..261eae51 100644 --- a/src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java +++ b/src/test/java/com/tasksprints/auction/domain/auth/repository/RefreshTokenRepositoryTest.java @@ -27,7 +27,7 @@ class RefreshTokenRepositoryTest { void setUp() { refreshToken = RefreshToken.builder() .id("testId") - .memberId(1L) + .userId(1L) .build(); } diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java index b06ec2f2..b7347c23 100644 --- a/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/AuthServiceImplTest.java @@ -55,6 +55,16 @@ void setUp() { userDetail = UserDetailResponse.of(existingUser); } + public static ResponseCookie createResponseCookie(String value) { + return ResponseCookie.from("refresh-token", value) + .maxAge(3600) + .secure(true) + .httpOnly(true) + .sameSite("None") + .path("/") + .build(); + } + @Nested @DisplayName("Validate login information test") class TestLogin { @@ -92,17 +102,6 @@ void validateLoginDifferentPassword() { @Nested @DisplayName("Issue response tokens test") class TestIssueTokens { - - public static ResponseCookie createResponseCookie(String value) { - return ResponseCookie.from("refresh-token", value) - .maxAge(3600) - .secure(true) - .httpOnly(true) - .sameSite("None") - .path("/") - .build(); - } - public static AccessToken createAccessToken(String value) { return new AccessToken(value); } diff --git a/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java index 5aa3abb0..64de817d 100644 --- a/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/domain/auth/service/RefreshTokenServiceImplTest.java @@ -1,9 +1,12 @@ package com.tasksprints.auction.domain.auth.service; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import com.tasksprints.auction.domain.auth.exception.RefreshTokenException; import com.tasksprints.auction.domain.auth.model.RefreshToken; import com.tasksprints.auction.domain.auth.repository.RefreshTokenRepository; +import java.util.Optional; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -24,7 +27,7 @@ class RefreshTokenServiceImplTest { @Nested @DisplayName("Save RefreshToken test") - class saveRefreshTokenTest { + class TestSaveRefreshToken { @Test @DisplayName("should return RefreshToken, when the test is success") void testSaveRefreshToken_success() { @@ -38,8 +41,38 @@ void testSaveRefreshToken_success() { RefreshToken actualRefreshToken = refreshTokenService.saveRefreshToken(refreshTokenValue, userId); // then - Assertions.assertEquals(expectedRefreshToken.getMemberId(), actualRefreshToken.getMemberId()); - Assertions.assertEquals(expectedRefreshToken.getId(), actualRefreshToken.getId()); + assertEquals(expectedRefreshToken.getUserId(), actualRefreshToken.getUserId()); + assertEquals(expectedRefreshToken.getId(), actualRefreshToken.getId()); + } + } + + @Nested + @DisplayName("Find refresh Token test") + class TestFindRefreshToken { + + void testFindRefreshToken_success() { + // given + String refreshTokenValue = "refreshToken"; + RefreshToken existedRefreshToken = RefreshToken.of("refreshToken", 1L); + when(refreshTokenRepository.findById(any())).thenReturn(Optional.ofNullable(existedRefreshToken)); + + // when + RefreshToken foundRefreshToken = refreshTokenService.findRefreshTokenById(refreshTokenValue); + + // then + assertEquals(1L, foundRefreshToken.getUserId()); + assertEquals(refreshTokenValue, foundRefreshToken.getId()); + } + + void testFindRefreshToken_fail() { + // given + String refreshTokenValue = "refreshToken"; + when(refreshTokenRepository.findById(any())).thenReturn(Optional.empty()); + + // when, then + Assertions.assertThrows(RefreshTokenException.class, () -> { + refreshTokenService.findRefreshTokenById(refreshTokenValue); + }); } } } From 3eafa4619e4921cde2bfcbc8569bb0078e98efc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Sun, 1 Dec 2024 20:14:06 +0900 Subject: [PATCH 25/27] feat : Add Permission check AOP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 유저의 권한을 체크하는 AOP를 추가한다 - 엔드포인트에서, 해당 AOP를 통해 유저가 올바른 접근을 하고 있는지 확인한다 - 올바른 접근일 경우, controller를 실행하고 아닐 경우 예외를 던진다 --- .../auction/common/jwt/UserCheck.java | 25 +++++++++++++++++++ .../auction/common/jwt/UserOnly.java | 11 ++++++++ .../auction/domain/auth/model/Accessor.java | 14 +++++------ 3 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/tasksprints/auction/common/jwt/UserCheck.java create mode 100644 src/main/java/com/tasksprints/auction/common/jwt/UserOnly.java diff --git a/src/main/java/com/tasksprints/auction/common/jwt/UserCheck.java b/src/main/java/com/tasksprints/auction/common/jwt/UserCheck.java new file mode 100644 index 00000000..1e8e8c01 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/jwt/UserCheck.java @@ -0,0 +1,25 @@ +package com.tasksprints.auction.common.jwt; + +import com.tasksprints.auction.domain.auth.exception.AuthException; +import com.tasksprints.auction.domain.auth.model.Accessor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class UserCheck { + @Before("@annotation(UserOnly)") + public void userCheck(JoinPoint joinPoint) { + Object[] args = joinPoint.getArgs(); + + for (Object arg : args) { + if (arg instanceof Accessor accessor) { + if (!accessor.isUser()) { + throw new AuthException("Invalid Access"); + } + } + } + } +} diff --git a/src/main/java/com/tasksprints/auction/common/jwt/UserOnly.java b/src/main/java/com/tasksprints/auction/common/jwt/UserOnly.java new file mode 100644 index 00000000..bc794653 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/jwt/UserOnly.java @@ -0,0 +1,11 @@ +package com.tasksprints.auction.common.jwt; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserOnly { +} diff --git a/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java b/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java index 03060d48..14e00fda 100644 --- a/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java +++ b/src/main/java/com/tasksprints/auction/domain/auth/model/Accessor.java @@ -1,28 +1,28 @@ package com.tasksprints.auction.domain.auth.model; import lombok.Builder; -import lombok.Getter; -import lombok.RequiredArgsConstructor; @Builder -@Getter -@RequiredArgsConstructor -public class Accessor { +public record Accessor(Long userId, Role role) { private static final Long GUEST_USER_ID = 0L; - private final Long userId; - private final Role role; public static Accessor guest() { return Accessor.of(GUEST_USER_ID, Role.GUEST); } + public static Accessor user(Long userId) { return Accessor.of(userId, Role.USER); } + public static Accessor admin(Long userId) { return Accessor.of(userId, Role.ADMIN); } + public boolean isUser() { + return Role.USER.equals(role); + } + private static Accessor of(Long userId, Role role) { return Accessor.builder() .userId(userId) From 7fdb53bbef1a7d0c28c02aa066cb3fb399e72fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=B1=84=EC=97=B0?= Date: Sun, 1 Dec 2024 20:27:20 +0900 Subject: [PATCH 26/27] refactor : Delete test directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일관성을 위해 테스트 디렉토리에 존재하는 auth 디렉토리를 삭제합니다 --- .../tasksprints/auction/api/{auth => }/AuthControllerTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename src/test/java/com/tasksprints/auction/api/{auth => }/AuthControllerTest.java (98%) diff --git a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java b/src/test/java/com/tasksprints/auction/api/AuthControllerTest.java similarity index 98% rename from src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java rename to src/test/java/com/tasksprints/auction/api/AuthControllerTest.java index c414961a..1348acea 100644 --- a/src/test/java/com/tasksprints/auction/api/auth/AuthControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/AuthControllerTest.java @@ -1,4 +1,4 @@ -package com.tasksprints.auction.api.auth; +package com.tasksprints.auction.api; import static org.hamcrest.Matchers.containsString; import static org.mockito.Mockito.*; @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.tasksprints.auction.api.BaseControllerTest; +import com.tasksprints.auction.api.auth.AuthController; import com.tasksprints.auction.common.constant.ApiResponseMessages; import com.tasksprints.auction.domain.auth.dto.request.LoginRequest; import com.tasksprints.auction.domain.auth.dto.request.LoginRequest.Login; From 70d44f3a8c88d58ad85462fe4a404c517a3af1d9 Mon Sep 17 00:00:00 2001 From: taehyun <126179088+KNU-K@users.noreply.github.com> Date: Sat, 21 Dec 2024 16:25:35 +0900 Subject: [PATCH 27/27] =?UTF-8?q?fix(user)=20:=20=EC=98=A4=ED=83=80=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 --- .../auction/domain/user/service/UserServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java index 3249ed70..6d2e5302 100644 --- a/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java @@ -69,11 +69,11 @@ public UserDetailResponse getUserDetailByEmail(String email) { User user = userRepository.findByEmail(email) .orElseThrow(() -> new UserNotFoundException("User not found with email " + email)); return UserDetailResponse.of(user); + } @Override public User getUserById(Long id) { return userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException("User not found with id " + id)); - } }