Skip to content

Commit b8934ad

Browse files
authored
Merge pull request #103 from prgrms-web-devcourse-final-project/develop
refactor[cicd]: jpa DDL 방식 변경
2 parents 40c190d + 0653458 commit b8934ad

File tree

6 files changed

+317
-22
lines changed

6 files changed

+317
-22
lines changed

.github/workflows/CI-CD_Pipeline.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ jobs:
243243
PROD_DATASOURCE_DRIVER=com.mysql.cj.jdbc.Driver
244244
PROD_DATASOURCE_USERNAME=root
245245
PROD_DATASOURCE_PASSWORD=${{ secrets.DB_PASSWORD }}
246+
PROD_JPA_HIBERNATE_DDL_AUTO=update
246247

247248
PROD_REDIS_HOST=redis_1
248249
PROD_REDIS_PORT=6379

backend/src/main/java/com/ai/lawyer/domain/member/service/MemberService.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,22 +70,22 @@ public void logout(String loginId, HttpServletResponse response) {
7070

7171
public MemberResponse refreshToken(String refreshToken, HttpServletResponse response) {
7272
// Redis에서 리프레시 토큰으로 사용자 찾기
73-
String username = tokenProvider.findUsernameByRefreshToken(refreshToken);
74-
if (username == null) {
73+
String loginId = tokenProvider.findUsernameByRefreshToken(refreshToken);
74+
if (loginId == null) {
7575
throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다.");
7676
}
7777

7878
// 리프레시 토큰 유효성 검증
79-
if (!tokenProvider.validateRefreshToken(username, refreshToken)) {
79+
if (!tokenProvider.validateRefreshToken(loginId, refreshToken)) {
8080
throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다.");
8181
}
8282

8383
// 회원 정보 조회
84-
Member member = memberRepository.findByLoginId(username)
84+
Member member = memberRepository.findByLoginId(loginId)
8585
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));
8686

8787
// RTR(Refresh Token Rotation) 패턴: 기존 리프레시 토큰 삭제
88-
tokenProvider.deleteRefreshToken(username);
88+
tokenProvider.deleteRefreshToken(loginId);
8989

9090
// 새로운 액세스 토큰과 리프레시 토큰 생성
9191
String newAccessToken = tokenProvider.generateAccessToken(member);

backend/src/main/java/com/ai/lawyer/global/jwt/CookieUtil.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class CookieUtil {
1010

1111
private static final String ACCESS_TOKEN_NAME = "accessToken";
1212
private static final String REFRESH_TOKEN_NAME = "refreshToken";
13-
private static final int ACCESS_TOKEN_EXPIRE_TIME = 30 * 60; // 30분
13+
private static final int ACCESS_TOKEN_EXPIRE_TIME = 5 * 60; // 5분
1414
private static final int REFRESH_TOKEN_EXPIRE_TIME = 7 * 24 * 60 * 60; // 7일
1515

1616
public void setTokenCookies(HttpServletResponse response, String accessToken, String refreshToken) {
@@ -54,7 +54,6 @@ public String getAccessTokenFromCookies(HttpServletRequest request) {
5454
return getTokenFromCookies(request, ACCESS_TOKEN_NAME);
5555
}
5656

57-
@SuppressWarnings("unused")
5857
public String getRefreshTokenFromCookies(HttpServletRequest request) {
5958
return getTokenFromCookies(request, REFRESH_TOKEN_NAME);
6059
}

backend/src/main/java/com/ai/lawyer/global/jwt/JwtAuthenticationFilter.java

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.ai.lawyer.global.jwt;
22

3+
import com.ai.lawyer.domain.member.entity.Member;
4+
import com.ai.lawyer.domain.member.repositories.MemberRepository;
35
import jakarta.servlet.FilterChain;
46
import jakarta.servlet.ServletException;
57
import jakarta.servlet.http.HttpServletRequest;
@@ -23,17 +25,35 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
2325

2426
private final TokenProvider tokenProvider;
2527
private final CookieUtil cookieUtil;
28+
private final MemberRepository memberRepository;
2629

2730
@Override
2831
protected void doFilterInternal(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable FilterChain filterChain)
2932
throws ServletException, IOException {
3033

31-
if (request != null) {
32-
String accessToken = cookieUtil.getAccessTokenFromCookies(request);
34+
if (request != null && response != null) {
35+
// 1. Authorization 헤더에서 Bearer 토큰 추출 시도 (우선순위 1)
36+
String accessToken = extractTokenFromAuthorizationHeader(request);
37+
boolean fromHeader = accessToken != null;
38+
39+
// 2. Authorization 헤더에 없으면 쿠키에서 토큰 추출 (우선순위 2)
40+
if (accessToken == null) {
41+
accessToken = cookieUtil.getAccessTokenFromCookies(request);
42+
}
3343

3444
// JWT 액세스 토큰 검증 및 인증 처리
35-
if (accessToken != null && tokenProvider.validateToken(accessToken)) {
36-
setAuthentication(accessToken);
45+
if (accessToken != null) {
46+
TokenProvider.TokenValidationResult validationResult = tokenProvider.validateTokenWithResult(accessToken);
47+
48+
if (validationResult == TokenProvider.TokenValidationResult.VALID) {
49+
// 유효한 토큰인 경우 인증 처리
50+
setAuthentication(accessToken);
51+
} else if (validationResult == TokenProvider.TokenValidationResult.EXPIRED && !fromHeader) {
52+
// 만료된 토큰이고 쿠키에서 왔을 경우에만 자동 갱신 시도
53+
// (Authorization 헤더 토큰은 클라이언트가 직접 관리해야 함)
54+
tryAutoRefreshToken(request, response, accessToken);
55+
}
56+
// INVALID인 경우 아무 처리 하지 않음 (인증되지 않은 상태로 진행)
3757
}
3858
}
3959

@@ -42,6 +62,19 @@ protected void doFilterInternal(@Nullable HttpServletRequest request, @Nullable
4262
}
4363
}
4464

65+
/**
66+
* Authorization 헤더에서 Bearer 토큰을 추출합니다.
67+
* @param request HTTP 요청
68+
* @return Bearer 토큰 값 또는 null
69+
*/
70+
private String extractTokenFromAuthorizationHeader(HttpServletRequest request) {
71+
String authHeader = request.getHeader("Authorization");
72+
if (authHeader != null && authHeader.startsWith("Bearer ")) {
73+
return authHeader.substring(7); // "Bearer " 제거
74+
}
75+
return null;
76+
}
77+
4578
/**
4679
* JWT 토큰에서 사용자 정보를 추출하여 Spring Security 인증 객체를 설정합니다.
4780
* @param token JWT 액세스 토큰
@@ -70,6 +103,61 @@ private void setAuthentication(String token) {
70103
}
71104
}
72105

106+
/**
107+
* 만료된 액세스 토큰으로 자동 갱신을 시도합니다.
108+
* @param request HTTP 요청
109+
* @param response HTTP 응답
110+
* @param expiredAccessToken 만료된 액세스 토큰
111+
*/
112+
private void tryAutoRefreshToken(HttpServletRequest request, HttpServletResponse response, String expiredAccessToken) {
113+
try {
114+
// 1. 만료된 토큰에서 loginId 추출
115+
String loginId = tokenProvider.getLoginIdFromExpiredToken(expiredAccessToken);
116+
if (loginId == null) {
117+
log.warn("만료된 토큰에서 loginId 추출 실패");
118+
return;
119+
}
120+
121+
// 2. 쿠키에서 리프레시 토큰 추출
122+
String refreshToken = cookieUtil.getRefreshTokenFromCookies(request);
123+
if (refreshToken == null) {
124+
log.info("리프레시 토큰이 없어 자동 갱신 불가: {}", loginId);
125+
return;
126+
}
127+
128+
// 3. 리프레시 토큰 유효성 검증
129+
if (!tokenProvider.validateRefreshToken(loginId, refreshToken)) {
130+
log.info("유효하지 않은 리프레시 토큰으로 자동 갱신 불가: {}", loginId);
131+
return;
132+
}
133+
134+
// 4. 회원 정보 조회
135+
Member member = memberRepository.findByLoginId(loginId).orElse(null);
136+
if (member == null) {
137+
log.warn("존재하지 않는 회원으로 자동 갱신 불가: {}", loginId);
138+
return;
139+
}
140+
141+
// 5. RTR(Refresh Token Rotation) 패턴: 기존 리프레시 토큰 삭제
142+
tokenProvider.deleteRefreshToken(loginId);
143+
144+
// 6. 새로운 액세스 토큰과 리프레시 토큰 생성
145+
String newAccessToken = tokenProvider.generateAccessToken(member);
146+
String newRefreshToken = tokenProvider.generateRefreshToken(member);
147+
148+
// 7. 새로운 토큰들을 쿠키에 설정
149+
cookieUtil.setTokenCookies(response, newAccessToken, newRefreshToken);
150+
151+
// 8. 새로운 액세스 토큰으로 인증 설정
152+
setAuthentication(newAccessToken);
153+
154+
log.info("액세스 토큰 자동 갱신 성공: {}", loginId);
155+
156+
} catch (Exception e) {
157+
log.warn("액세스 토큰 자동 갱신 실패: {}", e.getMessage());
158+
}
159+
}
160+
73161
/**
74162
* JWT 인증이 필요하지 않은 경로들을 필터링에서 제외합니다.
75163
* @param request HTTP 요청

backend/src/main/java/com/ai/lawyer/global/jwt/TokenProvider.java

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,25 +54,31 @@ public String generateRefreshToken(Member member) {
5454
return refreshToken;
5555
}
5656

57-
public boolean validateToken(String token) {
57+
/**
58+
* 토큰의 상태를 확인합니다.
59+
* @param token JWT 토큰
60+
* @return TokenValidationResult (유효, 만료, 오류)
61+
*/
62+
public TokenValidationResult validateTokenWithResult(String token) {
5863
try {
5964
Jwts.parserBuilder()
6065
.setSigningKey(getSigningKey())
6166
.build()
6267
.parseClaimsJws(token);
63-
return true;
64-
} catch (MalformedJwtException e) {
65-
log.warn("잘못된 JWT 토큰: {}", e.getMessage());
68+
return TokenValidationResult.VALID;
6669
} catch (ExpiredJwtException e) {
6770
log.warn("만료된 JWT 토큰: {}", e.getMessage());
68-
} catch (UnsupportedJwtException e) {
69-
log.warn("지원되지 않는 JWT 토큰: {}", e.getMessage());
70-
} catch (IllegalArgumentException e) {
71-
log.warn("JWT 토큰이 잘못되었습니다: {}", e.getMessage());
72-
} catch (SecurityException e) {
73-
log.warn("JWT 서명이 잘못되었습니다: {}", e.getMessage());
71+
return TokenValidationResult.EXPIRED;
72+
} catch (MalformedJwtException | UnsupportedJwtException | IllegalArgumentException | SecurityException e) {
73+
log.warn("유효하지 않은 JWT 토큰: {}", e.getMessage());
74+
return TokenValidationResult.INVALID;
7475
}
75-
return false;
76+
}
77+
78+
public enum TokenValidationResult {
79+
VALID, // 유효한 토큰
80+
EXPIRED, // 만료된 토큰
81+
INVALID // 잘못된 토큰
7682
}
7783

7884
public Long getMemberIdFromToken(String token) {
@@ -117,6 +123,28 @@ public String getLoginIdFromToken(String token) {
117123
}
118124
}
119125

126+
/**
127+
* 만료된 토큰에서도 loginId를 추출합니다.
128+
* @param token JWT 토큰 (만료되어도 괜찮음)
129+
* @return loginId 또는 null
130+
*/
131+
public String getLoginIdFromExpiredToken(String token) {
132+
try {
133+
Claims claims = Jwts.parserBuilder()
134+
.setSigningKey(getSigningKey())
135+
.build()
136+
.parseClaimsJws(token)
137+
.getBody();
138+
return claims.get("loginid", String.class);
139+
} catch (ExpiredJwtException e) {
140+
// 만료된 토큰이지만 claim은 추출 가능
141+
return e.getClaims().get("loginid", String.class);
142+
} catch (Exception e) {
143+
log.warn("만료된 토큰에서 로그인 ID 추출 실패: {}", e.getMessage());
144+
return null;
145+
}
146+
}
147+
120148
public boolean validateRefreshToken(String loginId, String refreshToken) {
121149
String redisKey = REFRESH_TOKEN_PREFIX + loginId;
122150
String storedToken = (String) redisTemplate.opsForValue().get(redisKey);

0 commit comments

Comments
 (0)