diff --git a/cakey-api/src/main/java/com/cakey/exception/handler/GlobalExceptionHandler.java b/cakey-api/src/main/java/com/cakey/common/exception/handler/GlobalExceptionHandler.java similarity index 99% rename from cakey-api/src/main/java/com/cakey/exception/handler/GlobalExceptionHandler.java rename to cakey-api/src/main/java/com/cakey/common/exception/handler/GlobalExceptionHandler.java index 6774c2c..27efb95 100644 --- a/cakey-api/src/main/java/com/cakey/exception/handler/GlobalExceptionHandler.java +++ b/cakey-api/src/main/java/com/cakey/common/exception/handler/GlobalExceptionHandler.java @@ -1,4 +1,4 @@ -package com.cakey.exception.handler; +package com.cakey.common.exception.handler; import com.cakey.cake.exception.CakeyApiBaseException; import com.cakey.rescode.ErrorBaseCode; diff --git a/cakey-api/src/main/java/com/cakey/common/filter/ExceptionHandlerFilter.java b/cakey-api/src/main/java/com/cakey/common/filter/ExceptionHandlerFilter.java new file mode 100644 index 0000000..8948b9a --- /dev/null +++ b/cakey-api/src/main/java/com/cakey/common/filter/ExceptionHandlerFilter.java @@ -0,0 +1,57 @@ +package com.cakey.common.filter; + +import com.cakey.Constants; +import com.cakey.common.response.BaseResponse; +import com.cakey.rescode.ErrorBaseCode; +import com.cakey.rescode.ErrorCode; +import com.cakey.user.exception.UserUnAuthorizedException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.io.PrintWriter; + +@Slf4j +public class ExceptionHandlerFilter extends OncePerRequestFilter { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException { + try { + filterChain.doFilter(request, response); + } catch (UserUnAuthorizedException e) { + handleUnauthorizedException(response, e); + } catch (Exception ee) { + handleException(response, ee); + } + } + + private void handleUnauthorizedException(HttpServletResponse response, Exception e) throws IOException { + UserUnAuthorizedException ue = (UserUnAuthorizedException) e; + ErrorCode errorCode = ue.getErrorCode(); + HttpStatus httpStatus = errorCode.getHttpStatus(); + setResponse(response, httpStatus, errorCode); + } + + private void handleException(HttpServletResponse response, Exception e) throws IOException { + log.error("-------------- Exception Handler Filter ------------ \n" + e.getCause() + e.getMessage()); + log.error("\n ---------------------------------------------------------"); + setResponse(response, HttpStatus.INTERNAL_SERVER_ERROR, ErrorBaseCode.INTERNAL_SERVER_ERROR); + } + + private void setResponse(HttpServletResponse response, HttpStatus httpStatus, ErrorCode errorBaseCode) throws IOException { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(Constants.CHARACTER_TYPE); + response.setStatus(httpStatus.value()); + PrintWriter writer = response.getWriter(); + writer.write(objectMapper.writeValueAsString(BaseResponse.of(errorBaseCode))); + } +} diff --git a/cakey-api/src/main/java/com/cakey/common/filter/OptionalAuthenticationFilter.java b/cakey-api/src/main/java/com/cakey/common/filter/OptionalAuthenticationFilter.java index 993c8b6..92aaeeb 100644 --- a/cakey-api/src/main/java/com/cakey/common/filter/OptionalAuthenticationFilter.java +++ b/cakey-api/src/main/java/com/cakey/common/filter/OptionalAuthenticationFilter.java @@ -1,8 +1,12 @@ package com.cakey.common.filter; import com.cakey.Constants; +import com.cakey.exception.AuthExpiredJwtException; +import com.cakey.exception.AuthWrongJwtException; import com.cakey.jwt.auth.JwtProvider; import com.cakey.jwt.auth.UserAuthentication; +import com.cakey.rescode.ErrorBaseCode; +import com.cakey.user.exception.UserUnAuthorizedException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; @@ -13,7 +17,7 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.apache.tomcat.util.http.parser.Authorization; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -21,6 +25,7 @@ @Component @RequiredArgsConstructor +@Slf4j public class OptionalAuthenticationFilter extends OncePerRequestFilter { //로그인 상관 X private final JwtProvider jwtProvider; @@ -45,7 +50,7 @@ public class OptionalAuthenticationFilter extends OncePerRequestFilter { //로 "/api/v1/store/*/size", "/api/v1/store/*/information", "/api/v1/store/*/kakaoLink", - "api/v1/user/login" + "/api/v1/user/login" ); @Override @@ -60,39 +65,41 @@ protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain - ) throws ServletException, IOException { + ) { String accessToken = request.getHeader(Constants.AUTHORIZATION); - if ("Bearer: ".equals(accessToken)) { - accessToken = null; + if (accessToken != null) { ///Authorization 헤더 왔을 때 + if (StringUtils.hasText(accessToken) && accessToken.startsWith(Constants.BEARER)) { + accessToken = accessToken.substring(Constants.BEARER.length()); + } else { + throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_WRONG_AT); ///액세스토큰 비어있거나 Bearer로 시작안할때 + } - } else if (StringUtils.hasText(accessToken) && accessToken.startsWith(Constants.BEARER)) { - /// "Bearer: "로 시작하는 경우 - accessToken = accessToken.substring(Constants.BEARER.length()).trim(); + try { + final long userId = jwtProvider.getUserIdFromSubject(accessToken); + SecurityContextHolder + .getContext() + .setAuthentication(new UserAuthentication(userId, null, null)); + filterChain.doFilter(request, response); - /// 접두사 제거 후 내용이 없으면 null 처리 - if (accessToken.isEmpty()) { - accessToken = null; + } catch (AuthExpiredJwtException e) { + throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_AT_EXPIRED); + } catch (AuthWrongJwtException e) { + throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_WRONG_AT); + } catch (Exception e) { + log.error("-------UNAUTHORIZED ERROR LOG -----------\n" + e.getMessage(), e); + throw new UserUnAuthorizedException(ErrorBaseCode.INTERNAL_SERVER_ERROR); } - } else { - // 유효하지 않은 경우 null 처리 - accessToken = null; - } - - if (accessToken != null) { - final long userId = jwtProvider.getUserIdFromSubject(accessToken); - SecurityContextHolder - .getContext() - .setAuthentication(new UserAuthentication(userId, null, null)); - } else { + } else { ///Authorization 헤더 안왔을 때 SecurityContextHolder .getContext() .setAuthentication(new UserAuthentication(null, null, null)); + try { + filterChain.doFilter(request, response); + } catch (Exception e) { + log.error("-------UNAUTHORIZED ERROR LOG ----------- \n " + e.getMessage(), e); + throw new UserUnAuthorizedException(ErrorBaseCode.INTERNAL_SERVER_ERROR); } } - - filterChain.doFilter(request, response); } - - } diff --git a/cakey-api/src/main/java/com/cakey/common/filter/RequiredAuthenticationFilter.java b/cakey-api/src/main/java/com/cakey/common/filter/RequiredAuthenticationFilter.java index e66ca2c..314f312 100644 --- a/cakey-api/src/main/java/com/cakey/common/filter/RequiredAuthenticationFilter.java +++ b/cakey-api/src/main/java/com/cakey/common/filter/RequiredAuthenticationFilter.java @@ -2,11 +2,16 @@ import com.cakey.Constants; import com.cakey.common.response.ApiResponseUtil; +import com.cakey.exception.AuthExpiredJwtException; +import com.cakey.exception.AuthWrongJwtException; import com.cakey.jwt.auth.JwtProvider; import com.cakey.jwt.auth.UserAuthentication; import com.cakey.rescode.ErrorBaseCode; import com.cakey.user.exception.UserBadRequestException; +import com.cakey.user.exception.UserErrorCode; +import com.cakey.user.exception.UserUnAuthorizedException; import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; @@ -30,7 +35,6 @@ @Slf4j public class RequiredAuthenticationFilter extends OncePerRequestFilter { private final JwtProvider jwtProvider; //로그인 필수 - private final ObjectMapper objectMapper; // 필터를 건너뛸 API 경로 목록 private static final List EXCLUDED_PATHS = List.of( @@ -52,8 +56,7 @@ public class RequiredAuthenticationFilter extends OncePerRequestFilter { "/api/v1/store/*/size", "/api/v1/store/*/information", "/api/v1/store/*/kakaoLink", - "api/v1/user/login" - + "/api/v1/user/login" ); @Override @@ -68,42 +71,28 @@ protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain - ) throws ServletException, IOException { - try { - String accessToken = request.getHeader(Constants.AUTHORIZATION); + ) { + String accessToken = request.getHeader(Constants.AUTHORIZATION); - if (StringUtils.hasText(accessToken) && accessToken.startsWith(Constants.BEARER)) { - accessToken = accessToken.substring(Constants.BEARER.length()); - } else { - throw new Exception(); - } + if (StringUtils.hasText(accessToken) && accessToken.startsWith(Constants.BEARER)) { + accessToken = accessToken.substring(Constants.BEARER.length()); + } else { + throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_WRONG_AT); ///액세스토큰 비어있거나 Bearer로 시작안할때 + } + try { final long userId = jwtProvider.getUserIdFromSubject(accessToken); - SecurityContextHolder .getContext() .setAuthentication(new UserAuthentication(userId, null, null)); - - filterChain.doFilter(request, response); // 다음 필터로 요청 전달 + filterChain.doFilter(request, response); + } catch (AuthExpiredJwtException e) { + throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_AT_EXPIRED); + } catch (AuthWrongJwtException e) { + throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_WRONG_AT); } catch (Exception e) { - log.error("--------------------쿠키 에러------------------------"); - log.error(e.getMessage()); - - // 예외 발생 시 JSON 응답 생성 - final ErrorBaseCode errorCode = ErrorBaseCode.UNAUTHORIZED; - - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setCharacterEncoding(Constants.CHARACTER_TYPE); - response.setStatus(errorCode.getHttpStatus().value()); // HTTP 상태 코드 401 설정 - - log.error("--------------------토큰 없음------------------------"); //todo: 추후 삭제(테스트용) - // `ApiResponseUtil.failure`를 이용해 응답 작성 - final PrintWriter writer = response.getWriter(); - writer.write(objectMapper.writeValueAsString( - ApiResponseUtil.failure(errorCode).getBody() - )); - writer.flush(); - return; // 체인 호출 중단 + log.error("-------UNAUTHORIZED ERROR LOG -----------\n" + e.getMessage(), e); + throw new UserUnAuthorizedException(ErrorBaseCode.INTERNAL_SERVER_ERROR); } } -} \ No newline at end of file +} diff --git a/cakey-api/src/main/java/com/cakey/config/SecurityConfig.java b/cakey-api/src/main/java/com/cakey/config/SecurityConfig.java index a7d056e..c411c9a 100644 --- a/cakey-api/src/main/java/com/cakey/config/SecurityConfig.java +++ b/cakey-api/src/main/java/com/cakey/config/SecurityConfig.java @@ -1,12 +1,10 @@ package com.cakey.config; -import lombok.RequiredArgsConstructor; -import org.springframework.boot.web.servlet.FilterRegistrationBean; +import com.cakey.common.filter.ExceptionHandlerFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; @@ -14,7 +12,6 @@ @Configuration @EnableWebSecurity -@RequiredArgsConstructor public class SecurityConfig { @Bean @@ -24,6 +21,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .httpBasic(AbstractHttpConfigurer::disable) .sessionManagement( session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(new ExceptionHandlerFilter(), UsernamePasswordAuthenticationFilter.class) .authorizeHttpRequests( auth -> auth.anyRequest().permitAll()) .build(); } diff --git a/cakey-api/src/main/java/com/cakey/user/controller/UserController.java b/cakey-api/src/main/java/com/cakey/user/controller/UserController.java index b546b91..58ffd61 100644 --- a/cakey-api/src/main/java/com/cakey/user/controller/UserController.java +++ b/cakey-api/src/main/java/com/cakey/user/controller/UserController.java @@ -6,9 +6,11 @@ import com.cakey.common.response.ApiResponseUtil; import com.cakey.common.response.BaseResponse; import com.cakey.rescode.SuccessCode; +import com.cakey.user.dto.JwtReissueRes; import com.cakey.user.dto.LoginSuccessRes; import com.cakey.user.service.UserService; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; @@ -53,15 +55,16 @@ public ResponseEntity> logout( userService.logout(userId, response); return ApiResponseUtil.success(SuccessCode.OK); } - -// //jwt 재발급 -// @GetMapping("/reissue") -// public ResponseEntity> jwtReissue( -// @CookieValue(name = "refreshToken") Cookie cookie -// ) { -// final String refreshToken = cookie.getValue(); -// final LoginSuccessRes loginSuccessRes = userService.jwtReissue(refreshToken); -// return ApiResponseUtil.success(SuccessCode.OK, loginSuccessRes); -// } + //jwt 재발급 + @PatchMapping("/reissue") + public ResponseEntity> jwtReissue( + @RequestHeader(value = "userId") final long userId, + @CookieValue(name = "refreshToken") final Cookie cookie, + final HttpServletResponse response + + ) { + final JwtReissueRes jwtReissueRes = userService.jwtReissue(userId, cookie.getValue(), response); + return ApiResponseUtil.success(SuccessCode.OK, jwtReissueRes); + } } diff --git a/cakey-api/src/main/java/com/cakey/user/dto/JwtReissueRes.java b/cakey-api/src/main/java/com/cakey/user/dto/JwtReissueRes.java new file mode 100644 index 0000000..d9bc341 --- /dev/null +++ b/cakey-api/src/main/java/com/cakey/user/dto/JwtReissueRes.java @@ -0,0 +1,9 @@ +package com.cakey.user.dto; + +public record JwtReissueRes( + String accessToken +) { + public static JwtReissueRes of(final String accessToken) { + return new JwtReissueRes(accessToken); + } +} diff --git a/cakey-api/src/main/java/com/cakey/user/exception/UserErrorCode.java b/cakey-api/src/main/java/com/cakey/user/exception/UserErrorCode.java index 28f238a..d55ac19 100644 --- a/cakey-api/src/main/java/com/cakey/user/exception/UserErrorCode.java +++ b/cakey-api/src/main/java/com/cakey/user/exception/UserErrorCode.java @@ -8,6 +8,9 @@ @RequiredArgsConstructor public enum UserErrorCode implements ErrorCode { + /** + * 400 Bad Request + */ /** * 404 Not Found @@ -18,7 +21,8 @@ public enum UserErrorCode implements ErrorCode { /** * 500 Server Internal Error */ - KAKAO_LOGIN_FAILED(HttpStatus.BAD_REQUEST, 50030, "카카오 로그인에 실패하였습니다"), + KAKAO_LOGIN_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 50030, "카카오 로그인에 실패하였습니다"), + USER_RT_CACHE_NOT_FOUNT(HttpStatus.INTERNAL_SERVER_ERROR, 50031, "서버 내부 캐시에 저장된 리프레시 토큰이 없습니다."), ; private final HttpStatus httpStatus; diff --git a/cakey-api/src/main/java/com/cakey/user/exception/UserUnAuthorizedException.java b/cakey-api/src/main/java/com/cakey/user/exception/UserUnAuthorizedException.java new file mode 100644 index 0000000..c3eb233 --- /dev/null +++ b/cakey-api/src/main/java/com/cakey/user/exception/UserUnAuthorizedException.java @@ -0,0 +1,15 @@ +package com.cakey.user.exception; + +import com.cakey.rescode.ErrorCode; +import org.springframework.http.HttpStatus; + +public class UserUnAuthorizedException extends UserApiBaseException { + public UserUnAuthorizedException(final ErrorCode errorCode) { + super(errorCode); + } + + @Override + HttpStatus getStatus() { + return HttpStatus.UNAUTHORIZED; + } +} diff --git a/cakey-api/src/main/java/com/cakey/user/service/UserService.java b/cakey-api/src/main/java/com/cakey/user/service/UserService.java index 7935489..9c22152 100644 --- a/cakey-api/src/main/java/com/cakey/user/service/UserService.java +++ b/cakey-api/src/main/java/com/cakey/user/service/UserService.java @@ -3,30 +3,28 @@ import com.cakey.Constants; import com.cakey.client.SocialType; -import com.cakey.client.dto.LoginReq; -import com.cakey.client.kakao.api.KakaoSocialService; +import com.cakey.client.kakao.api.KakaoSocialProvider; import com.cakey.client.kakao.api.dto.KakaoUserDto; import com.cakey.client.kakao.api.dto.UserCreateDto; +import com.cakey.exception.AuthExpiredJwtException; import com.cakey.exception.AuthKakaoException; +import com.cakey.exception.AuthRTCacheException; +import com.cakey.exception.AuthWrongJwtException; import com.cakey.jwt.auth.JwtProvider; import com.cakey.jwt.domain.Token; import com.cakey.jwt.domain.UserRole; -import com.cakey.rescode.ErrorCode; +import com.cakey.rescode.ErrorBaseCode; +import com.cakey.user.dto.JwtReissueRes; import com.cakey.user.dto.LoginSuccessRes; import com.cakey.common.exception.NotFoundBaseException; import com.cakey.user.dto.UserInfoDto; import com.cakey.user.dto.UserInfoRes; -import com.cakey.user.exception.UserBadRequestException; -import com.cakey.user.exception.UserErrorCode; -import com.cakey.user.exception.UserKakaoException; -import com.cakey.user.exception.UserNotFoundException; +import com.cakey.user.exception.*; import com.cakey.user.facade.UserFacade; -import com.cakey.user.facade.UserRetriever; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cloud.openfeign.aot.FeignChildContextInitializer; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -38,8 +36,7 @@ public class UserService { private final UserFacade userFacade; - private final KakaoSocialService kakaoSocialService; - + private final KakaoSocialProvider kakaoSocialProvider; private final JwtProvider jwtProvider; @Transactional @@ -49,12 +46,12 @@ public LoginSuccessRes login( final String redirectUri, final HttpServletResponse response ) { - //카카오 유저정보 + ///카카오 유저정보 final KakaoUserDto kakaoUserInfo; if (socialType.equals(SocialType.KAKAO)) { try { - kakaoUserInfo = kakaoSocialService.getKakaoUserInfo(authorizationCode, redirectUri); + kakaoUserInfo = kakaoSocialProvider.getKakaoUserInfo(authorizationCode, redirectUri); } catch (AuthKakaoException e) { throw new UserKakaoException(UserErrorCode.KAKAO_LOGIN_FAILED); } @@ -62,35 +59,31 @@ public LoginSuccessRes login( throw new UserBadRequestException(UserErrorCode.KAKAO_LOGIN_FAILED); } - //플랫폼 아이디 + ///플랫폼 아이디 final long platformId = kakaoUserInfo.id(); - //이미 우리 유저인지 확인해서 userId 뽑기 + ///이미 우리 유저인지 확인해서 userId 뽑기 final Long userId = userFacade.findUserIdFromSocialTypeAndPlatformId(socialType, platformId); - if (userId == null) { //유저 처음 가입 - //유저생성 + if (userId == null) { ///유저 처음 가입 + ///유저생성 final UserCreateDto userCreateDto = UserCreateDto.of(kakaoUserInfo.kakaoAccount().profile().nickname(), UserRole.USER, socialType, kakaoUserInfo.id(), kakaoUserInfo.kakaoAccount().email()); final long savedUserId = userFacade.createUser(userCreateDto); final Token newToken = jwtProvider.issueToken(savedUserId); - //쿠키설정 + ///쿠키설정 setRefreshCookie(newToken.getRefreshToken(), response); return LoginSuccessRes.of( savedUserId, kakaoUserInfo.kakaoAccount().profile().nickname(), newToken.getAccessToken()); - } else { //전에 이미 우리 유저 - - //리프레시 토큰 캐시 삭제 - jwtProvider.deleteRefreshToken(userId); - + } else { ///전에 이미 우리 유저 final Token newToken = jwtProvider.issueToken(userId); - //쿠키 설정 + ///쿠키 설정 setRefreshCookie(newToken.getRefreshToken(), response); return LoginSuccessRes.of( @@ -100,14 +93,47 @@ public LoginSuccessRes login( } } -// //jwt 재발급 -// public LoginSuccessRes jwtReissue(final String refreshToken) { - ///refresh token 검증 -// -// } + //jwt 재발급 + public JwtReissueRes jwtReissue(final long userId, final String refreshToken, final HttpServletResponse response) { + + final long userIdFromRT; + final String newRefreshToken; + + /// 받아온 RT로 userId 뽑아서 검증 + 뽑을 때 RT도 검증 + try { + userIdFromRT = jwtProvider.getUserIdFromSubject(refreshToken); + } catch (AuthExpiredJwtException e) { + throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_RT_EXPIRED); + } catch (AuthWrongJwtException e) { + throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_WRONG_RT); + } catch (Exception e) { + log.error("---------Jwt ReIssue Error ------- \n {} \n ----------------------------", e.getMessage(), e); + throw new UserUnAuthorizedException(ErrorBaseCode.INTERNAL_SERVER_ERROR); + } + + if (userId != userIdFromRT) { + throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_DIFF_USER_ID); + } + + /// 받아온 RT와 기존의 RT 비교 + try { + newRefreshToken = jwtProvider.findRTFromCache(userId); + } catch (AuthRTCacheException e) { + throw new UserUnAuthorizedException(UserErrorCode.USER_RT_CACHE_NOT_FOUNT); + } + if (!refreshToken.equals(newRefreshToken)) { + throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_WRONG_RT); + } + + /// 새로운 AT, RT 생성 + final Token newToken = jwtProvider.issueToken(userId); + setRefreshCookie(newToken.getRefreshToken(), response); + + return JwtReissueRes.of(newToken.getAccessToken()); + } //로그아웃 - public void logout(final long userId, final HttpServletResponse response) { + public void logout(final long userId, HttpServletResponse response) { try { userFacade.isExistById(userId); } catch (NotFoundBaseException e) { @@ -117,13 +143,10 @@ public void logout(final long userId, final HttpServletResponse response) { jwtProvider.deleteRefreshToken(userId); } - @CacheEvict(value = "refresh") - public void deleteRefreshToken(final long userId) { } - //refreshToken 쿠키 삭제 public void deleteRefreshCookie(HttpServletResponse response) { ResponseCookie refreshCookie = ResponseCookie.from(Constants.REFRESH_TOKEN, "") - .maxAge(0) // 쿠키 즉시 삭제 + .maxAge(0) /// 쿠키 즉시 삭제 .path("/") .secure(true) .sameSite("None") @@ -132,9 +155,10 @@ public void deleteRefreshCookie(HttpServletResponse response) { response.addHeader("Set-Cookie", refreshCookie.toString()); } + //refreshToken 쿠키 세팅 public void setRefreshCookie(final String refreshToken, final HttpServletResponse response) { ResponseCookie refreshCookie = ResponseCookie.from(Constants.REFRESH_TOKEN, refreshToken) - .maxAge(30 * 24 * 60 * 60 * 1000L) /// 1달 + .maxAge(14 * 24 * 60 * 60 * 1000) /// 리프레시 만료기간 (14일) .path("/") .secure(true) .sameSite("None") @@ -151,7 +175,5 @@ public UserInfoRes getUserInfo(final long userId) { throw new UserNotFoundException(UserErrorCode.USER_NOT_FOUND); } return UserInfoRes.from(userInfoDto); - } - } \ No newline at end of file diff --git a/cakey-api/src/test/java/com/cakey/jwt/auth/JwtGeneratorTest.java b/cakey-api/src/test/java/com/cakey/jwt/auth/JwtGeneratorTest.java index 71f5f62..219972b 100644 --- a/cakey-api/src/test/java/com/cakey/jwt/auth/JwtGeneratorTest.java +++ b/cakey-api/src/test/java/com/cakey/jwt/auth/JwtGeneratorTest.java @@ -1,6 +1,7 @@ package com.cakey.jwt.auth; import com.cakey.TestConfiguration; +import com.cakey.user.service.UserService; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -36,6 +37,8 @@ class JwtGeneratorTest { @Autowired private JwtProvider jwtProvider; + @Autowired + private UserService userService; @Test @DisplayName("리프레시 토큰 캐시에 등록") diff --git a/cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoSocialService.java b/cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoSocialProvider.java similarity index 96% rename from cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoSocialService.java rename to cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoSocialProvider.java index a96eb96..9860301 100644 --- a/cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoSocialService.java +++ b/cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoSocialProvider.java @@ -7,18 +7,18 @@ import com.cakey.client.SocialType; import com.cakey.exception.AuthKakaoException; import com.cakey.jwt.domain.UserRole; -import com.cakey.rescode.ErrorBaseCode; import feign.FeignException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; -@Service +@Component @Slf4j @RequiredArgsConstructor -public class KakaoSocialService { +public class KakaoSocialProvider { private final KakaoApiClient kakaoApiClient; private final KakaoAuthApiClient kakaoAuthApiClient; diff --git a/cakey-auth/src/main/java/com/cakey/exception/AuthExpiredJwtException.java b/cakey-auth/src/main/java/com/cakey/exception/AuthExpiredJwtException.java new file mode 100644 index 0000000..b8b6fe6 --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/exception/AuthExpiredJwtException.java @@ -0,0 +1,4 @@ +package com.cakey.exception; + +public class AuthExpiredJwtException extends AuthBaseException{ +} diff --git a/cakey-auth/src/main/java/com/cakey/exception/AuthRTCacheException.java b/cakey-auth/src/main/java/com/cakey/exception/AuthRTCacheException.java new file mode 100644 index 0000000..bfbf594 --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/exception/AuthRTCacheException.java @@ -0,0 +1,4 @@ +package com.cakey.exception; + +public class AuthRTCacheException extends AuthBaseException { +} diff --git a/cakey-auth/src/main/java/com/cakey/exception/AuthWrongJwtException.java b/cakey-auth/src/main/java/com/cakey/exception/AuthWrongJwtException.java new file mode 100644 index 0000000..ef15c36 --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/exception/AuthWrongJwtException.java @@ -0,0 +1,4 @@ +package com.cakey.exception; + +public class AuthWrongJwtException extends AuthBaseException { +} diff --git a/cakey-auth/src/main/java/com/cakey/jwt/CacheConfig.java b/cakey-auth/src/main/java/com/cakey/jwt/CacheConfig.java index 6a939d9..0f0de93 100644 --- a/cakey-auth/src/main/java/com/cakey/jwt/CacheConfig.java +++ b/cakey-auth/src/main/java/com/cakey/jwt/CacheConfig.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; +import com.cakey.Constants; import com.github.benmanes.caffeine.cache.Caffeine; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; @@ -22,7 +23,8 @@ public class CacheConfig { public CacheManager cacheManager() { SimpleCacheManager cacheManager = new SimpleCacheManager(); List caches = new ArrayList<>(); - caches.add(new CaffeineCache("refresh", Caffeine.newBuilder() + caches.add(new CaffeineCache(Constants.REFRESH_TOKEN, Caffeine.newBuilder() + .expireAfterWrite(14, TimeUnit.DAYS) /// refresToken 만료기간인 14일과 같도록 설정 .initialCapacity(100) .maximumSize(500) .recordStats() diff --git a/cakey-auth/src/main/java/com/cakey/jwt/auth/CustomAccessDeniedHandler.java b/cakey-auth/src/main/java/com/cakey/jwt/auth/CustomAccessDeniedHandler.java deleted file mode 100644 index c11d316..0000000 --- a/cakey-auth/src/main/java/com/cakey/jwt/auth/CustomAccessDeniedHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.cakey.jwt.auth; - -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.web.access.AccessDeniedHandler; -import org.springframework.stereotype.Component; - -@Component -public class CustomAccessDeniedHandler implements AccessDeniedHandler { - - private final ObjectMapper mapper = new ObjectMapper(); - - @Override - public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { - setResponse(response); - } - - private void setResponse(HttpServletResponse response) throws IOException { - response.setStatus(HttpServletResponse.SC_FORBIDDEN); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - - ResponseEntity responseEntity = new ResponseEntity(HttpStatus.NOT_FOUND); - - response.getWriter().write(mapper.writeValueAsString(responseEntity)); - } -} \ No newline at end of file diff --git a/cakey-auth/src/main/java/com/cakey/jwt/auth/CustomJwtAuthenticationEntryPoint.java b/cakey-auth/src/main/java/com/cakey/jwt/auth/CustomJwtAuthenticationEntryPoint.java deleted file mode 100644 index 6a8cefd..0000000 --- a/cakey-auth/src/main/java/com/cakey/jwt/auth/CustomJwtAuthenticationEntryPoint.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.cakey.jwt.auth; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.stereotype.Component; - -@Component -public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint { - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException){ - setResponse(response); - } - - private void setResponse(HttpServletResponse response){ - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - } -} - diff --git a/cakey-auth/src/main/java/com/cakey/jwt/auth/JwtGenerator.java b/cakey-auth/src/main/java/com/cakey/jwt/auth/JwtGenerator.java index 7f861c3..923ae64 100644 --- a/cakey-auth/src/main/java/com/cakey/jwt/auth/JwtGenerator.java +++ b/cakey-auth/src/main/java/com/cakey/jwt/auth/JwtGenerator.java @@ -1,24 +1,21 @@ package com.cakey.jwt.auth; -import com.cakey.jwt.domain.Token; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.Jws; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import com.cakey.Constants; +import com.cakey.exception.AuthExpiredJwtException; +import com.cakey.exception.AuthWrongJwtException; +import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import java.security.Key; import java.util.Base64; import java.util.Date; import lombok.RequiredArgsConstructor; -import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.CachePut; import org.springframework.stereotype.Component; @RequiredArgsConstructor @Component public class JwtGenerator { + private final JwtProperties jwtProperties; //액세스 토큰 발급 @@ -26,6 +23,10 @@ public String generateAccessToken(final long userId) { final Date now = new Date(); final Date expireDate = generateExpirationDate(now, true); + /// 추후에 사장님어드민 추가시 + /// final Claims claims = Jwts.claims(); + /// claims.put(USER_ROLE, role); + return Jwts.builder() .setHeaderParam(Header.TYPE, Header.JWT_TYPE) .setSubject(String.valueOf(userId)) @@ -35,11 +36,16 @@ public String generateAccessToken(final long userId) { .compact(); } - @Cacheable(value = "refresh") + //리프레시 토큰 발급 + @CachePut(value = Constants.REFRESH_TOKEN, key = "#userId") ///없으면 추가하고, 이미 있으면 업데이트 public String generateRefreshToken(final long userId) { final Date now = new Date(); final Date expireDate = generateExpirationDate(now, false); + /// 추후에 사장님어드민 추가시 + /// final Claims claims = Jwts.claims(); + /// claims.put(USER_ROLE, role); + return Jwts.builder() .setHeaderParam(Header.TYPE, Header.JWT_TYPE) .setSubject(String.valueOf(userId)) @@ -57,7 +63,7 @@ private Date generateExpirationDate(final Date now, final boolean isAccessToken) } } - public Key getSigningKey() { + private Key getSigningKey() { return Keys.hmacShaKeyFor(encodeSecretKeyToBase64().getBytes()); } @@ -67,24 +73,16 @@ private String encodeSecretKeyToBase64() { public Jws parseToken(final String token) { try { - JwtParser jwtParser = getJwtParser(); + final JwtParser jwtParser = getJwtParser(); return jwtParser.parseClaimsJws(token); - } - //todo: 추후 수정 -// } catch (ExpiredJwtException e) { -// throw new Ex -// } catch (UnsupportedJwtException e) { -// throw new UnauthorizedException(FailureCode.UNSUPPORTED_TOKEN_TYPE); -// } catch (MalformedJwtException e) { -// throw new UnauthorizedException(FailureCode.MALFORMED_TOKEN); -// } catch (SignatureException e) { -// throw new UnauthorizedException(FailureCode.INVALID_SIGNATURE_TOKEN); - catch (Exception e) { - throw new JwtException(e.getMessage()); + } catch (ExpiredJwtException e) { ///만료된 jwt 예외처리 + throw new AuthExpiredJwtException(); + } catch (UnsupportedJwtException | MalformedJwtException | SecurityException | IllegalArgumentException e) { ///잘못된 jwt 예외처리 + throw new AuthWrongJwtException(); } } - public JwtParser getJwtParser() { + private JwtParser getJwtParser() { return Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build(); diff --git a/cakey-auth/src/main/java/com/cakey/jwt/auth/JwtProvider.java b/cakey-auth/src/main/java/com/cakey/jwt/auth/JwtProvider.java index f673b07..2c86697 100644 --- a/cakey-auth/src/main/java/com/cakey/jwt/auth/JwtProvider.java +++ b/cakey-auth/src/main/java/com/cakey/jwt/auth/JwtProvider.java @@ -1,20 +1,18 @@ package com.cakey.jwt.auth; import com.cakey.Constants; -import com.cakey.exception.CakeyBaseException; +import com.cakey.exception.AuthRTCacheException; +import com.cakey.exception.AuthWrongJwtException; import com.cakey.jwt.domain.Token; -import com.cakey.rescode.ErrorBaseCode; -import com.cakey.rescode.ErrorCode; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jws; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; @RequiredArgsConstructor @Component +@Slf4j public class JwtProvider { private final JwtGenerator jwtGenerator; @@ -33,26 +31,31 @@ public String generateRefreshToken(final long userId) { return jwtGenerator.generateRefreshToken(userId); } - @CacheEvict(value = "refresh") + //RT 캐시에서 삭제 + @CacheEvict(value = Constants.REFRESH_TOKEN, key = "#userId") public void deleteRefreshToken(final long userId) { } + //RT 캐시에서 조회 + @Cacheable(value = Constants.REFRESH_TOKEN, key = "#userId") + public String findRTFromCache(final long userId) { + log.error("--------No RT In Cache --------"); + log.error("userId = {}", userId); + log.error("------------------------------"); + throw new AuthRTCacheException(); ///아무 값이 없으면 예외 던지기 + } + + //jwtSubject에서 userId추출 public long getUserIdFromSubject(final String token) { - Jws jws = jwtGenerator.parseToken(token); - String subject = jws.getBody().getSubject(); + final String subject = jwtGenerator.parseToken(token) + .getBody() + .getSubject(); //subject가 숫자문자열인지 예외처리 try { return Long.parseLong(subject); } catch (NumberFormatException e) { - throw new IllegalArgumentException(String.valueOf(ErrorBaseCode.INTERNAL_SERVER_ERROR)); //todo: 추후 변경 - } - } - - public String getAccessToken(final HttpServletRequest request) throws Exception { - final String accessToken = request.getHeader(Constants.AUTHORIZATION); - if (StringUtils.hasText(accessToken) && accessToken.startsWith(Constants.BEARER)) { - return accessToken.substring(Constants.BEARER.length()); + log.error("---------JWT NumberFormatException-------------" + e.getMessage() + e.getCause().toString()); + throw new AuthWrongJwtException(); } - throw new Exception(); } } \ No newline at end of file diff --git a/cakey-common/src/main/java/com/cakey/rescode/ErrorBaseCode.java b/cakey-common/src/main/java/com/cakey/rescode/ErrorBaseCode.java index 97851cd..6001dba 100644 --- a/cakey-common/src/main/java/com/cakey/rescode/ErrorBaseCode.java +++ b/cakey-common/src/main/java/com/cakey/rescode/ErrorBaseCode.java @@ -22,6 +22,11 @@ public enum ErrorBaseCode implements ErrorCode { * 401 Unauthorized */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 40100, "리소스 접근 인증 권한이 없습니다."), + UNAUTHORIZED_WRONG_AT(HttpStatus.UNAUTHORIZED, 40101, "잘못된 액세스 토큰입니다."), + UNAUTHORIZED_WRONG_RT(HttpStatus.UNAUTHORIZED, 40102, "잘못된 리프레시 토큰입니다."), + UNAUTHORIZED_AT_EXPIRED(HttpStatus.UNAUTHORIZED, 40103, "만료된 액세스 토큰입니다."), + UNAUTHORIZED_RT_EXPIRED(HttpStatus.UNAUTHORIZED, 40104, "만료된 리프레시 토큰입니다."), + UNAUTHORIZED_DIFF_USER_ID(HttpStatus.UNAUTHORIZED, 40105, "리프레시 토큰 userId와 다른 userId 입니다"), /** * 403 Forbidden