Skip to content

Commit 729c5df

Browse files
authored
[FEAT] 인증/인가 공통 응답 및 예외처리 적용 (#48)
* ✨feat: Spring Security, jwt, h2 의존성 추가 * ✨feat: Spring Security + JWT 작업 - JWT 커스텀 필터 추가 - JWT 유틸리티 작성 * ✨feat: USER 도메인 개발 - 테스트 컨트롤러 추가 - Entity 개발 * ✨feat: 인증 작업을 위한 비즈니스 로직 개발 * ✨feat: baseEntity 추가 * ✨feat: 스프링부트 실행 시점 설정 정보 로깅 추가 * ✨feat: http 테스트코드 작성 * ✨feat: yml 변경점 적용 * 🩹fix: jwt 시크릿 노출 제외 및 만료시간 노출 * 🩹fix: UserInfoResponse DTO 필드명 불일치 수정 * ✨feat: refresh관련 로직 수정 - API 인가 refresh 분기처리 추가 - accesstoken 재발급 엔드포인트 개발 - Refresh Token JSON -> Cookie 변경 (secure, httponly 설정) - .http 테스트 코드 작성 * ✨feat: 로그아웃 엔드포인트 개발 * ✨feat: 공통 응답 포맷 적용 * ✨feat: Auth 공통 응답 적용 * ✨feat: 인증 로직 예외 처리 적용 - Auth 도메인 전역 예외 처리 및 커스텀 예외 적용 - Security Sevlet 예외 처리 적용 - .http 테스트 코드 추가 * 🩹fix: 공통 응답 status 필드 추가 적용 * ✨feat: UserDetails 커스텀 - security context에서 userId 가져올 수 있도록 커스텀 클래스 추가 - 테스트 로그 추가 * 🐛fix: h2 관련 실행시점 오류 조치 * 🩹fix: 코드리뷰 피드백 반영
1 parent cc9a74c commit 729c5df

21 files changed

+322
-74
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ dependencies {
4242
implementation 'io.jsonwebtoken:jjwt:0.12.6'
4343

4444
// H2 Database
45-
runtimeOnly 'com.h2database:h2'
45+
developmentOnly 'com.h2database:h2'
4646

4747
}
4848

src/main/java/team/wego/wegobackend/auth/application/AuthService.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package team.wego.wegobackend.auth.application;
22

3-
import jakarta.servlet.http.Cookie;
43
import lombok.RequiredArgsConstructor;
54
import lombok.extern.slf4j.Slf4j;
65
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@@ -11,7 +10,13 @@
1110
import team.wego.wegobackend.auth.application.dto.response.LoginResponse;
1211
import team.wego.wegobackend.auth.application.dto.response.RefreshResponse;
1312
import team.wego.wegobackend.auth.application.dto.response.SignupResponse;
13+
import team.wego.wegobackend.auth.exception.DeletedUserException;
14+
import team.wego.wegobackend.auth.exception.InvalidPasswordException;
15+
import team.wego.wegobackend.auth.exception.UserAlreadyExistsException;
16+
import team.wego.wegobackend.auth.exception.UserNotFoundException;
17+
import team.wego.wegobackend.common.exception.AppErrorCode;
1418
import team.wego.wegobackend.common.security.Role;
19+
import team.wego.wegobackend.common.security.exception.ExpiredTokenException;
1520
import team.wego.wegobackend.common.security.jwt.JwtTokenProvider;
1621
import team.wego.wegobackend.user.domain.User;
1722
import team.wego.wegobackend.user.repository.UserRepository;
@@ -37,7 +42,7 @@ public class AuthService {
3742
public SignupResponse signup(SignupRequest request) {
3843
// 이메일 중복 체크
3944
if (userRepository.existsByEmail(request.getEmail())) {
40-
throw new IllegalArgumentException("이미 존재하는 회원");
45+
throw new UserAlreadyExistsException();
4146
}
4247

4348
User user = User.builder().email(request.getEmail())
@@ -56,14 +61,14 @@ public SignupResponse signup(SignupRequest request) {
5661
public LoginResponse login(LoginRequest request) {
5762

5863
User user = userRepository.findByEmail(request.getEmail())
59-
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자"));
64+
.orElseThrow(UserNotFoundException::new);
6065

6166
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
62-
throw new IllegalArgumentException("잘못된 비밀번호");
67+
throw new InvalidPasswordException();
6368
}
6469

6570
if (user.getDeleted()) {
66-
throw new IllegalArgumentException("탈퇴한 계정");
71+
throw new DeletedUserException();
6772
}
6873

6974
String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(),
@@ -82,16 +87,16 @@ public LoginResponse login(LoginRequest request) {
8287
public RefreshResponse refresh(String refreshToken) {
8388

8489
if (!jwtTokenProvider.validateRefreshToken(refreshToken)) {
85-
throw new IllegalArgumentException("유효하지 않은 Refresh Token");
90+
throw new ExpiredTokenException();
8691
}
8792

8893
String email = jwtTokenProvider.getEmailFromToken(refreshToken);
8994

9095
User user = userRepository.findByEmail(email)
91-
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자"));
96+
.orElseThrow(UserNotFoundException::new);
9297

9398
if (user.getDeleted()) {
94-
throw new IllegalArgumentException("탈퇴한 계정");
99+
throw new DeletedUserException();
95100
}
96101

97102
String newAccessToken = jwtTokenProvider.createAccessToken(
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package team.wego.wegobackend.auth.exception;
2+
3+
import team.wego.wegobackend.common.exception.AppErrorCode;
4+
import team.wego.wegobackend.common.exception.AppException;
5+
6+
public class DeletedUserException extends AppException {
7+
8+
public DeletedUserException() {
9+
super(AppErrorCode.DELETED_USER);
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package team.wego.wegobackend.auth.exception;
2+
3+
import team.wego.wegobackend.common.exception.AppErrorCode;
4+
import team.wego.wegobackend.common.exception.AppException;
5+
6+
public class InvalidPasswordException extends AppException {
7+
8+
public InvalidPasswordException() {
9+
super(AppErrorCode.INVALID_PASSWORD_VALUE);
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package team.wego.wegobackend.auth.exception;
2+
3+
import team.wego.wegobackend.common.exception.AppErrorCode;
4+
import team.wego.wegobackend.common.exception.AppException;
5+
6+
public class UserAlreadyExistsException extends AppException {
7+
8+
public UserAlreadyExistsException() {
9+
super(AppErrorCode.ALREADY_EXISTS_USER);
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package team.wego.wegobackend.auth.exception;
2+
3+
import team.wego.wegobackend.common.exception.AppErrorCode;
4+
import team.wego.wegobackend.common.exception.AppException;
5+
6+
public class UserNotFoundException extends AppException {
7+
8+
public UserNotFoundException() {
9+
super(AppErrorCode.USER_NOT_FOUND);
10+
}
11+
}

src/main/java/team/wego/wegobackend/auth/presentation/AuthController.java

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import jakarta.validation.Valid;
66
import lombok.RequiredArgsConstructor;
77
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.http.HttpStatus;
9+
import org.springframework.http.HttpStatusCode;
810
import org.springframework.http.ResponseEntity;
911
import org.springframework.web.bind.annotation.CookieValue;
1012
import org.springframework.web.bind.annotation.PostMapping;
@@ -17,6 +19,7 @@
1719
import team.wego.wegobackend.auth.application.dto.response.LoginResponse;
1820
import team.wego.wegobackend.auth.application.dto.response.RefreshResponse;
1921
import team.wego.wegobackend.auth.application.dto.response.SignupResponse;
22+
import team.wego.wegobackend.common.response.ApiResponse;
2023
import team.wego.wegobackend.common.security.jwt.JwtTokenProvider;
2124

2225
@Slf4j
@@ -33,53 +36,78 @@ public class AuthController {
3336
* 회원가입
3437
*/
3538
@PostMapping("/signup")
36-
public ResponseEntity<SignupResponse> signup(@Valid @RequestBody SignupRequest request) {
39+
public ResponseEntity<ApiResponse<SignupResponse>> signup(
40+
@Valid @RequestBody SignupRequest request) {
3741
SignupResponse response = authService.signup(request);
38-
return ResponseEntity.ok(response);
42+
return ResponseEntity
43+
.status(HttpStatus.CREATED)
44+
.body(ApiResponse.success(
45+
201,
46+
true,
47+
response));
3948
}
4049

4150
/**
4251
* 로그인
4352
*/
4453
@PostMapping("/login")
45-
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request,
54+
public ResponseEntity<ApiResponse<LoginResponse>> login(
55+
@Valid @RequestBody LoginRequest request,
4656
HttpServletResponse response) {
4757
LoginResponse loginResponse = authService.login(request);
4858

4959
response.addCookie(createRefreshTokenCookie(loginResponse.getRefreshToken()));
5060

51-
return ResponseEntity.ok(loginResponse);
61+
return ResponseEntity
62+
.status(HttpStatus.OK)
63+
.body(ApiResponse.success(
64+
200,
65+
true,
66+
loginResponse
67+
));
5268
}
5369

5470
/**
5571
* 로그아웃
56-
* */
72+
*/
5773
@PostMapping("/logout")
58-
public ResponseEntity<Void> logout(HttpServletResponse response) {
74+
public ResponseEntity<ApiResponse<Void>> logout(HttpServletResponse response) {
5975
// Refresh Token 쿠키만 삭제
6076
Cookie deleteCookie = new Cookie("refreshToken", null);
6177
deleteCookie.setPath("/");
6278
deleteCookie.setMaxAge(0);
6379
deleteCookie.setHttpOnly(true);
80+
deleteCookie.setSecure(true);
81+
deleteCookie.setAttribute("SameSite", "Strict");
6482
response.addCookie(deleteCookie);
6583

66-
return ResponseEntity.ok().build();
84+
return ResponseEntity
85+
.status(HttpStatus.NO_CONTENT)
86+
.body(ApiResponse.success(
87+
204,
88+
true
89+
));
6790
}
6891

6992
/**
7093
* Access Token 재발급
7194
*/
7295
@PostMapping("/refresh")
73-
public ResponseEntity<RefreshResponse> refresh(
74-
@CookieValue(name = "refreshToken", required = false) String refreshToken
75-
) {
96+
public ResponseEntity<ApiResponse<RefreshResponse>> refresh(
97+
@CookieValue(name = "refreshToken", required = false) String refreshToken) {
7698
if (refreshToken == null) {
7799
throw new IllegalArgumentException("Refresh 토큰이 없습니다.");
78100
}
79101

80102
RefreshResponse response = authService.refresh(refreshToken);
81103

82-
return ResponseEntity.ok(response);
104+
return ResponseEntity
105+
.status(HttpStatus.CREATED)
106+
.body(ApiResponse.success(
107+
201,
108+
true,
109+
response
110+
));
83111
}
84112

85113
/**

src/main/java/team/wego/wegobackend/common/exception/AppErrorCode.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,16 @@ public enum AppErrorCode implements ErrorCode {
2020

2121
ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "공통: 요청한 리소스를 찾을 수 없습니다."),
2222
RESP_BODY_WRITE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "공통: 응답 본문을 생성/쓰기 중 오류가 발생했습니다."),
23-
MEDIA_TYPE_NOT_ACCEPTABLE(HttpStatus.NOT_ACCEPTABLE, "공통: 요청한 응답 형식을 제공할 수 없습니다.");
23+
MEDIA_TYPE_NOT_ACCEPTABLE(HttpStatus.NOT_ACCEPTABLE, "공통: 요청한 응답 형식을 제공할 수 없습니다."),
24+
25+
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원 : 가입된 회원이 아닙니다."),
26+
ALREADY_EXISTS_USER(HttpStatus.BAD_REQUEST, "회원 : 이미 가입한 회원입니다."),
27+
INVALID_PASSWORD_VALUE(HttpStatus.BAD_REQUEST, "회원 : 비밀번호가 일치하지 않습니다."),
28+
DELETED_USER(HttpStatus.BAD_REQUEST, "회원 : 탈퇴한 회원입니다."),
29+
30+
EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "인증 : 만료된 토큰입니다."),
31+
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "인증 : 유효하지 않은 토큰입니다.")
32+
;
2433

2534
private final HttpStatus httpStatus;
2635
private final String messageTemplate;

src/main/java/team/wego/wegobackend/common/exception/GlobalExceptionHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
@RestControllerAdvice
2323
public class GlobalExceptionHandler {
2424

25-
private static final String PROBLEM_BASE_URI = "http://13.125.199.79:8080/problem/";
25+
private static final String PROBLEM_BASE_URI = "about:blank";
2626

2727
@ExceptionHandler(AppException.class)
2828
public ResponseEntity<ErrorResponse> handleApp(AppException ex,
@@ -261,6 +261,6 @@ private static String rootCauseMessage(Throwable ex) {
261261
}
262262

263263
private static String toProblemType(String title) {
264-
return PROBLEM_BASE_URI + title.toLowerCase().replace('_', '-');
264+
return PROBLEM_BASE_URI;
265265
}
266266
}

src/main/java/team/wego/wegobackend/common/response/ApiResponse.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,31 @@
44

55
@JsonInclude(JsonInclude.Include.NON_NULL)
66
public record ApiResponse<T>(
7+
int status,
78
boolean success,
89
T data
910
) {
1011

11-
public static <T> ApiResponse<T> success(T data) {
12-
return new ApiResponse<>(true, data);
12+
public static <T> ApiResponse<T> success(int status, T data) {
13+
return new ApiResponse<>(status, true, data);
1314
}
1415

15-
public static <T> ApiResponse<T> success(boolean isSuccess, T data) {
16-
return new ApiResponse<>(isSuccess, data);
16+
public static <T> ApiResponse<T> success(int status, boolean isSuccess, T data) {
17+
return new ApiResponse<>(status, isSuccess, data);
1718
}
1819

19-
public static <T> ApiResponse<T> success(String message) {
20-
return new ApiResponse<>(true, null);
20+
/**
21+
* No Content (ex : 204)
22+
* */
23+
public static <T> ApiResponse<T> success(int status, boolean isSuccess) {
24+
return new ApiResponse<>(status, true, null);
2125
}
2226

23-
public static <T> ApiResponse<T> error(String message) {
24-
return new ApiResponse<>(false, null);
27+
public static <T> ApiResponse<T> error(int status, String message) {
28+
return new ApiResponse<>(status, false, null);
2529
}
2630

27-
public static <T> ApiResponse<T> error(boolean isSuccess, T data) {
28-
return new ApiResponse<>(isSuccess, data);
31+
public static <T> ApiResponse<T> error(int status, boolean isSuccess, T data) {
32+
return new ApiResponse<>(status, isSuccess, data);
2933
}
3034
}

0 commit comments

Comments
 (0)