diff --git a/src/main/java/com/gcms/v3/domain/auth/domain/entity/RefreshToken.java b/src/main/java/com/gcms/v3/domain/auth/domain/entity/RefreshToken.java new file mode 100644 index 0000000..beba913 --- /dev/null +++ b/src/main/java/com/gcms/v3/domain/auth/domain/entity/RefreshToken.java @@ -0,0 +1,20 @@ +package com.gcms.v3.domain.auth.domain.entity; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +@Getter +@Builder +@RedisHash(value = "refreshToken", timeToLive = 60L * 60 * 24 * 7) +public class RefreshToken { + + @Indexed + private String email; + + @Id + @Indexed + private String token; +} diff --git a/src/main/java/com/gcms/v3/domain/auth/domain/repository/RefreshTokenRepository.java b/src/main/java/com/gcms/v3/domain/auth/domain/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..08172e1 --- /dev/null +++ b/src/main/java/com/gcms/v3/domain/auth/domain/repository/RefreshTokenRepository.java @@ -0,0 +1,12 @@ +package com.gcms.v3.domain.auth.domain.repository; + +import com.gcms.v3.domain.auth.domain.entity.RefreshToken; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface RefreshTokenRepository extends CrudRepository { + Optional findByToken(String token); + + RefreshToken findByEmail(String email); +} diff --git a/src/main/java/com/gcms/v3/domain/auth/exception/ExpiredTokenException.java b/src/main/java/com/gcms/v3/domain/auth/exception/ExpiredTokenException.java new file mode 100644 index 0000000..dcd2141 --- /dev/null +++ b/src/main/java/com/gcms/v3/domain/auth/exception/ExpiredTokenException.java @@ -0,0 +1,10 @@ +package com.gcms.v3.domain.auth.exception; + +import com.gcms.v3.global.error.BasicException; +import com.gcms.v3.global.error.ErrorCode; + +public class ExpiredTokenException extends BasicException { + public ExpiredTokenException() { + super(ErrorCode.EXPIRED_TOKEN); + } +} diff --git a/src/main/java/com/gcms/v3/domain/auth/presentation/AuthController.java b/src/main/java/com/gcms/v3/domain/auth/presentation/AuthController.java index 3196e4e..0e78609 100644 --- a/src/main/java/com/gcms/v3/domain/auth/presentation/AuthController.java +++ b/src/main/java/com/gcms/v3/domain/auth/presentation/AuthController.java @@ -2,6 +2,7 @@ import com.gcms.v3.domain.auth.presentation.data.request.SignInRequestDto; import com.gcms.v3.domain.auth.presentation.data.response.TokenInfoResponseDto; +import com.gcms.v3.domain.auth.service.ReissueTokenService; import com.gcms.v3.domain.auth.service.SignInService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -13,10 +14,17 @@ public class AuthController { private final SignInService signInService; + private final ReissueTokenService reissueTokenService; @PostMapping public ResponseEntity signIn (@RequestBody SignInRequestDto signInRequestDto) { TokenInfoResponseDto res = signInService.execute(signInRequestDto); return ResponseEntity.ok(res); } + + @PostMapping("/reissueToken") + public ResponseEntity reissueToken (@RequestBody String refreshToken) { + TokenInfoResponseDto res = reissueTokenService.execute(refreshToken); + return ResponseEntity.ok(res); + } } diff --git a/src/main/java/com/gcms/v3/domain/auth/service/ReissueTokenService.java b/src/main/java/com/gcms/v3/domain/auth/service/ReissueTokenService.java new file mode 100644 index 0000000..d74c45c --- /dev/null +++ b/src/main/java/com/gcms/v3/domain/auth/service/ReissueTokenService.java @@ -0,0 +1,7 @@ +package com.gcms.v3.domain.auth.service; + +import com.gcms.v3.domain.auth.presentation.data.response.TokenInfoResponseDto; + +public interface ReissueTokenService { + TokenInfoResponseDto execute(String refreshToken); +} diff --git a/src/main/java/com/gcms/v3/domain/auth/service/impl/ReissueTokenServiceImpl.java b/src/main/java/com/gcms/v3/domain/auth/service/impl/ReissueTokenServiceImpl.java new file mode 100644 index 0000000..8281444 --- /dev/null +++ b/src/main/java/com/gcms/v3/domain/auth/service/impl/ReissueTokenServiceImpl.java @@ -0,0 +1,49 @@ +package com.gcms.v3.domain.auth.service.impl; + +import com.gcms.v3.domain.auth.domain.entity.RefreshToken; +import com.gcms.v3.domain.auth.domain.repository.RefreshTokenRepository; +import com.gcms.v3.domain.auth.exception.UserNotFoundException; +import com.gcms.v3.domain.auth.presentation.data.response.TokenInfoResponseDto; +import com.gcms.v3.domain.auth.service.ReissueTokenService; +import com.gcms.v3.domain.user.domain.entity.User; +import com.gcms.v3.domain.user.domain.repository.UserRepository; +import com.gcms.v3.global.security.exception.ExpiredTokenException; +import com.gcms.v3.global.security.jwt.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class ReissueTokenServiceImpl implements ReissueTokenService { + + private final JwtTokenProvider jwtTokenProvider; + private final UserRepository userRepository; + private final RefreshTokenRepository refreshTokenRepository; + + public TokenInfoResponseDto execute(String refreshToken) { + String refresh = jwtTokenProvider.parseToken(refreshToken); + + String email = jwtTokenProvider.exactEmailFromRefreshToken(refresh); + + User user = userRepository.findByEmail(email) + .orElseThrow(UserNotFoundException::new); + + RefreshToken existingRefreshToken = refreshTokenRepository.findByToken(refresh) + .orElseThrow(ExpiredTokenException::new); + + TokenInfoResponseDto responseDto = jwtTokenProvider.generateToken(user.getEmail()); + + saveRefreshToken(existingRefreshToken.getEmail(), responseDto.refreshToken()); + + return responseDto; + } + + private void saveRefreshToken(String email, String refreshToken) { + RefreshToken token = RefreshToken.builder() + .email(email) + .token(refreshToken) + .build(); + + refreshTokenRepository.save(token); + } +} diff --git a/src/main/java/com/gcms/v3/domain/auth/service/impl/SignInServiceImpl.java b/src/main/java/com/gcms/v3/domain/auth/service/impl/SignInServiceImpl.java index a3974e5..b350f99 100644 --- a/src/main/java/com/gcms/v3/domain/auth/service/impl/SignInServiceImpl.java +++ b/src/main/java/com/gcms/v3/domain/auth/service/impl/SignInServiceImpl.java @@ -1,5 +1,7 @@ package com.gcms.v3.domain.auth.service.impl; +import com.gcms.v3.domain.auth.domain.entity.RefreshToken; +import com.gcms.v3.domain.auth.domain.repository.RefreshTokenRepository; import com.gcms.v3.domain.auth.presentation.data.request.SignInRequestDto; import com.gcms.v3.domain.auth.presentation.data.response.TokenInfoResponseDto; import com.gcms.v3.domain.auth.service.SignInService; @@ -24,6 +26,7 @@ public class SignInServiceImpl implements SignInService { private final UserRepository userRepository; private final JwtTokenProvider jwtTokenProvider; private final UserRoleRepository userRoleRepository; + private final RefreshTokenRepository refreshTokenRepository; public TokenInfoResponseDto execute(SignInRequestDto signInRequestDto) { String accessToken = oAuth2Service.requestAccessToken(signInRequestDto.code()); @@ -32,7 +35,11 @@ public TokenInfoResponseDto execute(SignInRequestDto signInRequestDto) { User user = userRepository.findByEmail(googleOAuth2UserInfo.getEmail()) .orElseGet(() -> toEntity(googleOAuth2UserInfo)); - return jwtTokenProvider.generateToken(user.getEmail()); + TokenInfoResponseDto responseDto = jwtTokenProvider.generateToken(user.getEmail()); + + saveRefreshToken(user.getEmail(), responseDto.refreshToken()); + + return responseDto; } private User toEntity(GoogleOAuth2UserInfo googleOAuth2UserInfo) { @@ -55,4 +62,12 @@ private void saveAuthority(User user) { userRoleRepository.save(userRole); } + private void saveRefreshToken(String email, String refreshToken) { + RefreshToken token = RefreshToken.builder() + .email(email) + .token(refreshToken) + .build(); + + refreshTokenRepository.save(token); + } } diff --git a/src/main/java/com/gcms/v3/global/security/config/SecurityConfig.java b/src/main/java/com/gcms/v3/global/security/config/SecurityConfig.java index f4a717d..64d13fe 100644 --- a/src/main/java/com/gcms/v3/global/security/config/SecurityConfig.java +++ b/src/main/java/com/gcms/v3/global/security/config/SecurityConfig.java @@ -43,6 +43,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests((authorizeRequests) -> authorizeRequests .requestMatchers(HttpMethod.POST, "/v3/auth").permitAll() + .requestMatchers(HttpMethod.POST, "/v3/auth/reissueToken").authenticated() ) .addFilterBefore(new ExceptionFilter(objectMapper), UsernamePasswordAuthenticationFilter.class) diff --git a/src/main/java/com/gcms/v3/global/security/jwt/JwtTokenProvider.java b/src/main/java/com/gcms/v3/global/security/jwt/JwtTokenProvider.java index dd7e0ab..302bd08 100644 --- a/src/main/java/com/gcms/v3/global/security/jwt/JwtTokenProvider.java +++ b/src/main/java/com/gcms/v3/global/security/jwt/JwtTokenProvider.java @@ -126,4 +126,26 @@ public boolean validateToken(String token) { } } + public String parseToken(String token) { + if (token.startsWith(TOKEN_PREFIX)) { + return token.replace(TOKEN_PREFIX, ""); + } + else return null; + } + + public String exactEmailFromRefreshToken(String refreshToken) { + return getTokenSubject(refreshToken, refreshtokenkey); + } + + private String getTokenSubject (String token, Key secret) { + return getTokenBody(token, secret).getSubject(); + } + + private Claims getTokenBody(String token, Key secret) { + return Jwts.parserBuilder() + .setSigningKey(secret) + .build() + .parseClaimsJws(token) + .getBody(); + } }