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 index 08172e1..73c77b2 100644 --- 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 @@ -8,5 +8,5 @@ public interface RefreshTokenRepository extends CrudRepository { Optional findByToken(String token); - RefreshToken findByEmail(String email); + Optional findByEmail(String email); } 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 0e78609..1bcd88d 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,8 +2,10 @@ 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.LogoutService; import com.gcms.v3.domain.auth.service.ReissueTokenService; import com.gcms.v3.domain.auth.service.SignInService; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -15,6 +17,7 @@ public class AuthController { private final SignInService signInService; private final ReissueTokenService reissueTokenService; + private final LogoutService logoutService; @PostMapping public ResponseEntity signIn (@RequestBody SignInRequestDto signInRequestDto) { @@ -27,4 +30,10 @@ public ResponseEntity reissueToken (@RequestBody String re TokenInfoResponseDto res = reissueTokenService.execute(refreshToken); return ResponseEntity.ok(res); } + + @DeleteMapping("/logout") + public ResponseEntity logout(HttpServletRequest request) { + logoutService.execute(request); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/com/gcms/v3/domain/auth/service/LogoutService.java b/src/main/java/com/gcms/v3/domain/auth/service/LogoutService.java new file mode 100644 index 0000000..e3246a4 --- /dev/null +++ b/src/main/java/com/gcms/v3/domain/auth/service/LogoutService.java @@ -0,0 +1,7 @@ +package com.gcms.v3.domain.auth.service; + +import jakarta.servlet.http.HttpServletRequest; + +public interface LogoutService { + void execute(HttpServletRequest request); +} diff --git a/src/main/java/com/gcms/v3/domain/auth/service/impl/LogoutServiceImpl.java b/src/main/java/com/gcms/v3/domain/auth/service/impl/LogoutServiceImpl.java new file mode 100644 index 0000000..808549e --- /dev/null +++ b/src/main/java/com/gcms/v3/domain/auth/service/impl/LogoutServiceImpl.java @@ -0,0 +1,38 @@ +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.service.LogoutService; +import com.gcms.v3.domain.user.domain.entity.User; +import com.gcms.v3.domain.user.util.UserUtil; +import com.gcms.v3.global.redis.RedisUtil; +import com.gcms.v3.global.security.jwt.JwtTokenProvider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +@Transactional +public class LogoutServiceImpl implements LogoutService { + + private final UserUtil userUtil; + private final RefreshTokenRepository refreshTokenRepository; + private final JwtTokenProvider jwtTokenProvider; + private final RedisUtil redisUtil; + + public void execute(HttpServletRequest request) { + String accessToken = jwtTokenProvider.resolveToken(request); + + User user = userUtil.getCurrentUser(); + + RefreshToken refreshToken = refreshTokenRepository.findByEmail(user.getEmail()) + .orElseThrow(UserNotFoundException::new); + + refreshTokenRepository.delete(refreshToken); + + redisUtil.setBlackList(accessToken, "access_token", jwtTokenProvider.getExpiration(accessToken)); + } +} diff --git a/src/main/java/com/gcms/v3/domain/user/util/UserUtil.java b/src/main/java/com/gcms/v3/domain/user/util/UserUtil.java new file mode 100644 index 0000000..23c95aa --- /dev/null +++ b/src/main/java/com/gcms/v3/domain/user/util/UserUtil.java @@ -0,0 +1,21 @@ +package com.gcms.v3.domain.user.util; + +import com.gcms.v3.domain.auth.exception.UserNotFoundException; +import com.gcms.v3.domain.user.domain.entity.User; +import com.gcms.v3.domain.user.domain.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UserUtil { + + private final UserRepository userRepository; + + public User getCurrentUser() { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return userRepository.findByEmail(email) + .orElseThrow(UserNotFoundException::new); + } +} diff --git a/src/main/java/com/gcms/v3/global/redis/RedisConfig.java b/src/main/java/com/gcms/v3/global/redis/RedisConfig.java new file mode 100644 index 0000000..ddbfe37 --- /dev/null +++ b/src/main/java/com/gcms/v3/global/redis/RedisConfig.java @@ -0,0 +1,30 @@ +package com.gcms.v3.global.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@RequiredArgsConstructor +public class RedisConfig { + private final RedisProperties redisProperties; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisProperties.host(), redisProperties.port()); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate template = new RedisTemplate<>(); + template.setValueSerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new StringRedisSerializer()); + template.setConnectionFactory(redisConnectionFactory()); + template.setEnableTransactionSupport(true); + return template; + } +} diff --git a/src/main/java/com/gcms/v3/global/redis/RedisProperties.java b/src/main/java/com/gcms/v3/global/redis/RedisProperties.java new file mode 100644 index 0000000..5f1d894 --- /dev/null +++ b/src/main/java/com/gcms/v3/global/redis/RedisProperties.java @@ -0,0 +1,10 @@ +package com.gcms.v3.global.redis; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "spring.data.redis") +public record RedisProperties ( + String host, + int port +){ +} diff --git a/src/main/java/com/gcms/v3/global/redis/RedisUtil.java b/src/main/java/com/gcms/v3/global/redis/RedisUtil.java new file mode 100644 index 0000000..b615c06 --- /dev/null +++ b/src/main/java/com/gcms/v3/global/redis/RedisUtil.java @@ -0,0 +1,32 @@ +package com.gcms.v3.global.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +public class RedisUtil { + + private final RedisTemplate redisBlackListTemplate; + + public boolean checkExistsValue(String value) { + return !value.equals("false"); + } + + public void setBlackList(String key, Object o, Long milliSeconds) { + redisBlackListTemplate.setValueSerializer(new Jackson2JsonRedisSerializer(o.getClass())); + redisBlackListTemplate.opsForValue().set(key, o, milliSeconds, TimeUnit.MILLISECONDS); + } + + public Object getBlackList(String key) { + return redisBlackListTemplate.opsForValue().get(key); + } + + public boolean hasKeyBlackList(String key) { + return redisBlackListTemplate.hasKey(key); + } +} 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 64d13fe..21ef9a4 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 @@ -44,6 +44,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti authorizeRequests .requestMatchers(HttpMethod.POST, "/v3/auth").permitAll() .requestMatchers(HttpMethod.POST, "/v3/auth/reissueToken").authenticated() + .requestMatchers(HttpMethod.DELETE, "/v3/auth/logout").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 302bd08..732c684 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 @@ -1,6 +1,7 @@ package com.gcms.v3.global.security.jwt; import com.gcms.v3.domain.auth.presentation.data.response.TokenInfoResponseDto; +import com.gcms.v3.global.redis.RedisUtil; import com.gcms.v3.global.security.exception.InvalidAuthTokenException; import com.gcms.v3.global.security.auth.AuthDetailsService; import io.jsonwebtoken.*; @@ -37,6 +38,7 @@ public class JwtTokenProvider { private static Key refreshtokenkey; private final AuthDetailsService authDetailsService; private final JwtProperties jwtProperties; + private final RedisUtil redisUtil; @PostConstruct public void init() { @@ -116,6 +118,11 @@ public String resolveToken(HttpServletRequest request) { public boolean validateToken(String token) { try { Jwts.parserBuilder().setSigningKey(accessTokenkey).build().parseClaimsJws(token); + + if (redisUtil.hasKeyBlackList(token)) { + throw new InvalidAuthTokenException(); + } + return true; } catch (SecurityException | MalformedJwtException e) { throw new InvalidAuthTokenException(); @@ -148,4 +155,14 @@ private Claims getTokenBody(String token, Key secret) { .parseClaimsJws(token) .getBody(); } + + public Long getExpiration(String accessToken) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(accessTokenkey) + .build() + .parseClaimsJws(accessToken) + .getBody(); + + return claims.getExpiration().getTime(); + } }