Skip to content

Commit 2cf30d5

Browse files
committed
fix[member]: 테스트 코드에 수정된 로직 반영 및 주석 수정, 환경변수 오타 수정
1 parent b254d93 commit 2cf30d5

File tree

8 files changed

+123
-88
lines changed

8 files changed

+123
-88
lines changed

backend/.env.default

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ SPRING__SECURITY__OAUTH2__CLIENT__REGISTRATION__KAKAO__CLIENT_SECRET=NEED_TO_SET
77
SPRING__SECURITY__OAUTH2__CLIENT__REGISTRATION__NAVER__CLIENT_ID=NEED_TO_SET
88
SPRING__SECURITY__OAUTH2__CLIENT__REGISTRATION__NAVER__CLIENT_SECRET=NEED_TO_SET
99

10-
CUSTOM__JWT__SECRET_KEY=NEED_TO_SET
10+
CUSTOM_JWT_SECRET_KEY=NEED_TO_SET
1111
CUSTOM_JWT_ACCESS_TOKEN_EXPIRATION_SECONDS=NEED_TO_SET
1212

1313
PROD_DATASOURCE_URL=NEED_TO_SET

backend/src/main/java/com/ai/lawyer/domain/member/controller/MemberController.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ public ResponseEntity<Void> logout(Authentication authentication, HttpServletRes
6767
if (authentication != null && authentication.getName() != null) {
6868
String loginId = authentication.getName();
6969
memberService.logout(loginId, response);
70-
log.info("로그아웃 완료: email={}", loginId);
70+
log.info("로그아웃 완료: memberId={}", loginId);
7171
} else {
72-
// 인증 정보가 없어도 쿠키는 클리어
72+
// 인증되지 않은 상태에서도 클라이언트 쿠키 클리어 처리
7373
memberService.logout("", response);
7474
log.info("인증 정보 없이 로그아웃 완료");
7575
}
@@ -87,7 +87,7 @@ public ResponseEntity<MemberResponse> refreshToken(HttpServletRequest request,
8787
HttpServletResponse response) {
8888
log.info("토큰 재발급 요청");
8989

90-
// 쿠키에서 리프레시 토큰 추출 (간단한 방법)
90+
// HTTP 쿠키에서 리프레시 토큰 추출
9191
String refreshToken = extractRefreshTokenFromCookies(request);
9292

9393
if (refreshToken == null) {
@@ -116,7 +116,7 @@ public ResponseEntity<Void> withdraw(Authentication authentication, HttpServletR
116116
log.info("회원탈퇴 요청: memberId={}, email={}", memberId, loginId);
117117

118118
memberService.withdraw(memberId);
119-
memberService.logout(loginId, response); // 탈퇴 후 로그아웃 처리
119+
memberService.logout(loginId, response); // 회원 탈퇴 후 세션 및 토큰 정리
120120
log.info("회원탈퇴 성공: memberId={}, email={}", memberId, loginId);
121121
return ResponseEntity.ok().build();
122122
}
@@ -140,6 +140,11 @@ public ResponseEntity<MemberResponse> getMyInfo(Authentication authentication) {
140140
return ResponseEntity.ok(response);
141141
}
142142

143+
/**
144+
* HTTP 쿠키에서 리프레시 토큰을 추출합니다.
145+
* @param request HTTP 요청 객체
146+
* @return 리프레시 토큰 값 또는 null
147+
*/
143148
private String extractRefreshTokenFromCookies(HttpServletRequest request) {
144149
if (request.getCookies() != null) {
145150
for (jakarta.servlet.http.Cookie cookie : request.getCookies()) {

backend/src/main/java/com/ai/lawyer/domain/member/exception/MemberExceptionHandler.java

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,40 @@
1212
@Slf4j
1313
public class MemberExceptionHandler {
1414

15+
/**
16+
* IllegalArgumentException 고도화 처리
17+
* 메시지에 따라 HTTP 상태코드와 에러 메시지 다르게 반환
18+
*/
1519
@ExceptionHandler(IllegalArgumentException.class)
1620
public ResponseEntity<MemberErrorResponse> handleMemberIllegalArgumentException(IllegalArgumentException e) {
1721
log.warn("Member 도메인 IllegalArgumentException: {}", e.getMessage());
18-
MemberErrorResponse errorResponse = MemberErrorResponse.of(
19-
e.getMessage(),
20-
HttpStatus.BAD_REQUEST.value(),
21-
"잘못된 요청"
22-
);
23-
return ResponseEntity.badRequest().body(errorResponse);
22+
23+
String msg = e.getMessage();
24+
HttpStatus status;
25+
String error = switch (msg) {
26+
case "이미 존재하는 이메일입니다.", "잘못된 입력입니다." -> {
27+
status = HttpStatus.BAD_REQUEST;
28+
yield "잘못된 요청";
29+
}
30+
case "존재하지 않는 회원입니다.", "비밀번호가 일치하지 않습니다." -> {
31+
status = HttpStatus.UNAUTHORIZED;
32+
yield "인증 실패";
33+
}
34+
default -> {
35+
status = HttpStatus.BAD_REQUEST;
36+
yield "오류 발생";
37+
}
38+
};
39+
40+
// 메시지 기반으로 상태코드 결정
41+
42+
MemberErrorResponse errorResponse = MemberErrorResponse.of(msg, status.value(), error);
43+
return ResponseEntity.status(status).body(errorResponse);
2444
}
2545

46+
/**
47+
* 인증 관련 예외 처리
48+
*/
2649
@ExceptionHandler(MemberAuthenticationException.class)
2750
public ResponseEntity<MemberErrorResponse> handleMemberAuthenticationException(MemberAuthenticationException e) {
2851
log.warn("Member 도메인 AuthenticationException: {}", e.getMessage());
@@ -34,6 +57,9 @@ public ResponseEntity<MemberErrorResponse> handleMemberAuthenticationException(M
3457
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse);
3558
}
3659

60+
/**
61+
* 유효성 검증 실패 처리
62+
*/
3763
@ExceptionHandler(MethodArgumentNotValidException.class)
3864
public ResponseEntity<MemberErrorResponse> handleMemberValidationException(MethodArgumentNotValidException e) {
3965
String message = e.getBindingResult().getAllErrors().getFirst().getDefaultMessage();
@@ -45,4 +71,4 @@ public ResponseEntity<MemberErrorResponse> handleMemberValidationException(Metho
4571
);
4672
return ResponseEntity.badRequest().body(errorResponse);
4773
}
48-
}
74+
}

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

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,34 +46,32 @@ public MemberResponse login(MemberLoginRequest request, HttpServletResponse resp
4646
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
4747
}
4848

49-
// 토큰 생성 및 쿠키 설정
49+
// JWT 액세스 토큰과 리프레시 토큰 생성 후 HTTP 쿠키에 설정
5050
String accessToken = tokenProvider.generateAccessToken(member);
5151
String refreshToken = tokenProvider.generateRefreshToken(member);
5252
cookieUtil.setTokenCookies(response, accessToken, refreshToken);
5353

54-
// TODO: 추후 레디스에 토큰-회원 매핑 정보 저장
55-
5654
return MemberResponse.from(member);
5755
}
5856

5957
public void logout(String loginId, HttpServletResponse response) {
60-
// loginId가 있는 경우에만 Redis에서 리프레시 토큰 삭제
58+
// 로그인 ID가 존재할 경우 Redis에서 리프레시 토큰 삭제
6159
if (loginId != null && !loginId.trim().isEmpty()) {
6260
tokenProvider.deleteRefreshToken(loginId);
6361
}
6462

65-
// 쿠키는 항상 클리어 (인증 정보가 없어도 클라이언트의 쿠키는 삭제해야 함)
63+
// 인증 상태와 관계없이 클라이언트 쿠키 클리어
6664
cookieUtil.clearTokenCookies(response);
6765
}
6866

6967
public MemberResponse refreshToken(String refreshToken, HttpServletResponse response) {
70-
// Redis에서 리프레시 토큰으로 사용자를 찾기 (Redis 키 패턴: refresh_token:loginId)
68+
// Redis에서 리프레시 토큰으로 사용자 찾기
7169
String username = tokenProvider.findUsernameByRefreshToken(refreshToken);
7270
if (username == null) {
7371
throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다.");
7472
}
7573

76-
// Redis에서 리프레시 토큰 유효성 검증
74+
// 리프레시 토큰 유효성 검증
7775
if (!tokenProvider.validateRefreshToken(username, refreshToken)) {
7876
throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다.");
7977
}
@@ -82,14 +80,14 @@ public MemberResponse refreshToken(String refreshToken, HttpServletResponse resp
8280
Member member = memberRepository.findByLoginId(username)
8381
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));
8482

85-
// 기존 리프레시 토큰 Redis에서 삭제 (RTR 패턴)
83+
// RTR(Refresh Token Rotation) 패턴: 기존 리프레시 토큰 삭제
8684
tokenProvider.deleteRefreshToken(username);
8785

8886
// 새로운 액세스 토큰과 리프레시 토큰 생성
8987
String newAccessToken = tokenProvider.generateAccessToken(member);
9088
String newRefreshToken = tokenProvider.generateRefreshToken(member);
9189

92-
// 새로운 토큰들을 쿠키로 설정
90+
// 새로운 토큰들을 HTTP 쿠키에 설정
9391
cookieUtil.setTokenCookies(response, newAccessToken, newRefreshToken);
9492

9593
return MemberResponse.from(member);
@@ -110,11 +108,11 @@ public MemberResponse getMemberById(Long memberId) {
110108
return MemberResponse.from(member);
111109
}
112110

113-
public Member findByLoginId(String loginId) {
114-
return memberRepository.findByLoginId(loginId)
115-
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));
116-
}
117-
111+
/**
112+
* 로그인 ID 중복 검사
113+
* @param loginId 검사할 로그인 ID
114+
* @throws IllegalArgumentException 중복된 로그인 ID인 경우
115+
*/
118116
private void validateDuplicateLoginId(String loginId) {
119117
if (memberRepository.existsByLoginId(loginId)) {
120118
throw new IllegalArgumentException("이미 존재하는 이메일입니다.");

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ protected void doFilterInternal(@Nullable HttpServletRequest request, @Nullable
3131
if (request != null) {
3232
String accessToken = cookieUtil.getAccessTokenFromCookies(request);
3333

34-
// 액세스 토큰이 있는 경우
34+
// JWT 액세스 토큰 검증 및 인증 처리
3535
if (accessToken != null && tokenProvider.validateToken(accessToken)) {
36-
// 유효한 토큰 - 인증 정보 설정
3736
setAuthentication(accessToken);
3837
}
3938
}
@@ -43,9 +42,12 @@ protected void doFilterInternal(@Nullable HttpServletRequest request, @Nullable
4342
}
4443
}
4544

45+
/**
46+
* JWT 토큰에서 사용자 정보를 추출하여 Spring Security 인증 객체를 설정합니다.
47+
* @param token JWT 액세스 토큰
48+
*/
4649
private void setAuthentication(String token) {
4750
try {
48-
// 토큰에서 사용자 정보 추출
4951
Long memberId = tokenProvider.getMemberIdFromToken(token);
5052
String role = tokenProvider.getRoleFromToken(token);
5153

@@ -54,11 +56,11 @@ private void setAuthentication(String token) {
5456
return;
5557
}
5658

57-
// 권한 설정 (토큰에서 추출한 role 사용)
59+
// Spring Security 권한 형식으로 변환
5860
String authority = "ROLE_" + (role != null ? role : "USER");
5961
List<SimpleGrantedAuthority> authorities = List.of(new SimpleGrantedAuthority(authority));
6062

61-
// memberId를 principal로 사용하는 인증 객체 생성
63+
// memberId를 principal로 하는 인증 객체 생성
6264
UsernamePasswordAuthenticationToken authentication =
6365
new UsernamePasswordAuthenticationToken(memberId, null, authorities);
6466

@@ -68,10 +70,14 @@ private void setAuthentication(String token) {
6870
}
6971
}
7072

73+
/**
74+
* JWT 인증이 필요하지 않은 경로들을 필터링에서 제외합니다.
75+
* @param request HTTP 요청
76+
* @return true인 경우 필터 제외
77+
*/
7178
@Override
7279
protected boolean shouldNotFilter(HttpServletRequest request) {
7380
String path = request.getRequestURI();
74-
// 인증이 필요없는 경로들 (구체적으로 명시)
7581
return path.equals("/api/auth/signup") ||
7682
path.equals("/api/auth/login") ||
7783
path.equals("/api/auth/refresh") ||

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public String generateAccessToken(Member member) {
4747
public String generateRefreshToken(Member member) {
4848
String refreshToken = UUID.randomUUID().toString();
4949

50-
// Redis에 리프레시 토큰 저장
50+
// Redis에 리프레시 토큰 저장 (만료시간: 7일)
5151
String redisKey = REFRESH_TOKEN_PREFIX + member.getLoginId();
5252
redisTemplate.opsForValue().set(redisKey, refreshToken, Duration.ofSeconds(REFRESH_TOKEN_EXPIRE_TIME));
5353

@@ -114,6 +114,12 @@ public void deleteRefreshToken(String loginId) {
114114
redisTemplate.delete(redisKey);
115115
}
116116

117+
/**
118+
* 리프레시 토큰으로 사용자명을 찾습니다.
119+
* Redis에서 모든 리프레시 토큰 키를 순회하며 일치하는 토큰을 찾습니다.
120+
* @param refreshToken 찾을 리프레시 토큰
121+
* @return 사용자명 또는 null
122+
*/
117123
public String findUsernameByRefreshToken(String refreshToken) {
118124
String pattern = REFRESH_TOKEN_PREFIX + "*";
119125
var keys = redisTemplate.keys(pattern);

backend/src/main/java/com/ai/lawyer/global/security/SecurityConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public PasswordEncoder passwordEncoder() {
6060
@Bean
6161
public CorsConfigurationSource corsConfigurationSource() {
6262
CorsConfiguration configuration = new CorsConfiguration();
63-
configuration.setAllowedOrigins(List.of("http://localhost:3000")); // 프론트엔드 주소
63+
configuration.setAllowedOrigins(List.of("http://localhost:3000"));
6464
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
6565
configuration.setAllowedHeaders(List.of("*"));
6666
configuration.setAllowCredentials(true);

0 commit comments

Comments
 (0)