From 0c6035944aec3fa00058ff1bd5dddf54e926aec1 Mon Sep 17 00:00:00 2001 From: 1223v <1223v@naver.com> Date: Fri, 17 Nov 2023 20:22:48 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20?= =?UTF-8?q?API=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80.=20RT=EB=A7=8C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20->=20AT=EB=8A=94=20=ED=81=B4=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EC=96=B8=ED=8A=B8=EC=97=90=EC=84=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/jwt/service/JwtService.java | 51 +++++-------------- .../src/user/UserController.java | 11 ++++ .../readyverydemo/src/user/UserService.java | 4 ++ .../src/user/UserServiceImpl.java | 25 +++++++++ 4 files changed, 52 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/readyvery/readyverydemo/security/jwt/service/JwtService.java b/src/main/java/com/readyvery/readyverydemo/security/jwt/service/JwtService.java index 70aea91..e0f74e0 100644 --- a/src/main/java/com/readyvery/readyverydemo/security/jwt/service/JwtService.java +++ b/src/main/java/com/readyvery/readyverydemo/security/jwt/service/JwtService.java @@ -1,5 +1,7 @@ package com.readyvery.readyverydemo.security.jwt.service; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Date; import java.util.NoSuchElementException; @@ -28,12 +30,12 @@ public class JwtService { /** * JWT의 Subject와 Claim으로 email 사용 -> 클레임의 name을 "email"으로 설정 - * JWT의 헤더에 들어오는 값 : 'Authorization(Key) = Bearer {토큰} (Value)' 형식 + * */ private static final String ACCESS_TOKEN_SUBJECT = "AccessToken"; private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken"; private static final String EMAIL_CLAIM = "email"; - //private static final String BEARER = "Bearer "; + private static final String USER_NUMBER = "userNumber"; private final UserRepository userRepository; @Value("${jwt.secretKey}") @@ -55,10 +57,12 @@ public class JwtService { public String createAccessToken(String email) { UserInfo userInfo = userRepository.findByEmail(email) .orElseThrow(() -> new IllegalArgumentException("이메일에 해당하는 유저가 없습니다.")); - Date now = new Date(); + Instant now = Instant.now(); + Instant expirationTime = now.plus(accessTokenExpirationPeriod, ChronoUnit.SECONDS); + return JWT.create() // JWT 토큰을 생성하는 빌더 반환 .withSubject(ACCESS_TOKEN_SUBJECT) // JWT의 Subject 지정 -> AccessToken이므로 AccessToken - .withExpiresAt(new Date(now.getTime() + accessTokenExpirationPeriod)) // 토큰 만료 시간 설정 + .withExpiresAt(Date.from(expirationTime)) // 토큰 만료 시간 설정 //클레임으로는 저희는 email 하나만 사용합니다. //추가적으로 식별자나, 이름 등의 정보를 더 추가하셔도 됩니다. @@ -73,23 +77,14 @@ public String createAccessToken(String email) { * RefreshToken은 Claim에 email도 넣지 않으므로 withClaim() X */ public String createRefreshToken() { - Date now = new Date(); + Instant now = Instant.now(); + Instant expirationTime = now.plus(refreshTokenExpirationPeriod, ChronoUnit.SECONDS); return JWT.create() .withSubject(REFRESH_TOKEN_SUBJECT) - .withExpiresAt(new Date(now.getTime() + refreshTokenExpirationPeriod)) + .withExpiresAt(Date.from(expirationTime)) .sign(Algorithm.HMAC512(secretKey)); } - // /** - // * AccessToken 헤더에 실어서 보내기 - // */ - // public void sendAccessToken(HttpServletResponse response, String accessToken) { - // response.setStatus(HttpServletResponse.SC_OK); - // - // response.setHeader(accessHeader, accessToken); - // log.info("재발급된 Access Token : {}", accessToken); - // } - /** * AccessToken + RefreshToken 헤더에 실어서 보내기 */ @@ -102,28 +97,6 @@ public void sendAccessAndRefreshToken(HttpServletResponse response, String acces log.info("Access Token, Refresh Token 헤더 설정 완료"); } - // /** - // * 헤더에서 RefreshToken 추출 - // * 토큰 형식 : Bearer XXX에서 Bearer를 제외하고 순수 토큰만 가져오기 위해서 - // * 헤더를 가져온 후 "Bearer"를 삭제(""로 replace) - // */ - // public Optional extractRefreshToken(HttpServletRequest request) { - // return Optional.ofNullable(request.getHeader(refreshHeader)) - // .filter(refreshToken -> refreshToken.startsWith(BEARER)) - // .map(refreshToken -> refreshToken.replace(BEARER, "")); - // } - - /** - * 헤더에서 AccessToken 추출 - * 토큰 형식 : Bearer XXX에서 Bearer를 제외하고 순수 토큰만 가져오기 위해서 - * 헤더를 가져온 후 "Bearer"를 삭제(""로 replace) - */ - // public Optional extractAccessToken(HttpServletRequest request) { - // return Optional.ofNullable(request.getHeader(accessHeader)) - // .filter(refreshToken -> refreshToken.startsWith(BEARER)) - // .map(refreshToken -> refreshToken.replace(BEARER, "")); - // } - /** * 쿠키에서 RefreshToken 추출 */ @@ -178,7 +151,7 @@ public Optional extractEmail(String accessToken) { */ public void setAccessTokenCookie(HttpServletResponse response, String accessToken) { Cookie accessTokenCookie = new Cookie(accessCookie, accessToken); // 쿠키 생성 - accessTokenCookie.setHttpOnly(true); // JavaScript가 쿠키를 읽는 것을 방지 + //accessTokenCookie.setHttpOnly(true); // JavaScript가 쿠키를 읽는 것을 방지 accessTokenCookie.setPath("/"); // 쿠키 경로 설정 // 필요한 경우 Secure 플래그 설정 (HTTPS에서만 쿠키 전송) diff --git a/src/main/java/com/readyvery/readyverydemo/src/user/UserController.java b/src/main/java/com/readyvery/readyverydemo/src/user/UserController.java index a564daf..6051bdd 100644 --- a/src/main/java/com/readyvery/readyverydemo/src/user/UserController.java +++ b/src/main/java/com/readyvery/readyverydemo/src/user/UserController.java @@ -9,6 +9,7 @@ import com.readyvery.readyverydemo.src.user.dto.UserAuthRes; import com.readyvery.readyverydemo.src.user.dto.UserInfoRes; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @RestController @@ -56,6 +57,16 @@ public CustomUserDetails userDetail(@AuthenticationPrincipal CustomUserDetails u return userDetails; } + /** + * 사용자 로그아웃 + */ + @GetMapping("/user/logout") + public boolean logout(@AuthenticationPrincipal CustomUserDetails userDetails, HttpServletResponse response) { + + userServiceImpl.removeRefreshTokenInDB(userDetails.getId(), response); + return true; + } + /** * Access 토큰 재발급 * diff --git a/src/main/java/com/readyvery/readyverydemo/src/user/UserService.java b/src/main/java/com/readyvery/readyverydemo/src/user/UserService.java index 5601a01..7ab523c 100644 --- a/src/main/java/com/readyvery/readyverydemo/src/user/UserService.java +++ b/src/main/java/com/readyvery/readyverydemo/src/user/UserService.java @@ -4,9 +4,13 @@ import com.readyvery.readyverydemo.src.user.dto.UserAuthRes; import com.readyvery.readyverydemo.src.user.dto.UserInfoRes; +import jakarta.servlet.http.HttpServletResponse; + public interface UserService { UserAuthRes getUserAuthById(CustomUserDetails userDetails); UserInfoRes getUserInfoById(Long id); + void removeRefreshTokenInDB(Long id, HttpServletResponse response); + } diff --git a/src/main/java/com/readyvery/readyverydemo/src/user/UserServiceImpl.java b/src/main/java/com/readyvery/readyverydemo/src/user/UserServiceImpl.java index 3580a1b..2f00bbd 100644 --- a/src/main/java/com/readyvery/readyverydemo/src/user/UserServiceImpl.java +++ b/src/main/java/com/readyvery/readyverydemo/src/user/UserServiceImpl.java @@ -1,5 +1,6 @@ package com.readyvery.readyverydemo.src.user; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -12,6 +13,8 @@ import com.readyvery.readyverydemo.src.user.dto.UserInfoRes; import com.readyvery.readyverydemo.src.user.dto.UserMapper; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @Service @@ -21,6 +24,8 @@ public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final UserMapper userMapper; + @Value("${jwt.refresh.cookie}") + private String refreshCookie; @Override public UserAuthRes getUserAuthById(CustomUserDetails userDetails) { @@ -35,6 +40,26 @@ public UserInfoRes getUserInfoById(Long id) { return userMapper.userInfoToUserInfoRes(userInfo); } + @Override + public void removeRefreshTokenInDB(Long id, HttpServletResponse response) { + UserInfo user = getUserInfo(id); + user.updateRefresh(null); // Refresh Token을 null 또는 빈 문자열로 업데이트 + userRepository.save(user); + invalidateRefreshTokenCookie(response); // 쿠키 무효화 + } + + /** + * 로그아웃 + * @param response + */ + private void invalidateRefreshTokenCookie(HttpServletResponse response) { + Cookie refreshTokenCookie = new Cookie(refreshCookie, null); // 쿠키 이름을 동일하게 설정 + refreshTokenCookie.setHttpOnly(true); + refreshTokenCookie.setPath("/api/v1/refresh/token"); // 기존과 동일한 경로 설정 + refreshTokenCookie.setMaxAge(0); // 만료 시간을 0으로 설정하여 즉시 만료 + response.addCookie(refreshTokenCookie); + } + private UserInfo getUserInfo(Long id) { return userRepository.findById(id).orElseThrow( () -> new BusinessLogicException(ExceptionCode.USER_NOT_FOUND)