From c69c4d794fd98ecd33bcb69cfe6e9709037d0b57 Mon Sep 17 00:00:00 2001 From: seungzzok <123801984+seungzzok@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:17:42 +0900 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20build=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 의존성 변경 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d7d8ff7..7b13745 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ dependencies { // MySQL implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'com.mysql:mysql-connector-j' // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' From be910ae188ec4b326c78dc2442d46c644930c777 Mon Sep 17 00:00:00 2001 From: seungzzok <123801984+seungzzok@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:20:24 +0900 Subject: [PATCH 2/6] =?UTF-8?q?chore:=20System.out.println=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../writon/admin/domain/controller/OrganizationController.java | 3 --- .../admin/global/config/auth/JwtAuthenticationEntryPoint.java | 1 - .../java/com/writon/admin/global/config/auth/JwtFilter.java | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/com/writon/admin/domain/controller/OrganizationController.java b/src/main/java/com/writon/admin/domain/controller/OrganizationController.java index 505dc97..88ccaa7 100644 --- a/src/main/java/com/writon/admin/domain/controller/OrganizationController.java +++ b/src/main/java/com/writon/admin/domain/controller/OrganizationController.java @@ -91,13 +91,10 @@ public SuccessDto> editPositions( private void deleteImage() { Organization organization = tokenUtil.getOrganization(); String imageUrl = organization.getLogo(); - System.out.println("deleteImage 함수 실행" + imageUrl); if (!Objects.equals(imageUrl, "") && !Objects.equals(imageUrl, DEFAULT_LOGO_URL)) { imageService.deleteImage(imageUrl); - System.out.println("deleteImage 실행" + imageUrl); - } } } diff --git a/src/main/java/com/writon/admin/global/config/auth/JwtAuthenticationEntryPoint.java b/src/main/java/com/writon/admin/global/config/auth/JwtAuthenticationEntryPoint.java index d21b980..2e75441 100644 --- a/src/main/java/com/writon/admin/global/config/auth/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/writon/admin/global/config/auth/JwtAuthenticationEntryPoint.java @@ -24,7 +24,6 @@ public void commence( HttpServletResponse response, AuthenticationException authException ) throws IOException { -// System.out.println("JwtAuthenticationEntryPoint"); String exception = (String) request.getAttribute("exception"); ErrorCode errorCode = ErrorCode.UNAUTHORIZED; diff --git a/src/main/java/com/writon/admin/global/config/auth/JwtFilter.java b/src/main/java/com/writon/admin/global/config/auth/JwtFilter.java index 5e86e9c..d2c0b34 100644 --- a/src/main/java/com/writon/admin/global/config/auth/JwtFilter.java +++ b/src/main/java/com/writon/admin/global/config/auth/JwtFilter.java @@ -49,7 +49,7 @@ protected void doFilterInternal( // 2. 토큰의 존재여부 & accessToken 유효성 검사 if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt, request)) { - System.out.println("JWT Token 검증 통과"); + exceptionResponseHandler.setResponse(response, ErrorCode.ACCESS_TOKEN_NOT_FOUND); // 3. 로그아웃 유저 확인 (access: O, refresh: X) Claims identifier = tokenProvider.getIdentifier(jwt); From c33031b54bc6bade5538b97fb3ac321a9ae41e5e Mon Sep 17 00:00:00 2001 From: seungzzok <123801984+seungzzok@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:31:28 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20Token=20=EC=BF=A0=ED=82=A4=EB=A1=9C?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CookieProvider 파일로 쿠키 생성, 토큰 쿠키 생성 메서드 제작 - 기존의 Bearer로 JWT를 관리하는 로직을 전부 쿠키 로직으로 변경 - Cookie class를 사용해 쿠키로부터 토큰 추출 메서드 제작 - SuccessDto 헤더에 쿠키를 주입할 수 있도록 변경 - 토큰 재발급 API에서 refreshToken없이 accessToken만 재발급 후 전달 - 로그아웃 시 빈 쿠키를 전달하도록 작성 (쿠키는 httpOnly 옵션을 가지고 있으므로 백에서만 접근가능) --- .../domain/controller/AuthController.java | 38 +++++++-- .../dto/response/auth/LoginResponseDto.java | 4 - .../dto/response/auth/ReissueResponseDto.java | 5 +- .../wrapper/auth/LoginResponseWrapper.java | 13 +++ .../admin/domain/service/AuthService.java | 26 +++--- .../global/config/auth/CookieProvider.java | 82 +++++++++++++++++++ .../admin/global/config/auth/JwtFilter.java | 40 +++++---- .../global/config/auth/TokenProvider.java | 6 +- .../admin/global/response/SuccessDto.java | 9 ++ 9 files changed, 176 insertions(+), 47 deletions(-) create mode 100644 src/main/java/com/writon/admin/domain/dto/wrapper/auth/LoginResponseWrapper.java create mode 100644 src/main/java/com/writon/admin/global/config/auth/CookieProvider.java diff --git a/src/main/java/com/writon/admin/domain/controller/AuthController.java b/src/main/java/com/writon/admin/domain/controller/AuthController.java index b5d5995..c274240 100644 --- a/src/main/java/com/writon/admin/domain/controller/AuthController.java +++ b/src/main/java/com/writon/admin/domain/controller/AuthController.java @@ -6,9 +6,15 @@ import com.writon.admin.domain.dto.response.auth.LoginResponseDto; import com.writon.admin.domain.dto.response.auth.ReissueResponseDto; import com.writon.admin.domain.dto.response.auth.SignUpResponseDto; +import com.writon.admin.domain.dto.wrapper.auth.LoginResponseWrapper; import com.writon.admin.domain.service.AuthService; +import com.writon.admin.global.config.auth.CookieProvider; import com.writon.admin.global.response.SuccessDto; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -21,7 +27,9 @@ public class AuthController { private final AuthService authService; + private final CookieProvider cookieProvider; + // ========== 회원가입 API ========== @PostMapping("/signup") public SuccessDto signup(@RequestBody SignUpRequestDto signUpRequestDto) { SignUpResponseDto signUpResponseDto = authService.signup(signUpRequestDto); @@ -29,23 +37,39 @@ public SuccessDto signup(@RequestBody SignUpRequestDto signUp return new SuccessDto<>(signUpResponseDto); } + + // ========== 로그인 API ========== @PostMapping("/login") - public SuccessDto login(@RequestBody LoginRequestDto loginRequestDto) { - LoginResponseDto loginResponseDto = authService.login(loginRequestDto); + public ResponseEntity> login(@RequestBody LoginRequestDto loginRequestDto) { + LoginResponseWrapper loginResponseWrapper = authService.login(loginRequestDto); + HttpHeaders cookieHeaders = cookieProvider.createTokenCookie(loginResponseWrapper.getTokenDto()); - return new SuccessDto<>(loginResponseDto); + return new SuccessDto<>(loginResponseWrapper.getLoginResponseDto()).toResponseEntity( + cookieHeaders); } + + // ========== 토큰 재발급 API ========== @PostMapping("/reissue") - public SuccessDto reissue(@RequestBody ReissueRequestDto reissueRequestDto) { - ReissueResponseDto reissueResponseDto = authService.reissue(reissueRequestDto); + public ResponseEntity> reissue( + @CookieValue(value = "accessToken", required = false) String accessToken, + @CookieValue(value = "refreshToken", required = false) String refreshToken + ) { + String newAccessToken = authService.reissue(accessToken, refreshToken); + HttpHeaders cookieHeaders = cookieProvider.createTokenCookie(newAccessToken); - return new SuccessDto<>(reissueResponseDto); + return new SuccessDto<>().toResponseEntity(cookieHeaders); } + + // ========== 로그아웃 API ========== @DeleteMapping("/logout") - public SuccessDto logout() { + public ResponseEntity> logout() { authService.logout(); + HttpHeaders cookieHeaders = cookieProvider.removeTokenCookie(); + + return new SuccessDto<>().toResponseEntity(cookieHeaders); + } return new SuccessDto<>(); } diff --git a/src/main/java/com/writon/admin/domain/dto/response/auth/LoginResponseDto.java b/src/main/java/com/writon/admin/domain/dto/response/auth/LoginResponseDto.java index 6c649b3..576ee6c 100644 --- a/src/main/java/com/writon/admin/domain/dto/response/auth/LoginResponseDto.java +++ b/src/main/java/com/writon/admin/domain/dto/response/auth/LoginResponseDto.java @@ -9,14 +9,10 @@ @Getter @AllArgsConstructor public class LoginResponseDto { - - private String accessToken; - private String refreshToken; private boolean hasOrganization; private Long organizationId; // nullable private String organizationName; // nullable private String themeColor; // nullable private String organizationLogo; // nullable private List challengeList; - } diff --git a/src/main/java/com/writon/admin/domain/dto/response/auth/ReissueResponseDto.java b/src/main/java/com/writon/admin/domain/dto/response/auth/ReissueResponseDto.java index faffd95..75e6c2d 100644 --- a/src/main/java/com/writon/admin/domain/dto/response/auth/ReissueResponseDto.java +++ b/src/main/java/com/writon/admin/domain/dto/response/auth/ReissueResponseDto.java @@ -1,12 +1,11 @@ package com.writon.admin.domain.dto.response.auth; +import com.writon.admin.global.config.auth.TokenDto; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor public class ReissueResponseDto { - - private String accessToken; - private String refreshToken; + private TokenDto tokenDto; } diff --git a/src/main/java/com/writon/admin/domain/dto/wrapper/auth/LoginResponseWrapper.java b/src/main/java/com/writon/admin/domain/dto/wrapper/auth/LoginResponseWrapper.java new file mode 100644 index 0000000..e72a951 --- /dev/null +++ b/src/main/java/com/writon/admin/domain/dto/wrapper/auth/LoginResponseWrapper.java @@ -0,0 +1,13 @@ +package com.writon.admin.domain.dto.wrapper.auth; + +import com.writon.admin.domain.dto.response.auth.LoginResponseDto; +import com.writon.admin.global.config.auth.TokenDto; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LoginResponseWrapper { + private TokenDto tokenDto; + private LoginResponseDto loginResponseDto; +} diff --git a/src/main/java/com/writon/admin/domain/service/AuthService.java b/src/main/java/com/writon/admin/domain/service/AuthService.java index 32ed244..1498659 100644 --- a/src/main/java/com/writon/admin/domain/service/AuthService.java +++ b/src/main/java/com/writon/admin/domain/service/AuthService.java @@ -6,6 +6,7 @@ import com.writon.admin.domain.dto.response.auth.LoginResponseDto; import com.writon.admin.domain.dto.response.auth.ReissueResponseDto; import com.writon.admin.domain.dto.response.auth.SignUpResponseDto; +import com.writon.admin.domain.dto.wrapper.auth.LoginResponseWrapper; import com.writon.admin.domain.entity.challenge.Challenge; import com.writon.admin.domain.entity.lcoal.ChallengeResponse; import com.writon.admin.domain.entity.organization.AdminUser; @@ -17,6 +18,7 @@ import com.writon.admin.global.config.auth.TokenProvider; import com.writon.admin.global.error.CustomException; import com.writon.admin.global.error.ErrorCode; +import jakarta.servlet.http.HttpServletRequest; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -59,7 +61,7 @@ public SignUpResponseDto signup(SignUpRequestDto signUpRequestDto) { } // ========== Login API ========== - public LoginResponseDto login(LoginRequestDto loginRequestDto) { + public LoginResponseWrapper login(LoginRequestDto loginRequestDto) { // 1. Login ID/PW 를 기반으로 AuthenticationToken 생성 UsernamePasswordAuthenticationToken authenticationToken = loginRequestDto.toAuthentication(); @@ -103,10 +105,7 @@ public LoginResponseDto login(LoginRequestDto loginRequestDto) { .map(entity -> new ChallengeResponse(entity.getId(), entity.getName())) .toList(); - // 8. Response 전달 - return new LoginResponseDto( - tokenDto.getAccessToken(), - tokenDto.getRefreshToken(), + LoginResponseDto loginResponseDto = new LoginResponseDto( organization.isPresent(), organization.map(Organization::getId).orElse(null), organization.map(Organization::getName).orElse(null), @@ -114,26 +113,27 @@ public LoginResponseDto login(LoginRequestDto loginRequestDto) { organization.map(Organization::getLogo).orElse(null), challengeList ); + + // 8. Response 전달 + return new LoginResponseWrapper(tokenDto, loginResponseDto); } // ========== Reissue API ========== - public ReissueResponseDto reissue(ReissueRequestDto reissueRequestDto) { + public String reissue(String accessToken, String refreshToken) { // 1. Access Token 에서 identifier 가져오기 - String identifier = tokenProvider.getIdentifier(reissueRequestDto.getAccessToken()).getSubject(); + String identifier = tokenProvider.getIdentifier(accessToken) + .getSubject(); // 2. Refresh Token 일치여부 확인 - String refreshToken = refreshTokenService.getRefreshToken(identifier); + String storedRefreshToken = refreshTokenService.getRefreshToken(identifier); - if (refreshToken == null || !refreshToken.equals(reissueRequestDto.getRefreshToken())) { + if (refreshToken == null || !refreshToken.equals(storedRefreshToken)) { throw new CustomException(ErrorCode.REFRESH_TOKEN_INCONSISTENCY); } // 3. 새로운 Access Token 생성 - String accessToken = tokenProvider.createAccessToken(identifier); - - // 4. 토큰 발급 - return new ReissueResponseDto(accessToken, refreshToken); + return tokenProvider.createAccessToken(identifier); } // ========== Logout API ========== diff --git a/src/main/java/com/writon/admin/global/config/auth/CookieProvider.java b/src/main/java/com/writon/admin/global/config/auth/CookieProvider.java new file mode 100644 index 0000000..51c95d2 --- /dev/null +++ b/src/main/java/com/writon/admin/global/config/auth/CookieProvider.java @@ -0,0 +1,82 @@ +package com.writon.admin.global.config.auth; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class CookieProvider { + + private static final long ACCESS_TOKEN_MAX_AGE = 60 * (60 + 30) ; // 1시간 30분 (단위: s) + private static final long REFRESH_TOKEN_MAX_AGE = 60 * 60 * 24; // 1일 (단위: s) + + // AT, RT 쿠키헤더를 생성하는 메서드 + public HttpHeaders createTokenCookie(TokenDto tokenDto) { + ResponseCookie accessTokenCookie = createCookie( + "accessToken", + tokenDto.getAccessToken(), + ACCESS_TOKEN_MAX_AGE + ); + ResponseCookie refreshTokenCookie = createCookie( + "refreshToken", + tokenDto.getRefreshToken(), + REFRESH_TOKEN_MAX_AGE + ); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()); + headers.add(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); + + return headers; + } + + + // AT 쿠키헤더만 생성하는 메서드 + public HttpHeaders createTokenCookie(String accessToken) { + ResponseCookie accessTokenCookie = createCookie( + "accessToken", + accessToken, + ACCESS_TOKEN_MAX_AGE + ); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()); + + return headers; + } + + + // AT, RT 쿠키헤더를 제거하는 메서드 + public HttpHeaders removeTokenCookie() { + ResponseCookie accessTokenCookie = createCookie( + "accessToken", + "", + 0 + ); + ResponseCookie refreshTokenCookie = createCookie( + "refreshToken", + "", + 0 + ); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()); + headers.add(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); + + return headers; + } + + + // Cookie를 생성하는 메서드 + private ResponseCookie createCookie(String name, String value, long maxAge) { + return ResponseCookie.from(name, value) + .httpOnly(true) // HttpOnly 속성 적용 + .secure(true) // HTTPS에서만 전송 (로컬 테스트 시 false 설정) + .path("/") // 모든 경로에서 사용 가능하도록 설정 + .sameSite("Strict") // CSRF 방지 + .maxAge(maxAge) // 쿠키 만료 시간 설정 + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/writon/admin/global/config/auth/JwtFilter.java b/src/main/java/com/writon/admin/global/config/auth/JwtFilter.java index d2c0b34..018d890 100644 --- a/src/main/java/com/writon/admin/global/config/auth/JwtFilter.java +++ b/src/main/java/com/writon/admin/global/config/auth/JwtFilter.java @@ -5,6 +5,7 @@ import io.jsonwebtoken.Claims; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; @@ -20,16 +21,13 @@ @RequiredArgsConstructor public class JwtFilter extends OncePerRequestFilter { - public static final String AUTHORIZATION_HEADER = "Authorization"; - public static final String BEARER_PREFIX = "Bearer "; - private final TokenProvider tokenProvider; private final RedisTemplate redisTemplate; private final ExceptionResponseHandler exceptionResponseHandler = new ExceptionResponseHandler(); private final List excludedPaths = Arrays.asList("/auth/login","/auth/signup"); @Override - protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException{ + protected boolean shouldNotFilter(HttpServletRequest request) { // 로그인, 회원가입 API URL이 포함되는지 확인하는 함수 String path = request.getRequestURI(); return excludedPaths.stream().anyMatch(path::equals); @@ -45,21 +43,26 @@ protected void doFilterInternal( ) throws IOException, ServletException { // 1. Request Header 에서 토큰을 꺼냄 - String jwt = resolveToken(request); + String accessToken = extractTokenFromCookie(request, "accessToken"); + String refreshToken = extractTokenFromCookie(request, "refreshToken"); - // 2. 토큰의 존재여부 & accessToken 유효성 검사 - if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt, request)) { + // 2. 토큰의 존재여부 검사 + if (!StringUtils.hasText(accessToken)){ exceptionResponseHandler.setResponse(response, ErrorCode.ACCESS_TOKEN_NOT_FOUND); + return; + + // 3. accessToken 유효성 검사 + } else if (tokenProvider.validateToken(accessToken, request)) { - // 3. 로그아웃 유저 확인 (access: O, refresh: X) - Claims identifier = tokenProvider.getIdentifier(jwt); + // 4. 장시간 사용자 확인 (access: O, refresh: X) + Claims identifier = tokenProvider.getIdentifier(accessToken); - if (redisTemplate.opsForValue().get(identifier.getSubject()) == null) { + if (refreshToken == null || redisTemplate.opsForValue().get(identifier.getSubject()) == null) { exceptionResponseHandler.setResponse(response, ErrorCode.REFRESH_TOKEN_EXPIRATION); return; } - // 4. 정상적인 인증확인 (access: O, refresh: O) + // 5. 정상적인 인증확인 (access: O, refresh: O) Authentication authentication = tokenProvider.getAuthentication(identifier); SecurityContextHolder.getContext().setAuthentication(authentication); } @@ -67,11 +70,16 @@ protected void doFilterInternal( filterChain.doFilter(request, response); } - // Request Header 에서 토큰 정보 추출 - private String resolveToken(HttpServletRequest request) { - String bearerToken = request.getHeader(AUTHORIZATION_HEADER); - if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) { - return bearerToken.split(" ")[1].trim(); + + // Cookie로부터 Token을 추출하는 메서드 + public String extractTokenFromCookie(HttpServletRequest request, String tokenName) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (tokenName.equals(cookie.getName())) { + return cookie.getValue(); + } + } } return null; } diff --git a/src/main/java/com/writon/admin/global/config/auth/TokenProvider.java b/src/main/java/com/writon/admin/global/config/auth/TokenProvider.java index 6ab6a6a..b53db77 100644 --- a/src/main/java/com/writon/admin/global/config/auth/TokenProvider.java +++ b/src/main/java/com/writon/admin/global/config/auth/TokenProvider.java @@ -44,10 +44,8 @@ public TokenProvider(@Value("${jwt.secret}") String secret) { // 유저 정보를 가지고 AccessToken, RefreshToken 을 생성하는 메서드 public TokenDto createToken(String identifier) { - // AccessToken 생성 + // Token 생성 String accessToken = createAccessToken(identifier); - - // RefreshToken 생성 String refreshToken = createRefreshToken(); return TokenDto.builder() @@ -129,7 +127,7 @@ public boolean validateToken(String token, HttpServletRequest request) { log.info("잘못된 JWT 서명입니다."); } catch (ExpiredJwtException e) { log.info("만료된 JWT 토큰입니다."); - // 토큰 재발급의 경우 유효성 통과해서 로직 실행하도록 설정 + // 토큰 재발급, 로그아웃의 경우 유효성 통과해서 로직 실행하도록 설정 if (request.getRequestURI().equals("/auth/reissue") || request.getRequestURI().equals("/auth/logout")) { return true; } else { diff --git a/src/main/java/com/writon/admin/global/response/SuccessDto.java b/src/main/java/com/writon/admin/global/response/SuccessDto.java index 6fb98b5..8b6e4d6 100644 --- a/src/main/java/com/writon/admin/global/response/SuccessDto.java +++ b/src/main/java/com/writon/admin/global/response/SuccessDto.java @@ -1,6 +1,9 @@ package com.writon.admin.global.response; import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; @Getter public class SuccessDto { @@ -22,4 +25,10 @@ public SuccessDto(T data) { this.message = "success"; this.data = data; } + + public ResponseEntity> toResponseEntity(HttpHeaders headers) { + return ResponseEntity.status(HttpStatus.OK) + .headers(headers) + .body(this); + } } From f746e6f6549bf90b63e8853dea329be9892bcbac Mon Sep 17 00:00:00 2001 From: seungzzok <123801984+seungzzok@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:32:54 +0900 Subject: [PATCH 4/6] feat: Get AuthCheck API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Home에 접근 시 쿠키의 유효성 검사를 위해 만든 api - 단순히 cookie만 확인한 뒤 응답값을 전달 --- .../com/writon/admin/domain/controller/AuthController.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/writon/admin/domain/controller/AuthController.java b/src/main/java/com/writon/admin/domain/controller/AuthController.java index c274240..90ccff6 100644 --- a/src/main/java/com/writon/admin/domain/controller/AuthController.java +++ b/src/main/java/com/writon/admin/domain/controller/AuthController.java @@ -16,6 +16,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -71,7 +72,11 @@ public ResponseEntity> logout() { return new SuccessDto<>().toResponseEntity(cookieHeaders); } + + // ========== 토큰 유효성 검사 API ========== + @GetMapping("/check") + public SuccessDto tokenCheck() { + return new SuccessDto<>(); } - } From e639d6cad10f2e311ce356859ed9f0dfb83c7f5b Mon Sep 17 00:00:00 2001 From: seungzzok <123801984+seungzzok@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:35:10 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20Token=20Expire=20Time=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AccessToken : 10분 -> 1시간 - RefreshToken : 2시간 -> 1일(24시간) - AccessToken Cookie : 1시간 반 (토큰 재발급 때문에 30분 추가하여 설정) - RefreshToken Cookie : 1일(24시간) --- .../com/writon/admin/domain/service/RefreshTokenService.java | 2 +- .../com/writon/admin/global/config/auth/TokenProvider.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/writon/admin/domain/service/RefreshTokenService.java b/src/main/java/com/writon/admin/domain/service/RefreshTokenService.java index 26463ae..52bd6fe 100644 --- a/src/main/java/com/writon/admin/domain/service/RefreshTokenService.java +++ b/src/main/java/com/writon/admin/domain/service/RefreshTokenService.java @@ -9,7 +9,7 @@ @AllArgsConstructor public class RefreshTokenService { private final RedisTemplate redisTemplate; - private final Long REFRESH_TOKEN_EXPIRE_TIME = 60 * 2L; // TTL: 2시간 + private final Long REFRESH_TOKEN_EXPIRE_TIME = 60 * 24L; // TTL: 1일(24시간) public void saveRefreshToken(String email, String refreshToken) { redisTemplate.opsForValue().set(email, refreshToken, REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MINUTES); // 이메일을 key로 저장 diff --git a/src/main/java/com/writon/admin/global/config/auth/TokenProvider.java b/src/main/java/com/writon/admin/global/config/auth/TokenProvider.java index b53db77..66746ff 100644 --- a/src/main/java/com/writon/admin/global/config/auth/TokenProvider.java +++ b/src/main/java/com/writon/admin/global/config/auth/TokenProvider.java @@ -30,8 +30,8 @@ public class TokenProvider { private static final String AUTHORITIES_KEY = "auth"; - private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 10; // 10분 - private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 2; // 2시간 + private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 60; // 1시간 (단위: ms) + private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24; // 1일(24시간) (단위: ms) private final Key key; From 8e1f88a92cf34a21abf03ec082c9cb90c62624e1 Mon Sep 17 00:00:00 2001 From: seungzzok <123801984+seungzzok@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:36:00 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20ErrorCode=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cookie의 만료시간 설정에 따라 토큰 없이 요청이 들어올 수 있으므로 ACCESS_TOKEN_NOT_FOUND 설정 - 에러코드의 순서 변경 --- .../config/auth/JwtAuthenticationEntryPoint.java | 4 ++++ .../com/writon/admin/global/error/ErrorCode.java | 15 ++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/writon/admin/global/config/auth/JwtAuthenticationEntryPoint.java b/src/main/java/com/writon/admin/global/config/auth/JwtAuthenticationEntryPoint.java index 2e75441..642ad39 100644 --- a/src/main/java/com/writon/admin/global/config/auth/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/writon/admin/global/config/auth/JwtAuthenticationEntryPoint.java @@ -32,6 +32,10 @@ public void commence( errorCode = ErrorCode.ACCESS_TOKEN_EXPIRATION; } + if (exception.equals(ErrorCode.ACCESS_TOKEN_NOT_FOUND.getCode())) { + errorCode = ErrorCode.ACCESS_TOKEN_NOT_FOUND; + } + if (exception.equals(ErrorCode.REFRESH_TOKEN_EXPIRATION.getCode())) { errorCode = ErrorCode.REFRESH_TOKEN_EXPIRATION; } diff --git a/src/main/java/com/writon/admin/global/error/ErrorCode.java b/src/main/java/com/writon/admin/global/error/ErrorCode.java index f4f3e30..51718ad 100644 --- a/src/main/java/com/writon/admin/global/error/ErrorCode.java +++ b/src/main/java/com/writon/admin/global/error/ErrorCode.java @@ -21,13 +21,14 @@ public enum ErrorCode { // auth USER_NOT_FOUND(HttpStatus.NOT_FOUND, "A01", "사용자를 찾을 수 없습니다"), - UNAUTHORIZED_TOKEN(HttpStatus.UNAUTHORIZED, "A02", "권한이 없는 토큰입니다"), - REFRESH_TOKEN_EXPIRATION(HttpStatus.UNAUTHORIZED, "A03", "만료된 토큰입니다"), - ACCESS_TOKEN_EXPIRATION(HttpStatus.UNAUTHORIZED, "A04", "토큰 재발급을 요청해주세요"), - REFRESH_TOKEN_INCONSISTENCY(HttpStatus.NOT_FOUND, "A05", "토큰이 일치하지 않습니다"), - BAD_CREDENTIAL_ACCESS(HttpStatus.BAD_REQUEST, "A06", "아이디 혹은 비밀번호가 잘못되었습니다"), - DISABLED_USER(HttpStatus.NOT_FOUND, "A07", "비활성화된 계정입니다"), - LOCKED_USER(HttpStatus.NOT_FOUND, "A08", "계정이 잠겨 있습니다"), + BAD_CREDENTIAL_ACCESS(HttpStatus.BAD_REQUEST, "A02", "비밀번호가 잘못되었습니다"), + UNAUTHORIZED_TOKEN(HttpStatus.UNAUTHORIZED, "A03", "권한이 없는 토큰입니다"), + ACCESS_TOKEN_EXPIRATION(HttpStatus.UNAUTHORIZED, "A04", "AccessToken이 만료되었습니다"), + ACCESS_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "A05", "AccessToken이 존재하지 않습니다"), + REFRESH_TOKEN_EXPIRATION(HttpStatus.UNAUTHORIZED, "A06", "RefreshToken이 만료되었습니다"), + REFRESH_TOKEN_INCONSISTENCY(HttpStatus.NOT_FOUND, "A07", "RefreshToken이 일치하지 않습니다"), + DISABLED_USER(HttpStatus.NOT_FOUND, "A08", "비활성화된 계정입니다"), + LOCKED_USER(HttpStatus.NOT_FOUND, "A09", "계정이 잠겨 있습니다"), // organization ORGANIZATION_NOT_FOUND(HttpStatus.NOT_FOUND, "O01", "조직 정보를 찾을 수 없습니다"),