Skip to content

Commit 10650a3

Browse files
authored
Merge pull request #112 from JA-yeong-eop-JA-moeu-JA/feat/#110-logout
✨ feat: 로그아웃 기능 추가 및 토큰 재발급 로직 추가
2 parents 7d7b843 + 68180a1 commit 10650a3

File tree

9 files changed

+110
-7
lines changed

9 files changed

+110
-7
lines changed

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ dependencies {
6060

6161
// S3
6262
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
63+
64+
// Redis
65+
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
6366
}
6467

6568
tasks.named('test') {

src/main/java/com/jajaja/domain/auth/controller/AuthController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.jajaja.domain.auth.dto.TokenResponseDto;
44
import com.jajaja.domain.auth.service.AuthService;
55
import com.jajaja.global.apiPayload.ApiResponse;
6+
import com.jajaja.global.security.annotation.Auth;
67
import io.swagger.v3.oas.annotations.Operation;
78
import io.swagger.v3.oas.annotations.Parameter;
89
import jakarta.servlet.http.HttpServletResponse;
@@ -36,4 +37,14 @@ public ApiResponse<?> reissueToken(
3637
authService.reissueToken(refreshToken, response);
3738
return ApiResponse.onSuccess(null);
3839
}
40+
41+
@Operation(
42+
summary = "로그아웃 API | by 지안/윤진수",
43+
description = "로그아웃을 수행하고 쿠키를 삭제합니다."
44+
)
45+
@PostMapping("/logout")
46+
public ApiResponse<?> logout(@Auth Long memberId, HttpServletResponse response) {
47+
authService.logout(memberId, response);
48+
return ApiResponse.onSuccess(null);
49+
}
3950
}

src/main/java/com/jajaja/domain/auth/service/AuthService.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.jajaja.domain.auth.service;
22

