Skip to content

Commit

Permalink
Merge pull request #10 from GSM-MSG/feature/9-logout-function
Browse files Browse the repository at this point in the history
🔀 로그아웃 기능 구현
  • Loading branch information
ta2ye0n authored Nov 7, 2024
2 parents c2821f7 + 916008b commit 4f460b7
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
Optional<RefreshToken> findByToken(String token);

RefreshToken findByEmail(String email);
Optional<RefreshToken> findByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand All @@ -15,6 +17,7 @@ public class AuthController {

private final SignInService signInService;
private final ReissueTokenService reissueTokenService;
private final LogoutService logoutService;

@PostMapping
public ResponseEntity<TokenInfoResponseDto> signIn (@RequestBody SignInRequestDto signInRequestDto) {
Expand All @@ -27,4 +30,10 @@ public ResponseEntity<TokenInfoResponseDto> reissueToken (@RequestBody String re
TokenInfoResponseDto res = reissueTokenService.execute(refreshToken);
return ResponseEntity.ok(res);
}

@DeleteMapping("/logout")
public ResponseEntity<Void> logout(HttpServletRequest request) {
logoutService.execute(request);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.gcms.v3.domain.auth.service;

import jakarta.servlet.http.HttpServletRequest;

public interface LogoutService {
void execute(HttpServletRequest request);
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/gcms/v3/domain/user/util/UserUtil.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
30 changes: 30 additions & 0 deletions src/main/java/com/gcms/v3/global/redis/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -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<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setValueSerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory());
template.setEnableTransactionSupport(true);
return template;
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/gcms/v3/global/redis/RedisProperties.java
Original file line number Diff line number Diff line change
@@ -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
){
}
32 changes: 32 additions & 0 deletions src/main/java/com/gcms/v3/global/redis/RedisUtil.java
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.*;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
}

0 comments on commit 4f460b7

Please sign in to comment.