Skip to content

Commit

Permalink
Feat: 로그아웃 API 기능 추가. RT만 삭제 -> AT는 클라이언트에서 삭제
Browse files Browse the repository at this point in the history
  • Loading branch information
1223v committed Nov 17, 2023
1 parent e3210d5 commit 0c60359
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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}")
Expand All @@ -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 하나만 사용합니다.
//추가적으로 식별자나, 이름 등의 정보를 더 추가하셔도 됩니다.
Expand All @@ -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 헤더에 실어서 보내기
*/
Expand All @@ -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<String> 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<String> extractAccessToken(HttpServletRequest request) {
// return Optional.ofNullable(request.getHeader(accessHeader))
// .filter(refreshToken -> refreshToken.startsWith(BEARER))
// .map(refreshToken -> refreshToken.replace(BEARER, ""));
// }

/**
* 쿠키에서 RefreshToken 추출
*/
Expand Down Expand Up @@ -178,7 +151,7 @@ public Optional<String> 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에서만 쿠키 전송)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 토큰 재발급
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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)
Expand Down

0 comments on commit 0c60359

Please sign in to comment.