@@ -129,19 +129,38 @@ public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest requ
129129
130130 LoginResponse response = loginService .login (request );
131131
132- ResponseCookie cookie = ResponseCookie .from ("refresh" , response .getRefreshToken ())
132+ // accessToken을 HttpOnly 쿠키로 설정
133+ ResponseCookie accessCookie = ResponseCookie .from ("access" , response .getAccessToken ())
133134 .httpOnly (true )
134135 .secure (true )
135136 .sameSite ("None" )
136137 .path ("/" )
137- .maxAge (60L * 60 * 24 * 14 )
138+ .maxAge (60L * 60 ) // 1 hour
138139 .build ();
139140
141+ // refreshToken을 HttpOnly 쿠키로 설정
142+ ResponseCookie refreshCookie = ResponseCookie .from ("refresh" , response .getRefreshToken ())
143+ .httpOnly (true )
144+ .secure (true )
145+ .sameSite ("None" )
146+ .path ("/" )
147+ .maxAge (60L * 60 * 24 * 14 ) // 2 weeks
148+ .build ();
149+
150+ // JSON 응답에서 토큰 제거, 유저 정보만 포함
151+ LoginResponse safeResponse = LoginResponse .builder ()
152+ .userId (response .getUserId ())
153+ .email (response .getEmail ())
154+ .name (response .getName ())
155+ .role (response .getRole ())
156+ .phoneNumber (response .getPhoneNumber ())
157+ .point (response .getPoint ())
158+ .build ();
140159
141160 return ResponseEntity .ok ()
142- .header (HttpHeaders .SET_COOKIE , cookie .toString ())
143- .header (HttpHeaders .AUTHORIZATION , "Bearer " + response . getAccessToken ())
144- .body (response );
161+ .header (HttpHeaders .SET_COOKIE , accessCookie .toString ())
162+ .header (HttpHeaders .SET_COOKIE , refreshCookie . toString ())
163+ .body (safeResponse );
145164 }
146165
147166 @ Operation (
@@ -203,8 +222,19 @@ public ResponseEntity<?> reissue(
203222 response .header (HttpHeaders .SET_COOKIE , cookie .toString ());
204223 }
205224
206- // 응답 반환
207- return response .body (Map .of ("accessToken" , tokens .get ("accessToken" )));
225+ // accessToken을 HttpOnly 쿠키로 설정
226+ ResponseCookie accessCookie = ResponseCookie .from ("access" , tokens .get ("accessToken" ))
227+ .httpOnly (true )
228+ .secure (true )
229+ .sameSite ("None" )
230+ .path ("/" )
231+ .maxAge (60L * 60 ) // 1 hour
232+ .build ();
233+
234+ response .header (HttpHeaders .SET_COOKIE , accessCookie .toString ());
235+
236+ // JSON에서 accessToken 제거
237+ return response .body (Map .of ("message" , "토큰 갱신 성공" ));
208238
209239 } catch (Exception e ) {
210240 log .warn ("토큰 재발급 실패: {}" , e .getMessage ());
@@ -215,7 +245,7 @@ public ResponseEntity<?> reissue(
215245
216246 @ Operation (
217247 summary = "로그아웃 API" ,
218- description = "Access Token을 무효화하고 Refresh Token 쿠키를 삭제합니다." ,
248+ description = "Access Token을 무효화하고 Access/ Refresh Token 쿠키를 삭제합니다. 토큰이 없어도 정상적으로 처리됩니다 ." ,
219249 responses = {
220250 @ ApiResponse (
221251 responseCode = "200" ,
@@ -226,45 +256,38 @@ public ResponseEntity<?> reissue(
226256 "message": "로그아웃 성공"
227257 }
228258 """ ))
229- ),
230- @ ApiResponse (
231- responseCode = "400" ,
232- description = "Authorization 헤더 형식이 잘못됨" ,
233- content = @ Content (mediaType = "application/json" ,
234- examples = @ ExampleObject (value = """
235- {
236- "message": "잘못된 Authorization 헤더 형식입니다."
237- }
238- """ ))
239259 )
240260 }
241261 )
242262 @ PostMapping ("/logout" )
243263 public ResponseEntity <?> logout (
244- @ Parameter (description = "Bearer 토큰" , example = "Bearer eyJhbGciOiJIUzI1NiJ9..." )
245- @ RequestHeader (value = "Authorization" , required = false ) String authorizationHeader
264+ @ Parameter (description = "Access Token 쿠키" , example = "access=abc123" )
265+ @ CookieValue (value = "access" , required = false ) String accessToken ,
266+ @ Parameter (description = "Refresh Token 쿠키" , example = "refresh=abc123" )
267+ @ CookieValue (value = "refresh" , required = false ) String refreshToken
246268 ) {
247- // 헤더 유효성 검사
248- if (authorizationHeader == null || !authorizationHeader .startsWith ("Bearer " )) {
249- return ResponseEntity .badRequest ()
250- .body (Map .of ("message" , "잘못된 Authorization 헤더 형식입니다." ));
269+ // 토큰이 없어도 로그아웃 처리 (멱등성 보장)
270+ if (accessToken != null && !accessToken .isEmpty ()) {
271+ try {
272+ loginService .logout (accessToken );
273+ } catch (JwtException | IllegalArgumentException e ) {
274+ log .warn ("Invalid or expired JWT during logout: {}" , e .getMessage ());
275+ } catch (Exception e ) {
276+ log .error ("Unexpected error during logout" , e );
277+ }
251278 }
252279
253- String token = authorizationHeader .substring (7 );
254-
255- // 예외 처리 및 멱등성 보장
256- try {
257- loginService .logout (token );
258- } catch (JwtException | IllegalArgumentException e ) {
259- // 이미 만료되었거나 잘못된 토큰이라도 200 OK로 응답 (멱등성 보장)
260- log .warn ("Invalid or expired JWT during logout: {}" , e .getMessage ());
261- } catch (Exception e ) {
262- log .error ("Unexpected error during logout" , e );
263- // 내부 예외는 500으로 보내지 않고 안전하게 처리
264- }
280+ // Access Token 쿠키 삭제
281+ ResponseCookie deleteAccessCookie = ResponseCookie .from ("access" , "" )
282+ .httpOnly (true )
283+ .secure (true )
284+ .sameSite ("None" )
285+ .path ("/" )
286+ .maxAge (0 )
287+ .build ();
265288
266289 // Refresh Token 쿠키 삭제
267- ResponseCookie deleteCookie = ResponseCookie .from ("refresh" , "" )
290+ ResponseCookie deleteRefreshCookie = ResponseCookie .from ("refresh" , "" )
268291 .httpOnly (true )
269292 .secure (true )
270293 .sameSite ("None" )
@@ -273,7 +296,8 @@ public ResponseEntity<?> logout(
273296 .build ();
274297
275298 return ResponseEntity .ok ()
276- .header (HttpHeaders .SET_COOKIE , deleteCookie .toString ())
299+ .header (HttpHeaders .SET_COOKIE , deleteAccessCookie .toString ())
300+ .header (HttpHeaders .SET_COOKIE , deleteRefreshCookie .toString ())
277301 .body (Map .of ("message" , "로그아웃 성공" ));
278302 }
279303
0 commit comments