diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..5c4e2b02 Binary files /dev/null and b/.DS_Store differ diff --git a/src/main/java/space/space_spring/SpaceSpringApplication.java b/src/main/java/space/space_spring/SpaceSpringApplication.java index 6b2a19cf..7333c784 100644 --- a/src/main/java/space/space_spring/SpaceSpringApplication.java +++ b/src/main/java/space/space_spring/SpaceSpringApplication.java @@ -2,7 +2,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication public class SpaceSpringApplication { diff --git a/src/main/java/space/space_spring/domain/.DS_Store b/src/main/java/space/space_spring/domain/.DS_Store index cf1810f3..1741fef2 100644 Binary files a/src/main/java/space/space_spring/domain/.DS_Store and b/src/main/java/space/space_spring/domain/.DS_Store differ diff --git a/src/main/java/space/space_spring/domain/authorization/.DS_Store b/src/main/java/space/space_spring/domain/authorization/.DS_Store new file mode 100644 index 00000000..a9267028 Binary files /dev/null and b/src/main/java/space/space_spring/domain/authorization/.DS_Store differ diff --git a/src/main/java/space/space_spring/domain/authorization/auth/service/AuthService.java b/src/main/java/space/space_spring/domain/authorization/auth/service/AuthService.java index 25484a2d..87fb127f 100644 --- a/src/main/java/space/space_spring/domain/authorization/auth/service/AuthService.java +++ b/src/main/java/space/space_spring/domain/authorization/auth/service/AuthService.java @@ -9,10 +9,10 @@ import space.space_spring.domain.authorization.jwt.model.TokenType; import space.space_spring.domain.authorization.jwt.repository.JwtRepository; import space.space_spring.domain.user.model.PostLoginDto; -import space.space_spring.entity.TokenStorage; +import space.space_spring.entity.RefreshTokenStorage; import space.space_spring.entity.User; import space.space_spring.exception.CustomException; -import space.space_spring.jwt.JwtLoginProvider; +import space.space_spring.domain.authorization.jwt.model.JwtLoginProvider; import space.space_spring.util.user.UserUtils; import static space.space_spring.entity.enumStatus.UserSignupType.LOCAL; @@ -40,11 +40,11 @@ public PostLoginDto login(PostLoginDto.Request request) { validatePassword(userByEmail, request.getPassword()); // TODO 3. JWT 발급 -> access token, refresh token 2개 발급 - String accessToken = jwtLoginProvider.generateToken(userByEmail, TokenType.ACCESS); - String refreshToken = jwtLoginProvider.generateToken(userByEmail, TokenType.REFRESH); + String accessToken = jwtLoginProvider.generateToken(userByEmail.getUserId(), TokenType.ACCESS); + String refreshToken = jwtLoginProvider.generateToken(userByEmail.getUserId(), TokenType.REFRESH); // TODO 4. refresh token db에 저장 - TokenStorage tokenStorage = TokenStorage.builder() + RefreshTokenStorage tokenStorage = RefreshTokenStorage.builder() .user(userByEmail) .tokenValue(refreshToken) .build(); diff --git a/src/main/java/space/space_spring/domain/authorization/jwt/controller/JwtController.java b/src/main/java/space/space_spring/domain/authorization/jwt/controller/JwtController.java index b2cd2d93..8a597dbc 100644 --- a/src/main/java/space/space_spring/domain/authorization/jwt/controller/JwtController.java +++ b/src/main/java/space/space_spring/domain/authorization/jwt/controller/JwtController.java @@ -26,19 +26,9 @@ public class JwtController { */ @PostMapping("/new-token") public BaseResponse updateAccessToken(HttpServletRequest request, HttpServletResponse response) throws IOException { - // access token, refresh token 파싱 - TokenPairDTO tokenPairDTO = jwtService.resolveTokenPair(request); - // access token 로부터 user find - User userByAccessToken = jwtService.getUserByAccessToken(tokenPairDTO.getAccessToken()); + TokenPairDTO newTokenPairDTO = jwtService.updateTokenPair(request); - // refresh token 유효성 검사 - jwtService.validateRefreshToken(userByAccessToken, tokenPairDTO.getRefreshToken()); - - // access token, refresh token 새로 발급 - TokenPairDTO newTokenPairDTO = jwtService.updateTokenPair(userByAccessToken); - - // response header에 새로 발급한 token pair set response.setHeader("Authorization-refresh", "Bearer " + newTokenPairDTO.getRefreshToken()); response.setHeader("Authorization", "Bearer " + newTokenPairDTO.getAccessToken()); diff --git a/src/main/java/space/space_spring/jwt/JwtLoginProvider.java b/src/main/java/space/space_spring/domain/authorization/jwt/model/JwtLoginProvider.java similarity index 92% rename from src/main/java/space/space_spring/jwt/JwtLoginProvider.java rename to src/main/java/space/space_spring/domain/authorization/jwt/model/JwtLoginProvider.java index 998b42c5..81b6c7ce 100644 --- a/src/main/java/space/space_spring/jwt/JwtLoginProvider.java +++ b/src/main/java/space/space_spring/domain/authorization/jwt/model/JwtLoginProvider.java @@ -1,11 +1,13 @@ -package space.space_spring.jwt; +package space.space_spring.domain.authorization.jwt.model; import io.jsonwebtoken.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import space.space_spring.domain.authorization.jwt.model.TokenType; -import space.space_spring.entity.User; import space.space_spring.exception.CustomException; import space.space_spring.exception.jwt.bad_request.JwtUnsupportedTokenException; import space.space_spring.exception.jwt.unauthorized.JwtInvalidTokenException; @@ -17,6 +19,8 @@ @Slf4j @Component +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter public class JwtLoginProvider { @Value("${secret.jwt.access-secret-key}") private String ACCESS_SECRET_KEY; @@ -30,14 +34,12 @@ public class JwtLoginProvider { @Value("${secret.jwt.refresh-expired-in}") private Long REFRESH_EXPIRED_IN; - public String generateToken(User user, TokenType tokenType) { + public String generateToken(Long userId, TokenType tokenType) { // Claims claims = Jwts.claims().setSubject(jwtPayloadDto.getUserId().toString()); Date now = new Date(); Date expiration = setExpiration(now, tokenType); - Long userId = user.getUserId(); - return makeToken(tokenType, userId, now, expiration); } @@ -116,4 +118,5 @@ public Long getUserIdFromAccessToken(String token) { throw e; } } + } diff --git a/src/main/java/space/space_spring/domain/authorization/jwt/model/JwtLoginTokenResolver.java b/src/main/java/space/space_spring/domain/authorization/jwt/model/JwtLoginTokenResolver.java new file mode 100644 index 00000000..56381bc5 --- /dev/null +++ b/src/main/java/space/space_spring/domain/authorization/jwt/model/JwtLoginTokenResolver.java @@ -0,0 +1,45 @@ +package space.space_spring.domain.authorization.jwt.model; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import space.space_spring.exception.jwt.bad_request.JwtNoTokenException; +import space.space_spring.exception.jwt.bad_request.JwtUnsupportedTokenException; + +import static space.space_spring.response.status.BaseExceptionResponseStatus.TOKEN_NOT_FOUND; +import static space.space_spring.response.status.BaseExceptionResponseStatus.UNSUPPORTED_TOKEN_TYPE; + +@Component +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class JwtLoginTokenResolver { + + private static final String JWT_TOKEN_PREFIX = "Bearer "; + + public TokenPairDTO resolveTokenPair(HttpServletRequest request) { + // TODO 1. access token 파싱 + String accessToken = request.getHeader(HttpHeaders.AUTHORIZATION); + validateToken(accessToken); + + // TODO 2. refresh token 파싱 + String refreshToken = request.getHeader("Authorization-refresh"); + validateToken(refreshToken); + + // TODO 3. return + return TokenPairDTO.builder() + .accessToken(accessToken.substring(JWT_TOKEN_PREFIX.length())) + .refreshToken(refreshToken.substring(JWT_TOKEN_PREFIX.length())) + .build(); + } + + private void validateToken(String token) { + if (token == null) { + throw new JwtNoTokenException(TOKEN_NOT_FOUND); + } + if (!token.startsWith(JWT_TOKEN_PREFIX)) { + throw new JwtUnsupportedTokenException(UNSUPPORTED_TOKEN_TYPE); + } + } + +} diff --git a/src/main/java/space/space_spring/domain/authorization/jwt/repository/JwtRepository.java b/src/main/java/space/space_spring/domain/authorization/jwt/repository/JwtRepository.java index dad962d3..85654bcf 100644 --- a/src/main/java/space/space_spring/domain/authorization/jwt/repository/JwtRepository.java +++ b/src/main/java/space/space_spring/domain/authorization/jwt/repository/JwtRepository.java @@ -1,15 +1,24 @@ package space.space_spring.domain.authorization.jwt.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import space.space_spring.entity.TokenStorage; +import org.springframework.transaction.annotation.Transactional; +import space.space_spring.entity.RefreshTokenStorage; import space.space_spring.entity.User; import java.util.Optional; @Repository -public interface JwtRepository extends JpaRepository { +public interface JwtRepository extends JpaRepository { + + Optional findByUser(User user); + + @Modifying + @Transactional + @Query("DELETE FROM RefreshTokenStorage t WHERE t.user = :user") + void deleteByUser(@Param("user") User user); - Optional findByUser(User user); - void deleteByUser(User user); } diff --git a/src/main/java/space/space_spring/domain/authorization/jwt/service/JwtService.java b/src/main/java/space/space_spring/domain/authorization/jwt/service/JwtService.java index 1b3c65af..9a2ba721 100644 --- a/src/main/java/space/space_spring/domain/authorization/jwt/service/JwtService.java +++ b/src/main/java/space/space_spring/domain/authorization/jwt/service/JwtService.java @@ -2,70 +2,54 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import space.space_spring.domain.authorization.jwt.model.*; import space.space_spring.domain.authorization.jwt.repository.JwtRepository; import space.space_spring.domain.user.repository.UserRepository; -import space.space_spring.domain.authorization.jwt.model.TokenPairDTO; -import space.space_spring.domain.authorization.jwt.model.TokenType; -import space.space_spring.entity.TokenStorage; +import space.space_spring.entity.RefreshTokenStorage; import space.space_spring.entity.User; import space.space_spring.exception.CustomException; -import space.space_spring.exception.jwt.bad_request.JwtNoTokenException; -import space.space_spring.exception.jwt.bad_request.JwtUnsupportedTokenException; import space.space_spring.exception.jwt.unauthorized.JwtExpiredTokenException; import space.space_spring.exception.jwt.unauthorized.JwtUnauthorizedTokenException; -import space.space_spring.jwt.JwtLoginProvider; import static space.space_spring.response.status.BaseExceptionResponseStatus.*; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class JwtService { private final JwtRepository jwtRepository; - private final JwtLoginProvider jwtLoginProvider; private final UserRepository userRepository; + private final JwtLoginProvider jwtLoginProvider; + private final JwtLoginTokenResolver tokenResolver; - private static final String JWT_TOKEN_PREFIX = "Bearer "; - + @Transactional + public TokenPairDTO updateTokenPair(HttpServletRequest request) { + // request에서 기존의 TokenPair를 찾아와서 + TokenPairDTO oldTokenPair = tokenResolver.resolveTokenPair(request); - public TokenPairDTO resolveTokenPair(HttpServletRequest request) { - // TODO 1. access token 파싱 - String accessToken = request.getHeader(HttpHeaders.AUTHORIZATION); - validateToken(accessToken); + // 여기서 User 찾고 + User user = getUserByAccessToken(oldTokenPair.getAccessToken()); - // TODO 2. refresh token 파싱 - String refreshToken = request.getHeader("Authorization-refresh"); - validateToken(refreshToken); + // 이 User로 refresh token의 유효성 검사 진행하고 + validateRefreshToken(user, oldTokenPair.getRefreshToken()); - // TODO 3. return - return TokenPairDTO.builder() - .accessToken(accessToken.substring(JWT_TOKEN_PREFIX.length())) - .refreshToken(refreshToken.substring(JWT_TOKEN_PREFIX.length())) - .build(); - } - - private void validateToken(String token) { - if (token == null) { - throw new JwtNoTokenException(TOKEN_NOT_FOUND); - } - if (!token.startsWith(JWT_TOKEN_PREFIX)) { - throw new JwtUnsupportedTokenException(UNSUPPORTED_TOKEN_TYPE); - } + // access, refresh 새로 발급 + return updateTokenPair(user); } - public User getUserByAccessToken(String accessToken) { + private User getUserByAccessToken(String accessToken) { Long userIdFromToken = jwtLoginProvider.getUserIdFromAccessToken(accessToken); return userRepository.findByUserId(userIdFromToken) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); } - @Transactional - public void validateRefreshToken(User user, String refreshToken) { - TokenStorage tokenStorage = jwtRepository.findByUser(user) + private void validateRefreshToken(User user, String refreshToken) { + RefreshTokenStorage tokenStorage = jwtRepository.findByUser(user) .orElseThrow(() -> { // db에서 row delete 하는 코드 추가 @@ -77,41 +61,47 @@ public void validateRefreshToken(User user, String refreshToken) { if (jwtLoginProvider.isExpiredToken(refreshToken, TokenType.REFRESH)) { // refresh token이 만료된 경우 -> 예외 발생 -> 유저의 재 로그인 유도 // db에서 row delete 하는 코드 추가 - jwtRepository.deleteByUser(user); + deleteRefreshTokenStorage(user); + throw new JwtExpiredTokenException(EXPIRED_REFRESH_TOKEN); } - // TODO 2. refresh token이 db에 실제로 존재하는지 체크 + // TODO 2. refresh token이 db에 존재하는 token 값과 일치하는지 확인 if (!tokenStorage.checkTokenValue(refreshToken)) { - // refresh token이 db에 존재하지 않느 경우 -> 유효하지 않은 refresh token이므로 예외 발생 + // refresh token이 db에 존재하는 token 값과 일치하지 않는 경우 -> 유효하지 않은 refresh token이므로 예외 발생 // db에서 row delete 하는 코드 추가 - jwtRepository.deleteByUser(user); + deleteRefreshTokenStorage(user); + throw new JwtUnauthorizedTokenException(TOKEN_MISMATCH); } } - @Transactional - public TokenPairDTO updateTokenPair(User user) { - // TODO 1. new access token, refresh token 발급 - String newAccessToken = jwtLoginProvider.generateToken(user, TokenType.ACCESS); - String newRefreshToken = jwtLoginProvider.generateToken(user, TokenType.REFRESH); + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void deleteRefreshTokenStorage(User user) { + jwtRepository.deleteByUser(user); + } - // TODO 2. db의 refresh token update - TokenStorage tokenStorage = jwtRepository.findByUser(user) + private TokenPairDTO updateTokenPair(User user) { + RefreshTokenStorage tokenStorage = jwtRepository.findByUser(user) .orElseThrow(() -> new JwtUnauthorizedTokenException(TOKEN_MISMATCH)); + // new access token, new refresh token 발급 받아서 + String newAccessToken = jwtLoginProvider.generateToken(user.getUserId(), TokenType.ACCESS); + String newRefreshToken = jwtLoginProvider.generateToken(user.getUserId(), TokenType.REFRESH); + + // tokenStorage update 하고 tokenStorage.updateTokenValue(newRefreshToken); - // TODO 3. return return TokenPairDTO.builder() .accessToken(newAccessToken) .refreshToken(newRefreshToken) .build(); + } public TokenPairDTO provideJwtToOAuthUser(User userByOAuthInfo) { - String accessToken = jwtLoginProvider.generateToken(userByOAuthInfo, TokenType.ACCESS); - String refreshToken = jwtLoginProvider.generateToken(userByOAuthInfo, TokenType.REFRESH); + String accessToken = jwtLoginProvider.generateToken(userByOAuthInfo.getUserId(), TokenType.ACCESS); + String refreshToken = jwtLoginProvider.generateToken(userByOAuthInfo.getUserId(), TokenType.REFRESH); return TokenPairDTO.builder() .accessToken(accessToken) @@ -121,10 +111,11 @@ public TokenPairDTO provideJwtToOAuthUser(User userByOAuthInfo) { @Transactional public void updateRefreshToken(User user, String refreshToken) { - TokenStorage tokenStorage = jwtRepository.findByUser(user) + RefreshTokenStorage tokenStorage = jwtRepository.findByUser(user) .orElseThrow(() -> new JwtUnauthorizedTokenException(TOKEN_MISMATCH)); tokenStorage.updateTokenValue(refreshToken); } + } diff --git a/src/main/java/space/space_spring/entity/TokenStorage.java b/src/main/java/space/space_spring/entity/RefreshTokenStorage.java similarity index 59% rename from src/main/java/space/space_spring/entity/TokenStorage.java rename to src/main/java/space/space_spring/entity/RefreshTokenStorage.java index 5519d8e2..d79f3bef 100644 --- a/src/main/java/space/space_spring/entity/TokenStorage.java +++ b/src/main/java/space/space_spring/entity/RefreshTokenStorage.java @@ -1,7 +1,6 @@ package space.space_spring.entity; import jakarta.persistence.*; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -9,16 +8,14 @@ @Entity @Table(name = "Token_Storage") @Getter -@Builder @NoArgsConstructor -@AllArgsConstructor -public class TokenStorage { +public class RefreshTokenStorage { @Id @GeneratedValue @Column(name = "token_storage_id") private Long tokenStorageId; - @OneToOne + @OneToOne(orphanRemoval = true) // @Column(name = "user_id") private User user; @@ -33,4 +30,17 @@ public boolean checkTokenValue(String tokenValue) { return this.tokenValue.equals(tokenValue); } + @Builder + private RefreshTokenStorage(User user, String tokenValue) { + this.user = user; + this.tokenValue = tokenValue; + } + + public static RefreshTokenStorage create(User user, String tokenValue) { + return RefreshTokenStorage.builder() + .user(user) + .tokenValue(tokenValue) + .build(); + } + } diff --git a/src/main/java/space/space_spring/entity/User.java b/src/main/java/space/space_spring/entity/User.java index 9bddd8b2..ee6a88b1 100644 --- a/src/main/java/space/space_spring/entity/User.java +++ b/src/main/java/space/space_spring/entity/User.java @@ -2,12 +2,15 @@ import jakarta.annotation.Nullable; import jakarta.persistence.*; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import space.space_spring.entity.enumStatus.UserSignupType; @Entity @Table(name = "Users") @Getter +@NoArgsConstructor public class User extends BaseEntity { @Id @GeneratedValue @@ -34,6 +37,21 @@ public void saveUser(String email, String password, String userName, UserSignupT initializeBaseEntityFields(); } + @Builder + private User(String email, String password, String userName, UserSignupType signupType) { + this.email = email; + this.password = password; + this.userName = userName; + this.signupType = signupType.getSignupType(); + } + public static User create(String email, String password, String userName, UserSignupType signupType) { + return User.builder() + .email(email) + .password(password) + .userName(userName) + .signupType(signupType) + .build(); + } } diff --git a/src/main/java/space/space_spring/interceptor/jwtLogin/JwtLoginAuthInterceptor.java b/src/main/java/space/space_spring/interceptor/jwtLogin/JwtLoginAuthInterceptor.java index 03bcfd59..db9fc283 100644 --- a/src/main/java/space/space_spring/interceptor/jwtLogin/JwtLoginAuthInterceptor.java +++ b/src/main/java/space/space_spring/interceptor/jwtLogin/JwtLoginAuthInterceptor.java @@ -10,7 +10,7 @@ import space.space_spring.exception.jwt.bad_request.JwtNoTokenException; import space.space_spring.exception.jwt.bad_request.JwtUnsupportedTokenException; import space.space_spring.exception.jwt.unauthorized.JwtExpiredTokenException; -import space.space_spring.jwt.JwtLoginProvider; +import space.space_spring.domain.authorization.jwt.model.JwtLoginProvider; import static space.space_spring.response.status.BaseExceptionResponseStatus.*; diff --git a/src/main/java/space/space_spring/interceptor/jwtSocket/JwtChannelInterceptor.java b/src/main/java/space/space_spring/interceptor/jwtSocket/JwtChannelInterceptor.java index 3c137e09..5ae95aea 100644 --- a/src/main/java/space/space_spring/interceptor/jwtSocket/JwtChannelInterceptor.java +++ b/src/main/java/space/space_spring/interceptor/jwtSocket/JwtChannelInterceptor.java @@ -12,7 +12,7 @@ import space.space_spring.exception.jwt.bad_request.JwtNoTokenException; import space.space_spring.exception.jwt.bad_request.JwtUnsupportedTokenException; import space.space_spring.exception.jwt.unauthorized.JwtExpiredTokenException; -import space.space_spring.jwt.JwtLoginProvider; +import space.space_spring.domain.authorization.jwt.model.JwtLoginProvider; import static space.space_spring.response.status.BaseExceptionResponseStatus.*; diff --git a/src/main/java/space/space_spring/service/VoiceRoomService.java b/src/main/java/space/space_spring/service/VoiceRoomService.java index 09ab1d90..738b24cc 100644 --- a/src/main/java/space/space_spring/service/VoiceRoomService.java +++ b/src/main/java/space/space_spring/service/VoiceRoomService.java @@ -19,6 +19,10 @@ import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Optional; + + import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; diff --git a/src/test/java/space/space_spring/domain/authorization/jwt/model/JwtLoginProviderTest.java b/src/test/java/space/space_spring/domain/authorization/jwt/model/JwtLoginProviderTest.java new file mode 100644 index 00000000..053dc66b --- /dev/null +++ b/src/test/java/space/space_spring/domain/authorization/jwt/model/JwtLoginProviderTest.java @@ -0,0 +1,154 @@ +package space.space_spring.domain.authorization.jwt.model; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringJUnitConfig(JwtLoginProvider.class) +@TestPropertySource(properties = { // 테스트용 프로퍼티 소스(환경 설정) 지정 + "secret.jwt.access-secret-key=accessSecretKeyaccessSecretKeyaccessSecretKey", + "secret.jwt.refresh-secret-key=refreshSecretKeyrefreshSecretKeyrefreshSecretKey", + "secret.jwt.access-expired-in=3600000", + "secret.jwt.refresh-expired-in=604800000" +}) +class JwtLoginProviderTest { + + @Autowired + private JwtLoginProvider jwtLoginProvider; + + @Test + @DisplayName("userId를 인자로 넘기면, userId값을 payload에 가지고 있는 access token을 만들어 줍니다.") + void generateToken() throws Exception { + //given + Long userId = 1L; + + //when + String accessToken = jwtLoginProvider.generateToken(userId, TokenType.ACCESS); + + //then + Jws claims = Jwts.parserBuilder() + .setSigningKey(jwtLoginProvider.getACCESS_SECRET_KEY()) + .build() + .parseClaimsJws(accessToken); + + Long extractedUserId = claims.getBody().get("userId", Long.class); + + assertThat(extractedUserId).isEqualTo(userId); + } + + @Test + @DisplayName("인자로 받은 access token의 유효기간이 끝나지 않았다면, false를 return 한다.") + void isExpiredToken1() throws Exception { + //given + Date now = new Date(); + String accessToken = Jwts.builder() + .setExpiration(new Date(now.getTime() + 360000L)) // 현 시점부터 1시간 동안이 유효기간 + .signWith(SignatureAlgorithm.HS256, "accessSecretKeyaccessSecretKeyaccessSecretKey") + .compact(); + + //when + boolean expiredToken = jwtLoginProvider.isExpiredToken(accessToken, TokenType.ACCESS); + + //then + assertThat(expiredToken).isFalse(); + } + + @Test + @DisplayName("인자로 받은 access token의 유효기간이 끝났다면, true를 return 한다.") + void isExpiredToken2() throws Exception { + //given + Date now = new Date(); + String accessToken = Jwts.builder() + .setExpiration(new Date(now.getTime())) // 생성하자마자 유효기간 끝 + .signWith(SignatureAlgorithm.HS256, "accessSecretKeyaccessSecretKeyaccessSecretKey") + .compact(); + + //when + boolean expiredToken = jwtLoginProvider.isExpiredToken(accessToken, TokenType.ACCESS); + + //then + assertThat(expiredToken).isTrue(); + } + + @Test + @DisplayName("인자로 받은 refresh token의 유효기간이 끝나지 않았다면, false를 return 한다.") + void isExpiredToken3() throws Exception { + //given + Date now = new Date(); + String refreshToken = Jwts.builder() + .setExpiration(new Date(now.getTime() + 360000L)) // 현 시점부터 1시간 동안이 유효기간 + .signWith(SignatureAlgorithm.HS256, "refreshSecretKeyrefreshSecretKeyrefreshSecretKey") + .compact(); + + //when + boolean expiredToken = jwtLoginProvider.isExpiredToken(refreshToken, TokenType.REFRESH); + + //then + assertThat(expiredToken).isFalse(); + } + + @Test + @DisplayName("인자로 받은 refresh token의 유효기간이 끝났다면, true를 return 한다.") + void isExpiredToken4() throws Exception { + //given + Date now = new Date(); + String refreshToken = Jwts.builder() + .setExpiration(new Date(now.getTime())) // 생성하자마자 유효기간 끝 + .signWith(SignatureAlgorithm.HS256, "refreshSecretKeyrefreshSecretKeyrefreshSecretKey") + .compact(); + + //when + boolean expiredToken = jwtLoginProvider.isExpiredToken(refreshToken, TokenType.REFRESH); + + //then + assertThat(expiredToken).isTrue(); + } + + @Test + @DisplayName("만료되지 않은 access token의 payload에 담긴 userId 값을 return 한다.") + void getUserIdFromAccessToken1() throws Exception { + //given + Long userId = 1L; + Date now = new Date(); + String accessToken = Jwts.builder() + .setExpiration(new Date(now.getTime() + 360000L)) // 현 시점부터 1시간 동안이 유효기간 + .claim("userId", userId) + .signWith(SignatureAlgorithm.HS256, "accessSecretKeyaccessSecretKeyaccessSecretKey") + .compact(); + + //when + Long userIdFromAccessToken = jwtLoginProvider.getUserIdFromAccessToken(accessToken); + + //then + assertThat(userIdFromAccessToken).isEqualTo(userId); + } + + @Test + @DisplayName("만료된 access token의 payload에 담긴 userId 값을 return 한다.") + void getUserIdFromAccessToken2() throws Exception { + //given + Long userId = 1L; + Date now = new Date(); + String accessToken = Jwts.builder() + .setExpiration(new Date(now.getTime())) // 생성하자마자 유효기간 끝 + .claim("userId", userId) + .signWith(SignatureAlgorithm.HS256, "accessSecretKeyaccessSecretKeyaccessSecretKey") + .compact(); + + //when + Long userIdFromAccessToken = jwtLoginProvider.getUserIdFromAccessToken(accessToken); + + //then + assertThat(userIdFromAccessToken).isEqualTo(userId); + } +} \ No newline at end of file diff --git a/src/test/java/space/space_spring/domain/authorization/jwt/model/JwtLoginTokenResolverTest.java b/src/test/java/space/space_spring/domain/authorization/jwt/model/JwtLoginTokenResolverTest.java new file mode 100644 index 00000000..ad6e8b14 --- /dev/null +++ b/src/test/java/space/space_spring/domain/authorization/jwt/model/JwtLoginTokenResolverTest.java @@ -0,0 +1,79 @@ +package space.space_spring.domain.authorization.jwt.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import space.space_spring.exception.jwt.bad_request.JwtNoTokenException; +import space.space_spring.exception.jwt.bad_request.JwtUnsupportedTokenException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class JwtLoginTokenResolverTest { + + private static final String JWT_TOKEN_PREFIX = "Bearer "; + + @Test + @DisplayName("request header로부터 access token, refresh token을 rosolve한다.") + void resolveTokenPair() throws Exception { + //given + JwtLoginTokenResolver tokenResolver = new JwtLoginTokenResolver(); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", JWT_TOKEN_PREFIX + "accessToken"); + request.addHeader("Authorization-refresh", JWT_TOKEN_PREFIX + "refreshToken"); + + //when + TokenPairDTO tokenPairDTO = tokenResolver.resolveTokenPair(request); + + //then + assertThat(tokenPairDTO.getAccessToken()).isEqualTo("accessToken"); + assertThat(tokenPairDTO.getRefreshToken()).isEqualTo("refreshToken"); + } + + @Test + @DisplayName("access token값이 null 일 경우 예외를 던진다.") + void validateToken1() throws Exception { + //given + JwtLoginTokenResolver tokenResolver = new JwtLoginTokenResolver(); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization-refresh", JWT_TOKEN_PREFIX + "refreshToken"); + + //when //then + assertThatThrownBy(() -> tokenResolver.resolveTokenPair(request)) + .isInstanceOf(JwtNoTokenException.class) + .hasMessage("토큰이 HTTP Header에 없습니다."); + } + + @Test + @DisplayName("refresh token값이 null 일 경우 예외를 던진다.") + void validateToken2() throws Exception { + //given + JwtLoginTokenResolver tokenResolver = new JwtLoginTokenResolver(); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", JWT_TOKEN_PREFIX + "accessToken"); + + //when //then + assertThatThrownBy(() -> tokenResolver.resolveTokenPair(request)) + .isInstanceOf(JwtNoTokenException.class) + .hasMessage("토큰이 HTTP Header에 없습니다."); + } + + @Test + @DisplayName("토큰값의 prefix가 잘못된 경우 예외를 던진다.") + void validateToken3() throws Exception { + //given + JwtLoginTokenResolver tokenResolver = new JwtLoginTokenResolver(); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "wrong prefix " + "accessToken"); + request.addHeader("Authorization-refresh", "wrong prefix " + "refreshToken"); + + //when //then + assertThatThrownBy(() -> tokenResolver.resolveTokenPair(request)) + .isInstanceOf(JwtUnsupportedTokenException.class) + .hasMessage("지원되지 않는 토큰 형식입니다."); + } +} \ No newline at end of file diff --git a/src/test/java/space/space_spring/domain/authorization/jwt/service/JwtServiceTest.java b/src/test/java/space/space_spring/domain/authorization/jwt/service/JwtServiceTest.java new file mode 100644 index 00000000..8c1fe0fb --- /dev/null +++ b/src/test/java/space/space_spring/domain/authorization/jwt/service/JwtServiceTest.java @@ -0,0 +1,194 @@ +package space.space_spring.domain.authorization.jwt.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import space.space_spring.domain.authorization.jwt.model.JwtLoginProvider; +import space.space_spring.domain.authorization.jwt.model.TokenPairDTO; +import space.space_spring.domain.authorization.jwt.model.JwtLoginTokenResolver; +import space.space_spring.domain.authorization.jwt.repository.JwtRepository; +import space.space_spring.domain.user.repository.UserRepository; +import space.space_spring.entity.RefreshTokenStorage; +import space.space_spring.entity.User; +import space.space_spring.entity.enumStatus.UserSignupType; +import space.space_spring.exception.jwt.unauthorized.JwtExpiredTokenException; +import space.space_spring.exception.jwt.unauthorized.JwtUnauthorizedTokenException; + +import java.util.Date; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DataJpaTest +@Import({JwtService.class, JwtLoginProvider.class, JwtLoginTokenResolver.class}) +@ActiveProfiles("test") +@TestPropertySource(properties = { + "secret.jwt.access-secret-key=accessSecretKeyaccessSecretKeyaccessSecretKey", + "secret.jwt.refresh-secret-key=refreshSecretKeyrefreshSecretKeyrefreshSecretKey", + "secret.jwt.access-expired-in=3600000", + "secret.jwt.refresh-expired-in=604800000" +}) +@EnableJpaRepositories(basePackageClasses = {UserRepository.class, JwtRepository.class}) +@EntityScan(basePackageClasses = {User.class, RefreshTokenStorage.class}) +class JwtServiceTest { + + @Autowired + private JwtService jwtService; + + @Autowired + private JwtRepository jwtRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private JwtLoginProvider jwtLoginProvider; + + @Autowired + private JwtLoginTokenResolver tokenResolver; + + private static final String JWT_TOKEN_PREFIX = "Bearer "; + + @Test + @DisplayName("request의 refresh token이 유효한 토큰인 경우, 새로운 token pair를 발급하고 db의 token을 update 한다.") + void updateTokenPair1() throws Exception { + //given + User user = User.create("email", "password", "name", UserSignupType.LOCAL); + User savedUser = userRepository.save(user); + Long userId = savedUser.getUserId(); + + Date now = new Date(); + String accessToken = Jwts.builder() + .setExpiration(new Date(now.getTime())) // 생성하자마자 유효기간 끝 + .claim("userId", userId) + .signWith(SignatureAlgorithm.HS256, "accessSecretKeyaccessSecretKeyaccessSecretKey") + .compact(); + + String refreshToken = Jwts.builder() + .setExpiration(new Date(now.getTime() + 360000L)) // 현 시점부터 1시간 동안이 유효기간 + .signWith(SignatureAlgorithm.HS256, "refreshSecretKeyrefreshSecretKeyrefreshSecretKey") + .compact(); + + RefreshTokenStorage tokenStorage = RefreshTokenStorage.create(savedUser, refreshToken); + jwtRepository.save(tokenStorage); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", JWT_TOKEN_PREFIX + accessToken); + request.addHeader("Authorization-refresh", JWT_TOKEN_PREFIX + refreshToken); + + //when + TokenPairDTO tokenPairDTO = jwtService.updateTokenPair(request); + + //then + String newAccessToken = tokenPairDTO.getAccessToken(); + String newRefreshToken = tokenPairDTO.getRefreshToken(); + + Claims accessTokenClaims = Jwts.parser() + .setSigningKey(jwtLoginProvider.getACCESS_SECRET_KEY()) + .parseClaimsJws(newAccessToken) + .getBody(); + + assertThat(accessTokenClaims.get("userId", Long.class)).isEqualTo(userId); + + RefreshTokenStorage updatedRefreshTokenStorage = jwtRepository.findByUser(savedUser) + .orElseThrow(() -> new Exception("RefreshTokenStorage not found")); + + assertThat(updatedRefreshTokenStorage.getTokenValue()).isEqualTo(newRefreshToken); + + System.out.println("old access token = " + accessToken); + System.out.println("new access token = " + newAccessToken); + System.out.println("old refresh token = " + refreshToken); + System.out.println("new refresh token = " + newRefreshToken); + + } + + @Test + @DisplayName("request의 refresh token의 유효기간이 지났을 경우, 에러를 발생시키고 db의 token을 지운다.") + void updateTokenPair2() throws Exception { + //given + User user = User.create("email", "password", "name", UserSignupType.LOCAL); + User savedUser = userRepository.save(user); + Long userId = savedUser.getUserId(); + + Date now = new Date(); + String accessToken = Jwts.builder() + .setExpiration(new Date(now.getTime())) // 생성하자마자 유효기간 끝 + .claim("userId", userId) + .signWith(SignatureAlgorithm.HS256, "accessSecretKeyaccessSecretKeyaccessSecretKey") + .compact(); + + String refreshToken = Jwts.builder() + .setExpiration(new Date(now.getTime())) // 생성하자마자 유효기간 끝 + .signWith(SignatureAlgorithm.HS256, "refreshSecretKeyrefreshSecretKeyrefreshSecretKey") + .compact(); + + RefreshTokenStorage tokenStorage = RefreshTokenStorage.create(savedUser, refreshToken); + jwtRepository.save(tokenStorage); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", JWT_TOKEN_PREFIX + accessToken); + request.addHeader("Authorization-refresh", JWT_TOKEN_PREFIX + refreshToken); + + //when //then + assertThatThrownBy(() -> jwtService.updateTokenPair(request)) + .isInstanceOf(JwtExpiredTokenException.class) + .hasMessage("만료된 refresh token 입니다. 다시 로그인해야합니다."); + + Optional byUser = jwtRepository.findByUser(user); + assertThat(byUser).isEmpty(); + } + + @Test + @DisplayName("request의 refresh token이 db에 저장된 token 값과 다를 경우, 에러를 발생시키고 db의 token을 지운다.") + void updateTokenPair3() throws Exception { + //given + User user = User.create("email", "password", "name", UserSignupType.LOCAL); + User savedUser = userRepository.save(user); + Long userId = savedUser.getUserId(); + + Date now = new Date(); + String accessToken = Jwts.builder() + .setExpiration(new Date(now.getTime())) // 생성하자마자 유효기간 끝 + .claim("userId", userId) + .signWith(SignatureAlgorithm.HS256, "accessSecretKeyaccessSecretKeyaccessSecretKey") + .compact(); + + String refreshToken = Jwts.builder() + .setExpiration(new Date(now.getTime() + 360000L)) // 현 시점부터 1시간 동안이 유효기간 + .signWith(SignatureAlgorithm.HS256, "refreshSecretKeyrefreshSecretKeyrefreshSecretKey") + .compact(); + + String anotherRefreshToken = "anotherRefreshToken"; + + RefreshTokenStorage tokenStorage = RefreshTokenStorage.create(savedUser, anotherRefreshToken); + jwtRepository.save(tokenStorage); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", JWT_TOKEN_PREFIX + accessToken); + request.addHeader("Authorization-refresh", JWT_TOKEN_PREFIX + refreshToken); + + //when //then + assertThatThrownBy(() -> jwtService.updateTokenPair(request)) + .isInstanceOf(JwtUnauthorizedTokenException.class) + .hasMessage("저장된 refresh token 과 전달받은 refresh token 이 일치하지 않습니다. 다시 로그인해야합니다."); + + Optional byUser = jwtRepository.findByUser(user); + assertThat(byUser).isEmpty(); + + System.out.println("refresh token = " + refreshToken); + System.out.println("another refresh token = " + anotherRefreshToken); + } + + +} \ No newline at end of file diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 00000000..42c9af63 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,25 @@ +spring: + h2: + console: + enabled: true + path: /h2-console + datasource: + url: jdbc:h2:mem:~/space_spring + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + hibernate: + ddl-auto: create + show-sql: true + properties: + hibernate: + format_sql: true + + sql: + init: + mode: never # sql initialization 을 사용하지 않겠다 + + +server: + port: 8080 \ No newline at end of file