Skip to content

Commit b254d93

Browse files
committed
fix[member]: JWT 토큰 내용 변경과 기능 별 인가 오류 해결 및 에러메시지 반환 코드 추가
1 parent 048693c commit b254d93

File tree

7 files changed

+144
-75
lines changed

7 files changed

+144
-75
lines changed

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

Lines changed: 28 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import com.ai.lawyer.domain.member.dto.MemberLoginRequest;
44
import com.ai.lawyer.domain.member.dto.MemberResponse;
55
import com.ai.lawyer.domain.member.dto.MemberSignupRequest;
6-
import com.ai.lawyer.domain.member.entity.Member;
76
import com.ai.lawyer.domain.member.service.MemberService;
87
import io.swagger.v3.oas.annotations.Operation;
98
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -37,14 +36,9 @@ public class MemberController {
3736
public ResponseEntity<MemberResponse> signup(@Valid @RequestBody MemberSignupRequest request) {
3837
log.info("회원가입 요청: email={}, name={}", request.getLoginId(), request.getName());
3938

40-
try {
41-
MemberResponse response = memberService.signup(request);
42-
log.info("회원가입 성공: memberId={}", response.getMemberId());
43-
return ResponseEntity.status(HttpStatus.CREATED).body(response);
44-
} catch (IllegalArgumentException e) {
45-
log.warn("회원가입 실패: {}", e.getMessage());
46-
return ResponseEntity.badRequest().build();
47-
}
39+
MemberResponse response = memberService.signup(request);
40+
log.info("회원가입 성공: memberId={}", response.getMemberId());
41+
return ResponseEntity.status(HttpStatus.CREATED).body(response);
4842
}
4943

5044
@PostMapping("/login")
@@ -57,14 +51,9 @@ public ResponseEntity<MemberResponse> login(@Valid @RequestBody MemberLoginReque
5751
HttpServletResponse response) {
5852
log.info("로그인 요청: email={}", request.getLoginId());
5953

60-
try {
61-
MemberResponse memberResponse = memberService.login(request, response);
62-
log.info("로그인 성공: memberId={}", memberResponse.getMemberId());
63-
return ResponseEntity.ok(memberResponse);
64-
} catch (IllegalArgumentException e) {
65-
log.warn("로그인 실패: {}", e.getMessage());
66-
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
67-
}
54+
MemberResponse memberResponse = memberService.login(request, response);
55+
log.info("로그인 성공: memberId={}", memberResponse.getMemberId());
56+
return ResponseEntity.ok(memberResponse);
6857
}
6958

7059
@PostMapping("/logout")
@@ -102,18 +91,12 @@ public ResponseEntity<MemberResponse> refreshToken(HttpServletRequest request,
10291
String refreshToken = extractRefreshTokenFromCookies(request);
10392

10493
if (refreshToken == null) {
105-
log.warn("리프레시 토큰이 없음");
106-
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
94+
throw new com.ai.lawyer.domain.member.exception.MemberAuthenticationException("리프레시 토큰이 없습니다.");
10795
}
10896

109-
try {
110-
MemberResponse memberResponse = memberService.refreshToken(refreshToken, response);
111-
log.info("토큰 재발급 성공: memberId={}", memberResponse.getMemberId());
112-
return ResponseEntity.ok(memberResponse);
113-
} catch (IllegalArgumentException e) {
114-
log.warn("토큰 재발급 실패: {}", e.getMessage());
115-
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
116-
}
97+
MemberResponse memberResponse = memberService.refreshToken(refreshToken, response);
98+
log.info("토큰 재발급 성공: memberId={}", memberResponse.getMemberId());
99+
return ResponseEntity.ok(memberResponse);
117100
}
118101

119102
@DeleteMapping("/withdraw")
@@ -124,25 +107,18 @@ public ResponseEntity<MemberResponse> refreshToken(HttpServletRequest request,
124107
@ApiResponse(responseCode = "404", description = "존재하지 않는 회원")
125108
})
126109
public ResponseEntity<Void> withdraw(Authentication authentication, HttpServletResponse response) {
127-
if (authentication == null || authentication.getName() == null) {
128-
log.warn("인증되지 않은 회원탈퇴 요청");
129-
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
110+
if (authentication == null || authentication.getPrincipal() == null) {
111+
throw new com.ai.lawyer.domain.member.exception.MemberAuthenticationException("인증이 필요합니다.");
130112
}
131113

132-
String loginId = authentication.getName();
133-
log.info("회원탈퇴 요청: email={}", loginId);
134-
135-
try {
136-
// loginId로 Member를 조회하여 실제 memberId 사용
137-
Member member = memberService.findByLoginId(loginId);
138-
memberService.withdraw(member.getMemberId());
139-
memberService.logout(loginId, response); // 탈퇴 후 로그아웃 처리
140-
log.info("회원탈퇴 성공: email={}, memberId={}", loginId, member.getMemberId());
141-
return ResponseEntity.ok().build();
142-
} catch (IllegalArgumentException e) {
143-
log.warn("회원탈퇴 실패: {}", e.getMessage());
144-
return ResponseEntity.notFound().build();
145-
}
114+
Long memberId = (Long) authentication.getPrincipal();
115+
String loginId = (String) authentication.getDetails();
116+
log.info("회원탈퇴 요청: memberId={}, email={}", memberId, loginId);
117+
118+
memberService.withdraw(memberId);
119+
memberService.logout(loginId, response); // 탈퇴 후 로그아웃 처리
120+
log.info("회원탈퇴 성공: memberId={}, email={}", memberId, loginId);
121+
return ResponseEntity.ok().build();
146122
}
147123

148124
@GetMapping("/me")
@@ -152,24 +128,16 @@ public ResponseEntity<Void> withdraw(Authentication authentication, HttpServletR
152128
@ApiResponse(responseCode = "401", description = "인증되지 않은 사용자")
153129
})
154130
public ResponseEntity<MemberResponse> getMyInfo(Authentication authentication) {
155-
if (authentication == null || authentication.getName() == null) {
156-
log.warn("인증되지 않은 정보 조회 요청");
157-
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
131+
if (authentication == null || authentication.getPrincipal() == null) {
132+
throw new com.ai.lawyer.domain.member.exception.MemberAuthenticationException("인증이 필요합니다.");
158133
}
159134

160-
String loginId = authentication.getName();
161-
log.info("내 정보 조회 요청: email={}", loginId);
162-
163-
try {
164-
// loginId로 Member를 조회하여 실제 memberId 사용
165-
Member member = memberService.findByLoginId(loginId);
166-
MemberResponse response = memberService.getMemberById(member.getMemberId());
167-
log.info("내 정보 조회 성공: memberId={}", response.getMemberId());
168-
return ResponseEntity.ok(response);
169-
} catch (IllegalArgumentException e) {
170-
log.warn("내 정보 조회 실패: {}", e.getMessage());
171-
return ResponseEntity.notFound().build();
172-
}
135+
Long memberId = (Long) authentication.getPrincipal();
136+
log.info("내 정보 조회 요청: memberId={}", memberId);
137+
138+
MemberResponse response = memberService.getMemberById(memberId);
139+
log.info("내 정보 조회 성공: memberId={}", response.getMemberId());
140+
return ResponseEntity.ok(response);
173141
}
174142

175143
private String extractRefreshTokenFromCookies(HttpServletRequest request) {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.ai.lawyer.domain.member.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
import java.time.LocalDateTime;
7+
8+
@Getter
9+
@AllArgsConstructor
10+
public class MemberErrorResponse {
11+
private final String message;
12+
private final int status;
13+
private final String error;
14+
private final LocalDateTime timestamp;
15+
16+
public static MemberErrorResponse of(String message, int status, String error) {
17+
return new MemberErrorResponse(message, status, error, LocalDateTime.now());
18+
}
19+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.ai.lawyer.domain.member.exception;
2+
3+
public class MemberAuthenticationException extends RuntimeException {
4+
public MemberAuthenticationException(String message) {
5+
super(message);
6+
}
7+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.ai.lawyer.domain.member.exception;
2+
3+
import com.ai.lawyer.domain.member.dto.MemberErrorResponse;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.http.HttpStatus;
6+
import org.springframework.http.ResponseEntity;
7+
import org.springframework.web.bind.MethodArgumentNotValidException;
8+
import org.springframework.web.bind.annotation.ExceptionHandler;
9+
import org.springframework.web.bind.annotation.RestControllerAdvice;
10+
11+
@RestControllerAdvice(basePackages = "com.ai.lawyer.domain.member")
12+
@Slf4j
13+
public class MemberExceptionHandler {
14+
15+
@ExceptionHandler(IllegalArgumentException.class)
16+
public ResponseEntity<MemberErrorResponse> handleMemberIllegalArgumentException(IllegalArgumentException e) {
17+
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);
24+
}
25+
26+
@ExceptionHandler(MemberAuthenticationException.class)
27+
public ResponseEntity<MemberErrorResponse> handleMemberAuthenticationException(MemberAuthenticationException e) {
28+
log.warn("Member 도메인 AuthenticationException: {}", e.getMessage());
29+
MemberErrorResponse errorResponse = MemberErrorResponse.of(
30+
e.getMessage(),
31+
HttpStatus.UNAUTHORIZED.value(),
32+
"인증 실패"
33+
);
34+
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse);
35+
}
36+
37+
@ExceptionHandler(MethodArgumentNotValidException.class)
38+
public ResponseEntity<MemberErrorResponse> handleMemberValidationException(MethodArgumentNotValidException e) {
39+
String message = e.getBindingResult().getAllErrors().getFirst().getDefaultMessage();
40+
log.warn("Member 도메인 유효성 검증 실패: {}", message);
41+
MemberErrorResponse errorResponse = MemberErrorResponse.of(
42+
message,
43+
HttpStatus.BAD_REQUEST.value(),
44+
"유효성 검증 실패"
45+
);
46+
return ResponseEntity.badRequest().body(errorResponse);
47+
}
48+
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ public MemberResponse login(MemberLoginRequest request, HttpServletResponse resp
5757
}
5858

5959
public void logout(String loginId, HttpServletResponse response) {
60-
// Redis에서 리프레시 토큰 삭제
61-
tokenProvider.deleteRefreshToken(loginId);
60+
// loginId가 있는 경우에만 Redis에서 리프레시 토큰 삭제
61+
if (loginId != null && !loginId.trim().isEmpty()) {
62+
tokenProvider.deleteRefreshToken(loginId);
63+
}
6264

63-
// 쿠키 삭제
65+
// 쿠키는 항상 클리어 (인증 정보가 없어도 클라이언트의 쿠키는 삭제해야 함)
6466
cookieUtil.clearTokenCookies(response);
6567
}
6668

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,22 @@ protected void doFilterInternal(@Nullable HttpServletRequest request, @Nullable
4545

4646
private void setAuthentication(String token) {
4747
try {
48-
// TODO: 실제 JWT 구현 시 토큰에서 사용자 정보 추출
49-
// 현재는 임시로 토큰에서 사용자명 추출
50-
String username = tokenProvider.getUsernameFromToken(token);
48+
// 토큰에서 사용자 정보 추출
49+
Long memberId = tokenProvider.getMemberIdFromToken(token);
50+
String role = tokenProvider.getRoleFromToken(token);
5151

52-
// 간단한 권한 설정 (실제로는 토큰에서 권한 정보도 추출)
53-
List<SimpleGrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("ROLE_USER"));
52+
if (memberId == null) {
53+
log.warn("토큰에서 memberId를 추출할 수 없습니다.");
54+
return;
55+
}
56+
57+
// 권한 설정 (토큰에서 추출한 role 사용)
58+
String authority = "ROLE_" + (role != null ? role : "USER");
59+
List<SimpleGrantedAuthority> authorities = List.of(new SimpleGrantedAuthority(authority));
5460

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

5865
SecurityContextHolder.getContext().setAuthentication(authentication);
5966
} catch (Exception e) {
@@ -64,8 +71,10 @@ private void setAuthentication(String token) {
6471
@Override
6572
protected boolean shouldNotFilter(HttpServletRequest request) {
6673
String path = request.getRequestURI();
67-
// 인증이 필요없는 경로들
68-
return path.startsWith("/api/auth/") ||
74+
// 인증이 필요없는 경로들 (구체적으로 명시)
75+
return path.equals("/api/auth/signup") ||
76+
path.equals("/api/auth/login") ||
77+
path.equals("/api/auth/refresh") ||
6978
path.startsWith("/api/public/") ||
7079
path.startsWith("/swagger-") ||
7180
path.startsWith("/v3/api-docs");

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.springframework.stereotype.Component;
1111

1212
import javax.crypto.SecretKey;
13+
import java.nio.charset.StandardCharsets;
1314
import java.time.Duration;
1415
import java.util.Date;
1516
import java.util.UUID;
@@ -26,7 +27,7 @@ public class TokenProvider {
2627
private static final long REFRESH_TOKEN_EXPIRE_TIME = 7 * 24 * 60 * 60; // 7일
2728

2829
private SecretKey getSigningKey() {
29-
return Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes());
30+
return Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8));
3031
}
3132

3233
public String generateAccessToken(Member member) {
@@ -38,6 +39,7 @@ public String generateAccessToken(Member member) {
3839
.setIssuedAt(now)
3940
.setExpiration(expiry)
4041
.claim("memberId", member.getMemberId())
42+
.claim("role", member.getRole().name())
4143
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
4244
.compact();
4345
}
@@ -73,16 +75,30 @@ public boolean validateToken(String token) {
7375
return false;
7476
}
7577

76-
public String getUsernameFromToken(String token) {
78+
public Long getMemberIdFromToken(String token) {
7779
try {
7880
Claims claims = Jwts.parserBuilder()
7981
.setSigningKey(getSigningKey())
8082
.build()
8183
.parseClaimsJws(token)
8284
.getBody();
83-
return claims.getSubject();
85+
return claims.get("memberId", Long.class);
8486
} catch (Exception e) {
85-
log.warn("토큰에서 사용자 정보 추출 실패: {}", e.getMessage());
87+
log.warn("토큰에서 회원 ID 추출 실패: {}", e.getMessage());
88+
return null;
89+
}
90+
}
91+
92+
public String getRoleFromToken(String token) {
93+
try {
94+
Claims claims = Jwts.parserBuilder()
95+
.setSigningKey(getSigningKey())
96+
.build()
97+
.parseClaimsJws(token)
98+
.getBody();
99+
return claims.get("role", String.class);
100+
} catch (Exception e) {
101+
log.warn("토큰에서 역할 정보 추출 실패: {}", e.getMessage());
86102
return null;
87103
}
88104
}

0 commit comments

Comments
 (0)