Skip to content

Commit b2d5eb4

Browse files
authored
Refactor: 인증/인가 인프라 및 관련 API 개선 (#97) (#98)
* Ref: 에러 처리 개선 및 관련 코드 수정 * Ref: Auth 관련 테스트 보완 * Docs: Auth API Swagger 문서 작성 * Ref: CookieUtil 및 관련 코드 개선 * Test: User API 테스트 보완 및 관련 코드 수정 * Doces: User API Swagger 문서 보완 * Test: 테스트 수정
1 parent f05534b commit b2d5eb4

File tree

16 files changed

+434
-267
lines changed

16 files changed

+434
-267
lines changed

src/main/java/com/back/domain/user/controller/AuthController.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import lombok.RequiredArgsConstructor;
1313
import org.springframework.http.HttpStatus;
1414
import org.springframework.http.ResponseEntity;
15+
import org.springframework.security.access.prepost.PreAuthorize;
1516
import org.springframework.web.bind.annotation.PostMapping;
1617
import org.springframework.web.bind.annotation.RequestBody;
1718
import org.springframework.web.bind.annotation.RequestMapping;

src/main/java/com/back/domain/user/controller/AuthControllerDocs.java

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -260,17 +260,27 @@ ResponseEntity<RsData<LoginResponse>> login(
260260
),
261261
@ApiResponse(
262262
responseCode = "401",
263-
description = "이미 만료되었거나 유효하지 않은 Refresh Token",
263+
description = "만료 또는 유효하지 않은 Refresh Token",
264264
content = @Content(
265265
mediaType = "application/json",
266-
examples = @ExampleObject(value = """
267-
{
268-
"success": false,
269-
"code": "AUTH_401",
270-
"message": "이미 만료되었거나 유효하지 않은 토큰입니다.",
271-
"data": null
272-
}
273-
""")
266+
examples = {
267+
@ExampleObject(name = "만료된 Refresh Token", value = """
268+
{
269+
"success": false,
270+
"code": "AUTH_005",
271+
"message": "만료된 리프레시 토큰입니다.",
272+
"data": null
273+
}
274+
"""),
275+
@ExampleObject(name = "유효하지 않은 Refresh Token", value = """
276+
{
277+
"success": false,
278+
"code": "AUTH_003",
279+
"message": "유효하지 않은 리프레시 토큰입니다.",
280+
"data": null
281+
}
282+
""")
283+
}
274284
)
275285
),
276286
@ApiResponse(
@@ -356,16 +366,16 @@ ResponseEntity<RsData<Void>> logout(
356366
@ExampleObject(name = "Refresh Token 만료", value = """
357367
{
358368
"success": false,
359-
"code": "AUTH_401",
369+
"code": "AUTH_005",
360370
"message": "만료된 리프레시 토큰입니다.",
361371
"data": null
362372
}
363373
"""),
364374
@ExampleObject(name = "Refresh Token 위조/무효", value = """
365375
{
366376
"success": false,
367-
"code": "AUTH_401",
368-
"message": "유효하지 않은 Refresh Token입니다.",
377+
"code": "AUTH_003",
378+
"message": "유효하지 않은 리프레시 토큰입니다.",
369379
"data": null
370380
}
371381
""")

src/main/java/com/back/domain/user/controller/UserControllerDocs.java

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,35 @@ public interface UserControllerDocs {
8787
),
8888
@ApiResponse(
8989
responseCode = "401",
90-
description = "인증 실패 (토큰 없음/만료/잘못됨)",
90+
description = "인증 실패 (Access Token 문제)",
9191
content = @Content(
9292
mediaType = "application/json",
93-
examples = @ExampleObject(value = """
94-
{
95-
"success": false,
96-
"code": "AUTH_401",
97-
"message": "인증이 필요합니다.",
98-
"data": null
99-
}
100-
""")
93+
examples = {
94+
@ExampleObject(name = "토큰 없음", value = """
95+
{
96+
"success": false,
97+
"code": "AUTH_001",
98+
"message": "인증이 필요합니다.",
99+
"data": null
100+
}
101+
"""),
102+
@ExampleObject(name = "유효하지 않은 토큰", value = """
103+
{
104+
"success": false,
105+
"code": "AUTH_002",
106+
"message": "유효하지 않은 액세스 토큰입니다.",
107+
"data": null
108+
}
109+
"""),
110+
@ExampleObject(name = "만료된 토큰", value = """
111+
{
112+
"success": false,
113+
"code": "AUTH_004",
114+
"message": "만료된 액세스 토큰입니다.",
115+
"data": null
116+
}
117+
""")
118+
}
101119
)
102120
),
103121
@ApiResponse(
@@ -219,17 +237,35 @@ ResponseEntity<RsData<UserDetailResponse>> getMyInfo(
219237
),
220238
@ApiResponse(
221239
responseCode = "401",
222-
description = "인증 실패 (토큰 없음/만료/잘못됨)",
240+
description = "인증 실패 (토큰 없음/잘못됨/만료)",
223241
content = @Content(
224242
mediaType = "application/json",
225-
examples = @ExampleObject(value = """
226-
{
227-
"success": false,
228-
"code": "AUTH_401",
229-
"message": "인증이 필요합니다.",
230-
"data": null
231-
}
232-
""")
243+
examples = {
244+
@ExampleObject(name = "토큰 없음", value = """
245+
{
246+
"success": false,
247+
"code": "AUTH_001",
248+
"message": "인증이 필요합니다.",
249+
"data": null
250+
}
251+
"""),
252+
@ExampleObject(name = "잘못된 토큰", value = """
253+
{
254+
"success": false,
255+
"code": "AUTH_002",
256+
"message": "유효하지 않은 액세스 토큰입니다.",
257+
"data": null
258+
}
259+
"""),
260+
@ExampleObject(name = "만료된 토큰", value = """
261+
{
262+
"success": false,
263+
"code": "AUTH_004",
264+
"message": "만료된 액세스 토큰입니다.",
265+
"data": null
266+
}
267+
""")
268+
}
233269
)
234270
),
235271
@ApiResponse(

src/main/java/com/back/domain/user/service/AuthService.java

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,11 @@ public LoginResponse login(LoginRequest request, HttpServletResponse response) {
125125
"refreshToken",
126126
refreshToken,
127127
(int) jwtTokenProvider.getRefreshTokenExpirationInSeconds(),
128-
"/api/auth"
128+
"/",
129+
true
129130
);
130131

132+
131133
// LoginResponse 반환
132134
return new LoginResponse(
133135
accessToken,
@@ -150,15 +152,18 @@ public void logout(HttpServletRequest request, HttpServletResponse response) {
150152
}
151153

152154
// Refresh Token 검증
153-
if (!jwtTokenProvider.validateToken(refreshToken)) {
154-
throw new CustomException(ErrorCode.INVALID_TOKEN);
155-
}
155+
jwtTokenProvider.validateRefreshToken(refreshToken);
156156

157157
// DB에서 Refresh Token 삭제
158158
userTokenRepository.deleteByRefreshToken(refreshToken);
159159

160160
// 쿠키 삭제
161-
CookieUtil.clearCookie(response, "refreshToken", "/api/auth");
161+
CookieUtil.clearCookie(
162+
response,
163+
"refreshToken",
164+
"/",
165+
true
166+
);
162167
}
163168

164169
/**
@@ -178,13 +183,11 @@ public String refreshToken(HttpServletRequest request, HttpServletResponse respo
178183
}
179184

180185
// Refresh Token 검증
181-
if (!jwtTokenProvider.validateToken(refreshToken)) {
182-
throw new CustomException(ErrorCode.INVALID_TOKEN);
183-
}
186+
jwtTokenProvider.validateRefreshToken(refreshToken);
184187

185188
// DB에서 Refresh Token 조회
186189
UserToken userToken = userTokenRepository.findByRefreshToken(refreshToken)
187-
.orElseThrow(() -> new CustomException(ErrorCode.INVALID_TOKEN));
190+
.orElseThrow(() -> new CustomException(ErrorCode.INVALID_REFRESH_TOKEN));
188191

189192
// 사용자 정보 조회
190193
User user = userToken.getUser();

src/main/java/com/back/global/config/WebSocketConfig.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,7 @@ private void authenticateUser(StompHeaderAccessor accessor) {
105105
String token = authHeader.substring(7); // "Bearer " 제거
106106

107107
// JWT 토큰 검증
108-
if (!jwtTokenProvider.validateToken(token)) {
109-
throw new RuntimeException("유효하지 않은 인증 토큰입니다");
110-
}
108+
jwtTokenProvider.validateAccessToken(token);
111109

112110
// 토큰에서 사용자 정보 추출
113111
Authentication authentication = jwtTokenProvider.getAuthentication(token);

src/main/java/com/back/global/exception/ErrorCode.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,13 @@ public enum ErrorCode {
5959
NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON_404", "요청하신 리소스를 찾을 수 없습니다."),
6060

6161
// ======================== 인증/인가 에러 ========================
62-
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "AUTH_401", "인증이 필요합니다."),
63-
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_401", "유효하지 않은 토큰입니다."),
64-
EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_401", "만료된 액세스 토큰입니다."),
65-
EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_401", "만료된 리프레시 토큰입니다."),
66-
REFRESH_TOKEN_REUSE(HttpStatus.FORBIDDEN, "AUTH_403", "재사용된 리프레시 토큰입니다."),
67-
ACCESS_DENIED(HttpStatus.FORBIDDEN, "AUTH_403", "권한이 없습니다.");
62+
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "AUTH_001", "인증이 필요합니다."),
63+
INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_002", "유효하지 않은 액세스 토큰입니다."),
64+
INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_003", "유효하지 않은 리프레시 토큰입니다."),
65+
EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_004", "만료된 액세스 토큰입니다."),
66+
EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_005", "만료된 리프레시 토큰입니다."),
67+
REFRESH_TOKEN_REUSE(HttpStatus.FORBIDDEN, "AUTH_006", "재사용된 리프레시 토큰입니다."),
68+
ACCESS_DENIED(HttpStatus.FORBIDDEN, "AUTH_007", "권한이 없습니다.");
6869

6970

7071
private final HttpStatus status;

src/main/java/com/back/global/security/JwtAuthenticationEntryPoint.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.back.global.security;
22

33
import com.back.global.common.dto.RsData;
4+
import com.back.global.exception.CustomException;
45
import com.back.global.exception.ErrorCode;
56
import com.fasterxml.jackson.databind.ObjectMapper;
67
import jakarta.servlet.http.HttpServletRequest;
@@ -22,17 +23,24 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
2223
private final ObjectMapper objectMapper = new ObjectMapper();
2324

2425
@Override
25-
public void commence(
26-
HttpServletRequest request,
27-
HttpServletResponse response,
28-
AuthenticationException authException
29-
) throws IOException {
26+
public void commence(HttpServletRequest request,
27+
HttpServletResponse response,
28+
AuthenticationException authException) throws IOException {
3029

3130
response.setContentType("application/json;charset=UTF-8");
32-
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
3331

34-
RsData<Void> body = RsData.fail(ErrorCode.UNAUTHORIZED);
32+
// request attribute에서 ErrorCode 꺼내기
33+
ErrorCode errorCode = (ErrorCode) request.getAttribute("errorCode");
34+
35+
// ErrorCode가 없으면 UNAUTHORIZED로 기본 설정
36+
if (errorCode == null) {
37+
errorCode = ErrorCode.UNAUTHORIZED;
38+
}
39+
40+
response.setStatus(errorCode.getStatus().value());
41+
RsData<Void> body = RsData.fail(errorCode);
3542

3643
response.getWriter().write(objectMapper.writeValueAsString(body));
3744
}
45+
3846
}

src/main/java/com/back/global/security/JwtAuthenticationFilter.java

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package com.back.global.security;
22

3+
import com.back.global.exception.CustomException;
34
import jakarta.servlet.FilterChain;
45
import jakarta.servlet.ServletException;
56
import jakarta.servlet.http.HttpServletRequest;
67
import jakarta.servlet.http.HttpServletResponse;
78
import lombok.RequiredArgsConstructor;
9+
import org.springframework.security.authentication.InsufficientAuthenticationException;
810
import org.springframework.security.core.Authentication;
911
import org.springframework.security.core.context.SecurityContextHolder;
1012
import org.springframework.stereotype.Component;
@@ -21,30 +23,44 @@
2123
@RequiredArgsConstructor
2224
public class JwtAuthenticationFilter extends OncePerRequestFilter {
2325
private final JwtTokenProvider jwtTokenProvider;
26+
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
2427

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

30-
// Request Header에서 토큰 추출
31-
String token = resolveToken(request);
33+
try {
34+
// Request Header에서 토큰 추출
35+
String token = resolveToken(request);
3236

33-
// 토큰이 유효한 경우에만 Authentication 객체 생성 및 SecurityContext에 저장
34-
if (token != null && jwtTokenProvider.validateToken(token)) {
35-
Authentication authentication = jwtTokenProvider.getAuthentication(token);
36-
SecurityContextHolder.getContext().setAuthentication(authentication);
37+
// 토큰이 유효한 경우에만 Authentication 객체 생성 및 SecurityContext에 저장
38+
if (token != null) {
39+
jwtTokenProvider.validateAccessToken(token);
40+
Authentication authentication = jwtTokenProvider.getAuthentication(token);
41+
SecurityContextHolder.getContext().setAuthentication(authentication);
42+
}
43+
44+
// 다음 필터로 요청 전달
45+
filterChain.doFilter(request, response);
46+
} catch (CustomException ex) {
47+
// SecurityContext 초기화
48+
SecurityContextHolder.clearContext();
49+
50+
// request attribute에 ErrorCode 저장
51+
request.setAttribute("errorCode", ex.getErrorCode());
52+
53+
// 인증 실패 처리
54+
jwtAuthenticationEntryPoint.commence(
55+
request, response,
56+
new InsufficientAuthenticationException("Custom auth error")
57+
);
3758
}
3859

39-
// 다음 필터로 요청 전달
40-
filterChain.doFilter(request, response);
4160
}
4261

4362
/**
4463
* Request의 Authorization 헤더에서 JWT 토큰을 추출
45-
*
46-
* @param request HTTP 요청 객체
47-
* @return JWT 토큰 문자열 또는 null
4864
*/
4965
private String resolveToken(HttpServletRequest request) {
5066
String bearerToken = request.getHeader("Authorization");

0 commit comments

Comments
 (0)