33
import com.jajaja.domain.auth.dto.TokenResponseDto;
4+
import com.jajaja.domain.redis.entity.RefreshToken;
5+
import com.jajaja.domain.redis.repository.RefreshTokenRepository;
46
import com.jajaja.global.apiPayload.code.status.ErrorStatus;
57
import com.jajaja.global.apiPayload.exception.custom.UnauthorizedException;
68
import com.jajaja.global.security.jwt.JwtProvider;
@@ -18,6 +20,7 @@
1820
public class AuthService {
1921

2022
private final JwtProvider jwtProvider;
23+
private final RefreshTokenRepository refreshTokenRepository;
2124

2225
public TokenResponseDto getToken(Long memberId) {
2326
Authentication authentication = new UsernamePasswordAuthenticationToken(String.valueOf(memberId), null, null);
@@ -32,10 +35,30 @@ public void reissueToken(String refreshToken, HttpServletResponse response) {
3235
}
3336
jwtProvider.validateRefreshToken(refreshToken);
3437
Authentication authentication = jwtProvider.getAuthentication(refreshToken);
35-
// TODO: Redis에 저장된 refreshToken과 비교하여 일치하는지 확인
38+
39+
// Redis에 저장된 refreshToken과 비교하여 일치하는지 확인
40+
String memberId = authentication.getName();
41+
RefreshToken refreshTokenEntity = refreshTokenRepository.findById(memberId)
42+
.orElseThrow(() -> new UnauthorizedException(ErrorStatus.NOT_MATCH_REFRESH_TOKEN));
43+
if (!refreshTokenEntity.getRefreshToken().equals(refreshToken)) {
44+
throw new UnauthorizedException(ErrorStatus.NOT_MATCH_REFRESH_TOKEN);
45+
}
46+
47+
// 새로 accessToken과 refreshToken 발급
3648
String newAccessToken = jwtProvider.generateAccessToken(authentication);
3749
String newRefreshToken = jwtProvider.generateRefreshToken(authentication);
38-
// TODO: Redis에 새로 발급한 refreshToken 저장
50+
51+
// Redis에 새로 발급한 refreshToken 저장
52+
RefreshToken newRefreshTokenEntity = new RefreshToken(memberId, newRefreshToken);
53+
refreshTokenRepository.save(newRefreshTokenEntity);
54+
3955
jwtProvider.writeTokenCookies(response, newAccessToken, newRefreshToken);
4056
}
57+
58+
public void logout(Long memberId, HttpServletResponse response) {
59+
refreshTokenRepository.deleteById(memberId.toString());
60+
String accessToken = "";
61+
String refreshToken = "";
62+
jwtProvider.writeTokenCookies(response, accessToken, refreshToken);
63+
}
4164
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.jajaja.domain.redis.entity;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.data.annotation.Id;
7+
import org.springframework.data.redis.core.RedisHash;
8+
9+
@Getter
10+
@RequiredArgsConstructor
11+
@RedisHash(value = "refresh_token", timeToLive = 604800)
12+
public class RefreshToken {
13+
14+
@Id
15+
private String memberId;
16+
17+
private String refreshToken;
18+
19+
@Builder
20+
public RefreshToken(String memberId, String refreshToken) {
21+
this.memberId = memberId;
22+
this.refreshToken = refreshToken;
23+
}
24+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.jajaja.domain.redis.repository;
2+
3+
import com.jajaja.domain.redis.entity.RefreshToken;
4+
import org.springframework.data.repository.CrudRepository;
5+
6+
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
7+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.jajaja.global.config;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.data.redis.connection.RedisConnectionFactory;
7+
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
8+
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
9+
10+
@Configuration
11+
@EnableRedisRepositories
12+
public class RedisConfig {
13+
14+
@Value("${spring.data.redis.port}")
15+
private int port;
16+
17+
@Value("${spring.data.redis.host}")
18+
private String host;
19+
20+
@Bean
21+
public RedisConnectionFactory redisConnectionFactory() {
22+
return new LettuceConnectionFactory(host, port);
23+
}
24+
}

src/main/java/com/jajaja/global/security/jwt/JwtProvider.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
1414
import org.springframework.security.core.Authentication;
1515
import org.springframework.stereotype.Component;
16+
import org.springframework.util.StringUtils;
1617

1718
import javax.crypto.SecretKey;
1819
import java.util.Date;
@@ -85,19 +86,19 @@ public void writeTokenCookies(HttpServletResponse response, String accessToken,
8586
.httpOnly(true)
8687
.secure(secure)
8788
.sameSite(sameSite)
88-
.maxAge(jwtProperties.getExpiration().getAccess() / 1000);
89+
.maxAge(StringUtils.hasText(accessToken) ? jwtProperties.getExpiration().getAccess() / 1000 : 0);
8990

9091
ResponseCookie.ResponseCookieBuilder refreshBuilder = ResponseCookie.from("refreshToken", refreshToken)
9192
.path("/")
9293
.httpOnly(true)
9394
.secure(secure)
9495
.sameSite(sameSite)
95-
.maxAge(jwtProperties.getExpiration().getRefresh() / 1000);
96+
.maxAge(StringUtils.hasText(refreshToken) ? jwtProperties.getExpiration().getRefresh() / 1000 : 0);
9697

9798

98-
if (jwtProperties.getCookieDomain() != null && !jwtProperties.getCookieDomain().isBlank()) {
99-
accessBuilder.domain(jwtProperties.getCookieDomain()).build();
100-
refreshBuilder.domain(jwtProperties.getCookieDomain()).build();
99+
if (StringUtils.hasText(jwtProperties.getCookieDomain())) {
100+
accessBuilder.domain(jwtProperties.getCookieDomain());
101+
refreshBuilder.domain(jwtProperties.getCookieDomain());
101102
}
102103

103104
response.addHeader("Set-Cookie", accessBuilder.build().toString());

src/main/java/com/jajaja/global/security/oauth/CustomOAuth2SuccessHandler.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.jajaja.global.security.oauth;
22

3+
import com.jajaja.domain.redis.entity.RefreshToken;
4+
import com.jajaja.domain.redis.repository.RefreshTokenRepository;
35
import com.jajaja.global.security.jwt.JwtProperties;
46
import com.jajaja.global.security.jwt.JwtProvider;
57
import jakarta.servlet.ServletException;
@@ -18,11 +20,14 @@ public class CustomOAuth2SuccessHandler implements AuthenticationSuccessHandler
1820

1921
private final JwtProvider jwtProvider;
2022
private final JwtProperties jwtProperties;
23+
private final RefreshTokenRepository refreshTokenRepository;
2124

2225
@Override
2326
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
2427
String accessToken = jwtProvider.generateAccessToken(authentication);
2528
String refreshToken = jwtProvider.generateRefreshToken(authentication);
29+
RefreshToken refreshTokenEntity = new RefreshToken(authentication.getName(), refreshToken);
30+
refreshTokenRepository.save(refreshTokenEntity);
2631
jwtProvider.writeTokenCookies(response, accessToken, refreshToken);
2732
response.sendRedirect(jwtProperties.getRedirectUrl());
2833
}

src/main/resources/application.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ spring:
5050
token-uri: https://kauth.kakao.com/oauth/token
5151
user-info-uri: https://kapi.kakao.com/v2/user/me
5252
user-name-attribute: id
53+
data:
54+
redis:
55+
host: ${REDIS_HOST}
56+
port: ${REDIS_PORT}
57+
5358
jwt:
5459
token:
5560
secret-key: ${JWT_SECRET_KEY}

0 commit comments

Comments
 (0)