diff --git a/src/main/java/com/petmatz/api/global/exception/GlobalExceptionHandler.java b/src/main/java/com/petmatz/api/global/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..4419aa1 --- /dev/null +++ b/src/main/java/com/petmatz/api/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,32 @@ +package com.petmatz.api.global.exception; + +import com.petmatz.api.global.dto.Response; +import com.petmatz.common.exception.BaseErrorCode; +import com.petmatz.common.exception.ErrorReason; +import com.petmatz.domain.user.exception.UserErrorCode; +import com.petmatz.domain.user.exception.UserException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(UserException.class) + public ResponseEntity> handleUserException(UserException ex) { + // 예외에서 에러 코드 가져오기 + BaseErrorCode baseErrorCode = ex.getErrorCode(); + ErrorReason errorReason = baseErrorCode.getErrorReason(); + + int statusCode = errorReason.status(); + String message = errorReason.message(); + String errorCode2 = errorReason.errorCode(); + + // 실패 응답 생성 + Response response = Response.error(errorCode2, message); + + // 상태 코드와 함께 응답 반환 + return new ResponseEntity<>(response, HttpStatus.valueOf(statusCode)); + } +} diff --git a/src/main/java/com/petmatz/api/user/controller/JwtController.java b/src/main/java/com/petmatz/api/user/controller/JwtController.java new file mode 100644 index 0000000..ee36ce6 --- /dev/null +++ b/src/main/java/com/petmatz/api/user/controller/JwtController.java @@ -0,0 +1,24 @@ +package com.petmatz.api.user.controller; + +import com.petmatz.api.global.dto.Response; +import com.petmatz.common.security.jwt.JwtManager; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class JwtController { + + private final JwtManager jwtManager; + + @PostMapping("/token/reissue") + public Response reissueAccessToken(HttpServletResponse response, String refreshToken) { + jwtManager.refreshAccessToken(response, refreshToken); + return Response.success(); + } +} diff --git a/src/main/java/com/petmatz/api/user/controller/PastUserController.java b/src/main/java/com/petmatz/api/user/controller/PastUserController.java deleted file mode 100644 index f0cd8ad..0000000 --- a/src/main/java/com/petmatz/api/user/controller/PastUserController.java +++ /dev/null @@ -1,149 +0,0 @@ -//package com.petmatz.api.user.controller; -// -//import com.petmatz.api.user.request.*; -//import com.petmatz.domain.user.service.AuthService; -//import com.petmatz.domain.user.response.*; -//import com.petmatz.domain.user.service.UserService; -//import com.petmatz.user.common.LogInResponseDto; -//import jakarta.servlet.http.HttpServletResponse; -//import jakarta.validation.Valid; -//import lombok.RequiredArgsConstructor; -//import lombok.extern.slf4j.Slf4j; -//import org.springframework.http.ResponseEntity; -//import org.springframework.web.bind.annotation.*; -// -//import java.net.MalformedURLException; -// -//@Slf4j -//@RestController -//@RequiredArgsConstructor -//@RequestMapping("/api/auth") -//public class PastUserController { -// private final UserService userService; -// private final AuthService authService; -// -// @PostMapping("/email-certification") -// public ResponseEntity emailCertification(@RequestBody @Valid EmailCertificationRequestDto requestBody) { -// ResponseEntity response = userService.emailCertification(requestBody); -// log.info("[emailCertification]: { accountId: " + requestBody.getAccountId() + "}"); -// return response; -// } // 하나 했음 -// -// @PostMapping("/check-certification") -// public ResponseEntity checkCertification(@RequestBody @Valid CheckCertificationRequestDto requestBody) { -// ResponseEntity response = userService.checkCertification(CheckCertificationRequestDto.of(requestBody)); -// log.info("[checkCertification]: {accountId: " + requestBody.getAccountId() + ", certificationNumber: " + requestBody.getCertificationNumber() + "}"); -// return response; -// } // 했음 -// -// @PostMapping("/sign-up") -// public ResponseEntity signUp(@RequestBody @Valid SignUpRequestDto requestBody) throws MalformedURLException { -// SignUpResponseDto responseDto = authService.signUp(SignUpRequestDto.of(requestBody)); -// log.info("[signUp]: { accountId: " + requestBody.getAccountId() + ", password: " + requestBody.getPassword()); -// return ResponseEntity.ok(responseDto); -// } // o -// -// @PostMapping("/sign-in") -// public ResponseEntity signIn( -// @RequestBody @Valid SignInRequestDto requestBody, -// HttpServletResponse response) { -// -// ResponseEntity result = userService.signIn(SignInRequestDto.of(requestBody), response); -// log.info("[signIn]: { accountId: " + requestBody.getAccountId() + ", result: " + result.getStatusCode() + " }"); -// return result; -// } // o -// -// @PostMapping("/delete-user") -// public ResponseEntity deleteUser(@RequestBody @Valid DeleteIdRequestDto requestBody) { -// ResponseEntity response = userService.deleteId(requestBody); -// log.info("[deleteUser]:{password: " + requestBody.getPassword() + "}"); -// return response; -// } // o -// -// //---------------------------------------------------------------------------------------------------------------------------------// -// @GetMapping("/get-myprofile") -// public ResponseEntity getMypage() { -// ResponseEntity response = userService.getMypage(); -// log.info("[getMypage]"); -// return response; -// } // o -// -// @GetMapping("/get-otherprofile") -// public ResponseEntity getOtherMypage(@RequestParam @Valid Long userId) { -// ResponseEntity response = userService.getOtherMypage(userId); -// log.info("[getOtherMypage]"); -// return response; -// } // o -// -// @PostMapping("/edit-myprofile") -// public ResponseEntity editMyProfile(@RequestBody @Valid EditMyProfileRequestDto requestBody) { -// ResponseEntity response = userService.editMyProfile(EditMyProfileRequestDto.of(requestBody)); -// log.info("[editMyProfile]"); -// return response; -// } // o -// -// @PostMapping("/send-repassword") -// public ResponseEntity sendRepassword(@RequestBody @Valid SendRepasswordRequestDto requestBody) { -// ResponseEntity response = userService.sendRepassword(requestBody); -// log.info("[sendRepassword]: {accountId: " + requestBody.getAccountId() + "}"); -// return response; -// } // o -// -// @PostMapping("/repassword") -// public ResponseEntity repassword(@RequestBody @Valid RepasswordRequestDto requestBody) { -// ResponseEntity response = userService.repassword(RepasswordRequestDto.of(requestBody)); -// log.info("[repassword]: {currentPassword: " + requestBody.getCurrentPassword() + ", newPassword: " + requestBody.getNewPassword() + "}"); -// return response; -// } // o -// -// //--------------------------------------------------------------------------------------------------------------------------------------------------------------- -// @PostMapping("/hearting") -// public ResponseEntity hearting(@RequestBody @Valid HeartingRequestDto requestBody) { -// ResponseEntity response = userService.hearting(requestBody); -// log.info("[hearting]: {heartedId: " + requestBody.getHeartedId() + "}"); -// return response; -// } // o -// -// @GetMapping("/get-heartlist") -// public ResponseEntity getHeartedList() { -// ResponseEntity response = userService.getHeartedList(); -// log.info("[getHeartedList]"); -// return response; -// } // o -// -// @PostMapping("/update-location") -// public ResponseEntity updateLocation(@RequestBody @Valid UpdateLocationRequestDto requestBody) { -// ResponseEntity response = userService.updateLocation(UpdateLocationRequestDto.of(requestBody)); -// log.info("[updateLocation]"); -// return response; -// } // o -// -// @PostMapping("/update-recommendation") -// public ResponseEntity updateRecommend(@RequestBody @Valid UpdateRecommendationRequestDto requestBody) { -// ResponseEntity response = userService.updateRecommend(requestBody); -// log.info("[updateRecommend]"); -// return response; -// } -// -// @GetMapping("/get-recommended") -// public ResponseEntity getRecommend(@RequestBody @Valid UpdateRecommendationRequestDto requestBody) { -// ResponseEntity response = userService.getRecommend(requestBody); -// log.info("[getRecommend]"); -// return response; -// } -// -// @PostMapping("/logout") -// public ResponseEntity logout(HttpServletResponse res) { -// ResponseEntity response = userService.logout(res); -// log.info("[logout]"); -// return response; -// } -// -// @PostMapping("/edit-kakaoprofile") -// public ResponseEntity editKakaoProfile(@RequestBody @Valid EditKakaoProfileRequestDto requestBody) { -// ResponseEntity response = userService.editKakaoProfile(EditKakaoProfileRequestDto.of(requestBody)); -// log.info("[editKakaoProfile]"); -// return response; -// } -//} -// diff --git a/src/main/java/com/petmatz/common/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/petmatz/common/security/filter/JwtAuthenticationFilter.java index a9c0256..df183b2 100644 --- a/src/main/java/com/petmatz/common/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/petmatz/common/security/filter/JwtAuthenticationFilter.java @@ -1,5 +1,6 @@ package com.petmatz.common.security.filter; +import com.petmatz.common.security.jwt.JwtManager; import com.petmatz.common.security.jwt.JwtProvider; import com.petmatz.domain.user.constant.LoginRole; import com.petmatz.domain.user.entity.User; @@ -34,7 +35,7 @@ @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final UserRepository userRepository; - private final JwtProvider jwtProvider; + private final JwtManager jwtManager; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { @@ -48,7 +49,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } // JWT 유효성 검증 및 사용자 ID 추출 - Long userId = jwtProvider.validateAndGetUserId(token); // validate 메서드가 userId를 반환하도록 수정 + Long userId = jwtManager.validateAndGetUserId(token); // validate 메서드가 userId를 반환하도록 수정 if (userId == null) { filterChain.doFilter(request, response); return; diff --git a/src/main/java/com/petmatz/common/security/handler/OAuthSuccessHandler.java b/src/main/java/com/petmatz/common/security/handler/OAuthSuccessHandler.java index 41f65f4..d185261 100644 --- a/src/main/java/com/petmatz/common/security/handler/OAuthSuccessHandler.java +++ b/src/main/java/com/petmatz/common/security/handler/OAuthSuccessHandler.java @@ -1,5 +1,6 @@ package com.petmatz.common.security.handler; +import com.petmatz.common.security.jwt.JwtManager; import com.petmatz.common.security.jwt.JwtProvider; import com.petmatz.domain.user.entity.CustomOAuthUser; import com.petmatz.domain.user.entity.User; @@ -19,7 +20,7 @@ @RequiredArgsConstructor public class OAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { - private final JwtProvider jwtProvider; + private final JwtManager jwtManager; private final UserRepository userRepository; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { @@ -30,7 +31,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo String accountId = oAuth2User.getName(); // JWT 생성 - String token = jwtProvider.create(userId, accountId); + String token = jwtManager.createAccessToken(userId, accountId); // JWT 쿠키 설정 ResponseCookie jwtCookie = ResponseCookie.from("jwt", token) diff --git a/src/main/java/com/petmatz/common/security/jwt/JwtExtractProvider.java b/src/main/java/com/petmatz/common/security/jwt/JwtExtractProvider.java index 4e5eba2..89ec77e 100644 --- a/src/main/java/com/petmatz/common/security/jwt/JwtExtractProvider.java +++ b/src/main/java/com/petmatz/common/security/jwt/JwtExtractProvider.java @@ -3,5 +3,5 @@ public interface JwtExtractProvider { Long findIdFromJwt(); - String findAccountIdFromJwt(); + String findAccountIdFromJwt(); // Email 로도 사용 } diff --git a/src/main/java/com/petmatz/common/security/jwt/JwtExtractProviderImpl.java b/src/main/java/com/petmatz/common/security/jwt/JwtExtractProviderImpl.java index 3a8b72b..12795bc 100644 --- a/src/main/java/com/petmatz/common/security/jwt/JwtExtractProviderImpl.java +++ b/src/main/java/com/petmatz/common/security/jwt/JwtExtractProviderImpl.java @@ -1,6 +1,7 @@ package com.petmatz.common.security.jwt; import com.petmatz.domain.user.repository.UserRepository; +import com.petmatz.infra.redis.component.RedisTokenComponent; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -13,7 +14,7 @@ @RequiredArgsConstructor public class JwtExtractProviderImpl implements JwtExtractProvider { - private final JwtProvider jwtProvider; // JWT를 검증하고 ID를 추출하는 클래스 + private final JwtManager jwtManager; // JWT를 검증하고 ID를 추출하는 클래스 private final UserRepository userRepository; @Override @@ -33,7 +34,7 @@ public Long findIdFromJwt() { return (Long) principal; // Principal이 Long 타입인 경우 직접 반환 } else if (principal instanceof String) { // Principal이 String인 경우 JWT에서 ID 추출 - return jwtProvider.validateAndGetUserId((String) principal); + return jwtManager.validateAndGetUserId((String) principal); } else { throw new IllegalArgumentException("Invalid principal type: " + principal.getClass().getName()); } @@ -63,7 +64,8 @@ public String findAccountIdFromJwt() { return userRepository.findAccountIdByUserId(userId); // Repository 메서드 사용 } else if (principal instanceof String) { // Principal이 String 타입인 경우 JWT로 간주하고 accountId 추출 - Map claims = jwtProvider.validate((String) principal); + Map claims = jwtManager.validate((String) principal); + if (claims != null && claims.containsKey("accountId")) { return (String) claims.get("accountId"); } diff --git a/src/main/java/com/petmatz/common/security/jwt/JwtManager.java b/src/main/java/com/petmatz/common/security/jwt/JwtManager.java new file mode 100644 index 0000000..95c4ea6 --- /dev/null +++ b/src/main/java/com/petmatz/common/security/jwt/JwtManager.java @@ -0,0 +1,168 @@ +package com.petmatz.common.security.jwt; + +import com.petmatz.domain.user.component.CookieComponent; +import com.petmatz.domain.user.component.UserUtils; +import com.petmatz.infra.redis.component.RedisTokenComponent; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * JWT 토큰 생성 및 검증을 담당하는 클래스. + * 주어진 사용자 ID로 JWT 토큰을 생성 -> 토큰의 유효성을 검증하여 사용자 ID를 반환 + */ + +@Component +@Slf4j +@RequiredArgsConstructor +public class JwtManager { + + @Value("${secret-access-key}") + private String accessKey; + + @Value("${secret-refresh-key}") + private String refreshKey; + + private final RedisTokenComponent redisTokenComponent; + private final CookieComponent cookieComponent; + private final UserUtils userUtils; + + /** + * 주어진 사용자 ID와 계정 ID로 JWT 토큰을 생성하는 메서드. + * 토큰은 1시간 동안 유효하며, 사용자 ID를 서브젝트로 설정하고 계정 ID를 클레임으로 추가. + * + * @return 생성된 JWT 토큰 문자열 + */ + public String createAccessToken(Long userId, String accountId) { + // 토큰 만료 시간 설정 (1시간 후) + Date expireDate = Date.from(Instant.now().plus(24, ChronoUnit.HOURS)); + + // 비밀 키 생성 + Key key = Keys.hmacShaKeyFor(accessKey.getBytes(StandardCharsets.UTF_8)); + + // JWT 토큰 생성 + String jwt = Jwts.builder() + .signWith(key, SignatureAlgorithm.HS256) // 서명 알고리즘 및 키 설정 + .setSubject(userId.toString()) // 서브젝트에 사용자 ID 설정 + .claim("accountId", accountId) // 계정 ID를 클레임으로 추가 + .setIssuedAt(new Date()) // 토큰 발행 시간 + .setExpiration(expireDate) // 토큰 만료 시간 + .compact(); + log.info("Generated JWT: {}", jwt); + return jwt; + } + + public String createRefreshToken(Long userId) { + Date expireDate = Date.from(Instant.now().plus(2, ChronoUnit.WEEKS)); + SecretKey secretKey = Keys.hmacShaKeyFor(refreshKey.getBytes(StandardCharsets.UTF_8)); + + String refreshToken = Jwts.builder() + .signWith(secretKey, SignatureAlgorithm.RS256) + .setSubject(userId.toString()) + .setIssuedAt(new Date()) + .setExpiration(expireDate) + .compact(); + + redisTokenComponent.saveRefreshToken(userId, refreshToken); + + return refreshToken; + } + + // 토큰 검증 + public Map validate(String jwt) { + Key key = Keys.hmacShaKeyFor(accessKey.getBytes(StandardCharsets.UTF_8)); + + try { + var claims = Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(jwt) + .getBody(); + + // 서브젝트에서 사용자 ID 추출 + Long userId = Long.parseLong(claims.getSubject()); + // 클레임에서 계정 ID 추출 + String accountId = claims.get("accountId", String.class); + + // 결과 맵 생성 + Map result = new HashMap<>(); + result.put("userId", userId); + result.put("accountId", accountId); + return result; + + } catch (ExpiredJwtException e) { + log.warn("Access Token expired: {}", jwt); + return null; + } catch (Exception e) { + log.error("Invalid Access Token: {}", jwt, e); + return null; + } + } + + public Long validateAndGetUserId(String token) { + try { + Key key = Keys.hmacShaKeyFor(accessKey.getBytes(StandardCharsets.UTF_8)); + String subject = Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + + return Long.parseLong(subject); // subject를 Long 타입으로 변환 + } catch (ExpiredJwtException e) { + log.warn("Access Token expired" + e); + return null; + } catch (Exception e) { + log.error("Invalid Access Token: {}" + e); + return null; + } + } + + // refresh Token 으로 재발급 + public void refreshAccessToken(HttpServletResponse response, String refreshToken) { + try { + SecretKey refreshKey = Keys.hmacShaKeyFor(this.refreshKey.getBytes(StandardCharsets.UTF_8)); + + // Refresh Token 검증 및 파싱 + var claims = Jwts.parserBuilder() + .setSigningKey(refreshKey) + .build() + .parseClaimsJws(refreshToken) + .getBody(); + + Long userId = Long.parseLong(claims.getSubject()); + String accountId = userUtils.findAccountIdByUserId(userId); + + String storedToken = redisTokenComponent.getRefreshTokenFromRedis(userId); + if (storedToken == null || !storedToken.equals(refreshToken)) { + log.error("Redis에 없는 리프레시 토큰이거나. 일치하지 않는 토큰입니당 다시 한번 확인해주세요"); + } + + String accessToken = createAccessToken(userId, accountId); + cookieComponent.setAccessTokenCookie(response, accessToken); + } catch (ExpiredJwtException e) { + // Refresh Token 만료 시 예외 처리 + log.error("Refresh token has expired." + e); + } catch (Exception e) { + // 기타 검증 실패 시 예외 처리 + log.error("Invalid refresh token." + e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/petmatz/domain/user/component/AuthenticationComponent.java b/src/main/java/com/petmatz/domain/user/component/AuthenticationComponent.java index 8e6f759..72bbc66 100644 --- a/src/main/java/com/petmatz/domain/user/component/AuthenticationComponent.java +++ b/src/main/java/com/petmatz/domain/user/component/AuthenticationComponent.java @@ -1,6 +1,7 @@ package com.petmatz.domain.user.component; -import com.petmatz.common.security.jwt.JwtProvider; + +import com.petmatz.common.security.jwt.JwtManager; import com.petmatz.domain.user.entity.Certification; import com.petmatz.domain.user.entity.User; import com.petmatz.domain.user.exception.UserException; @@ -17,8 +18,8 @@ import java.security.cert.CertificateException; import java.time.LocalDateTime; -import static com.petmatz.domain.user.exception.MatchErrorCode.CERTIFICATION_EXPIRED; -import static com.petmatz.domain.user.exception.MatchErrorCode.MISS_MATCH_CODE; +import static com.petmatz.domain.user.exception.UserErrorCode.CERTIFICATION_EXPIRED; +import static com.petmatz.domain.user.exception.UserErrorCode.MISS_MATCH_CODE; @Component @RequiredArgsConstructor @@ -26,10 +27,18 @@ public class AuthenticationComponent { private final CertificationRepository certificationRepository; private final UserRepository userRepository; - private final JwtProvider jwtProvider; + private final JwtManager jwtManager; private final UserUtils userUtils; private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + public String createJwtAccessToken(User user) { + return jwtManager.createAccessToken(user.getId(), user.getAccountId()); + } + + public String createJwtRefreshToken(User user) { + return jwtManager.createRefreshToken(user.getId()); + } + public User validateSignInCredentials(SignInInfo info) throws AuthenticationException { String accountId = info.getAccountId(); String password = info.getPassword(); @@ -38,15 +47,11 @@ public User validateSignInCredentials(SignInInfo info) throws AuthenticationExce String encodedPassword = user.getPassword(); if (!passwordEncoder.matches(password, encodedPassword)) { - throw new AuthenticationException("비밀번호 불일치"); +// throw new UserException(); } return user; } - public String createJwtToken(User user) { - return jwtProvider.create(user.getId(), user.getAccountId()); - } - /** * 필수 정보 누락 확인 */ diff --git a/src/main/java/com/petmatz/domain/user/component/CookieComponent.java b/src/main/java/com/petmatz/domain/user/component/CookieComponent.java new file mode 100644 index 0000000..be37572 --- /dev/null +++ b/src/main/java/com/petmatz/domain/user/component/CookieComponent.java @@ -0,0 +1,36 @@ +package com.petmatz.domain.user.component; + +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.http.ResponseCookie; + + +@Component +public class CookieComponent { + + public void setAccessTokenCookie(HttpServletResponse response, String accessToken) { + ResponseCookie responseCookie = org.springframework.http.ResponseCookie.from("jwt", accessToken) + .httpOnly(true) // XSS 방지 + .secure(true) // HTTPS만 허용 + .path("/") // 모든 경로에서 접근 가능 + .sameSite("None") // SameSite=None 설정 + .maxAge(3600) // 1시간 유효 + .build(); + setCookieHeader(response, responseCookie); + } + + public void setRefreshTokenCookie(HttpServletResponse response, String refreshToken) { + ResponseCookie responseCookie = org.springframework.http.ResponseCookie.from("refreshToken", refreshToken) + .httpOnly(true) // XSS 방지 + .secure(true) // HTTPS만 허용 + .path("/") // 모든 경로에서 접근 가능 + .sameSite("None") // SameSite=None 설정 + .maxAge(60 * 60 * 24 * 14) // 2주 동안 유효 + .build(); + setCookieHeader(response, responseCookie); + } + + private static void setCookieHeader(HttpServletResponse response, ResponseCookie responseCookie) { + response.addHeader("Set-Cookie", responseCookie.toString()); + } +} diff --git a/src/main/java/com/petmatz/domain/user/component/EmailComponent.java b/src/main/java/com/petmatz/domain/user/component/EmailComponent.java index 04a250c..ffac476 100644 --- a/src/main/java/com/petmatz/domain/user/component/EmailComponent.java +++ b/src/main/java/com/petmatz/domain/user/component/EmailComponent.java @@ -1,14 +1,11 @@ package com.petmatz.domain.user.component; import com.petmatz.domain.user.entity.Certification; -import com.petmatz.domain.user.exception.UserException; import com.petmatz.domain.user.repository.CertificationRepository; import com.petmatz.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import static com.petmatz.domain.user.exception.MatchErrorCode.USER_DUPLICATE; - @Component @RequiredArgsConstructor public class EmailComponent { diff --git a/src/main/java/com/petmatz/domain/user/component/GeocodingComponent.java b/src/main/java/com/petmatz/domain/user/component/GeocodingComponent.java index 6fc3304..eabbe04 100644 --- a/src/main/java/com/petmatz/domain/user/component/GeocodingComponent.java +++ b/src/main/java/com/petmatz/domain/user/component/GeocodingComponent.java @@ -1,20 +1,17 @@ package com.petmatz.domain.user.component; -import com.fasterxml.jackson.annotation.JsonProperty; import com.petmatz.domain.user.entity.KakaoRegion; import com.petmatz.domain.user.exception.UserException; import com.petmatz.domain.user.response.KakaoGeocodingResponse; -import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; -import com.petmatz.domain.user.entity.KakaoRegion; import java.util.List; -import static com.petmatz.domain.user.exception.MatchErrorCode.INSUFFICIENT_LOCATION_DATA; +import static com.petmatz.domain.user.exception.UserErrorCode.INSUFFICIENT_LOCATION_DATA; @Service public class GeocodingComponent { diff --git a/src/main/java/com/petmatz/domain/user/component/HeartComponent.java b/src/main/java/com/petmatz/domain/user/component/HeartComponent.java index 90e9c71..61e727d 100644 --- a/src/main/java/com/petmatz/domain/user/component/HeartComponent.java +++ b/src/main/java/com/petmatz/domain/user/component/HeartComponent.java @@ -14,7 +14,7 @@ import java.util.List; import java.util.Optional; -import static com.petmatz.domain.user.exception.MatchErrorCode.*; +import static com.petmatz.domain.user.exception.UserErrorCode.*; @RequiredArgsConstructor @Component diff --git a/src/main/java/com/petmatz/domain/user/component/PasswordComponent.java b/src/main/java/com/petmatz/domain/user/component/PasswordComponent.java index bd743c9..a885c5c 100644 --- a/src/main/java/com/petmatz/domain/user/component/PasswordComponent.java +++ b/src/main/java/com/petmatz/domain/user/component/PasswordComponent.java @@ -6,7 +6,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; -import static com.petmatz.domain.user.exception.MatchErrorCode.PASSWORD_MISMATCH; +import static com.petmatz.domain.user.exception.UserErrorCode.PASSWORD_MISMATCH; @Component @RequiredArgsConstructor diff --git a/src/main/java/com/petmatz/domain/user/component/UserReader.java b/src/main/java/com/petmatz/domain/user/component/UserReader.java index 8e54c8d..307bc07 100644 --- a/src/main/java/com/petmatz/domain/user/component/UserReader.java +++ b/src/main/java/com/petmatz/domain/user/component/UserReader.java @@ -1,20 +1,17 @@ package com.petmatz.domain.user.component; import com.petmatz.domain.user.entity.User; -import com.petmatz.domain.user.exception.UserException; import com.petmatz.domain.user.repository.UserRepository; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import static com.petmatz.domain.user.exception.MatchErrorCode.USER_NOT_FOUND; - @Component @RequiredArgsConstructor public class UserReader { /** * 이것도 utils를 만들어서 곧 지울듯 + * 종원님 이거 다 쓰시면 지워주세욥 */ private final UserRepository userRepository; diff --git a/src/main/java/com/petmatz/domain/user/component/UserUtils.java b/src/main/java/com/petmatz/domain/user/component/UserUtils.java index 5adc216..903c81d 100644 --- a/src/main/java/com/petmatz/domain/user/component/UserUtils.java +++ b/src/main/java/com/petmatz/domain/user/component/UserUtils.java @@ -8,7 +8,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import static com.petmatz.domain.user.exception.MatchErrorCode.*; +import static com.petmatz.domain.user.exception.UserErrorCode.*; @Component @RequiredArgsConstructor @@ -42,6 +42,11 @@ public User findIdUser(Long userId) { return user; } + public String findAccountIdByUserId(Long userId) { + return userRepository.findByUserId(userId) + .orElseThrow(() -> new UserException(USER_NOT_FOUND)); + } + @Transactional public User getCurrentUser(Long userId) { diff --git a/src/main/java/com/petmatz/domain/user/exception/MatchErrorCode.java b/src/main/java/com/petmatz/domain/user/exception/UserErrorCode.java similarity index 84% rename from src/main/java/com/petmatz/domain/user/exception/MatchErrorCode.java rename to src/main/java/com/petmatz/domain/user/exception/UserErrorCode.java index c0f7184..856f6dd 100644 --- a/src/main/java/com/petmatz/domain/user/exception/MatchErrorCode.java +++ b/src/main/java/com/petmatz/domain/user/exception/UserErrorCode.java @@ -5,7 +5,7 @@ import lombok.RequiredArgsConstructor; @RequiredArgsConstructor -public enum MatchErrorCode implements BaseErrorCode { +public enum UserErrorCode implements BaseErrorCode { INSUFFICIENT_LOCATION_DATA(404, "INSUFFICIENT_LOCATION_DATA", "유요한 위치 정보를 가져올 수 없습니다."), MISS_KAKAO_LOACTION(400, "MISS_KAKAO_LOACTION", "카카오 지역 api를 호출 할 수 없습니다."), @@ -23,8 +23,6 @@ public enum MatchErrorCode implements BaseErrorCode { HEART_USER_DUPLICATE(400, "HEART_USER_DUPLICATE", "찜한 사용자가 이미 존재합니다."); - - private final Integer status; private final String errorCode; private final String message; @@ -33,5 +31,21 @@ public enum MatchErrorCode implements BaseErrorCode { public ErrorReason getErrorReason() { return ErrorReason.of(status, errorCode, message); } + + + + UserErrorCode(int status, String errorCode, String message) { + this.status = status; + this.errorCode = errorCode; + this.message = message; + } + + public int status() { + return status; + } + + public String message() { + return message; + } } diff --git a/src/main/java/com/petmatz/domain/user/repository/UserRepository.java b/src/main/java/com/petmatz/domain/user/repository/UserRepository.java index 5ce662c..9e92392 100644 --- a/src/main/java/com/petmatz/domain/user/repository/UserRepository.java +++ b/src/main/java/com/petmatz/domain/user/repository/UserRepository.java @@ -16,6 +16,8 @@ public interface UserRepository extends JpaRepository { boolean existsById(Long userId); boolean existsByAccountId(String accountId); Optional findById(Long userId); + + Optional findByUserId(Long userId); User findByAccountId(String accountId); @Query("SELECT u.accountId FROM User u WHERE u.id = :userId") @@ -27,4 +29,6 @@ public interface UserRepository extends JpaRepository { void deleteUserById(@Param("userId") Long userId); List findByRegionCodeOrderByRecommendationCountDesc(Integer regionCode); + + } diff --git a/src/main/java/com/petmatz/domain/user/response/EditKakaoProfileResponseDto.java b/src/main/java/com/petmatz/domain/user/response/EditKakaoProfileResponseDto.java deleted file mode 100644 index 59cda5a..0000000 --- a/src/main/java/com/petmatz/domain/user/response/EditKakaoProfileResponseDto.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.petmatz.domain.user.response; - -import com.petmatz.user.common.LogInResponseDto; -import com.petmatz.user.common.ResponseCode; -import com.petmatz.user.common.ResponseMessage; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -public class EditKakaoProfileResponseDto extends LogInResponseDto { - public EditKakaoProfileResponseDto(){ - } - - public static ResponseEntity idNotFound(){ - LogInResponseDto responseBody = new LogInResponseDto(ResponseCode.ID_NOT_FOUND, ResponseMessage.ID_NOT_FOUND); - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(responseBody); - } - - public static ResponseEntity editFailed(){ - LogInResponseDto responseBody = new LogInResponseDto(ResponseCode.EDIT_FAIL, ResponseMessage.EDIT_FAIL); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseBody); - } -} diff --git a/src/main/java/com/petmatz/domain/user/response/HeartingResponseDto.java b/src/main/java/com/petmatz/domain/user/response/HeartingResponseDto.java deleted file mode 100644 index dad0643..0000000 --- a/src/main/java/com/petmatz/domain/user/response/HeartingResponseDto.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.petmatz.domain.user.response; - -import com.petmatz.user.common.LogInResponseDto; -import com.petmatz.user.common.ResponseCode; -import com.petmatz.user.common.ResponseMessage; -import lombok.Getter; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -@Getter -public class HeartingResponseDto extends LogInResponseDto { - - private HeartingResponseDto(){ - super(); - } - - public static ResponseEntity heartedIdNotFound(){ - LogInResponseDto responseBody = new LogInResponseDto(ResponseCode.HEARTED_ID_NOT_FOUND, ResponseMessage.HEARTED_ID_NOT_FOUND); - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(responseBody); - } -} diff --git a/src/main/java/com/petmatz/domain/user/service/AuthService.java b/src/main/java/com/petmatz/domain/user/service/AuthService.java index 82449cb..b435e5c 100644 --- a/src/main/java/com/petmatz/domain/user/service/AuthService.java +++ b/src/main/java/com/petmatz/domain/user/service/AuthService.java @@ -1,9 +1,12 @@ package com.petmatz.domain.user.service; -import com.petmatz.common.security.jwt.JwtProvider; + +import com.petmatz.common.security.jwt.JwtManager; + import com.petmatz.domain.aws.AwsClient; import com.petmatz.domain.aws.vo.S3Imge; import com.petmatz.domain.user.component.AuthenticationComponent; +import com.petmatz.domain.user.component.CookieComponent; import com.petmatz.domain.user.entity.Certification; import com.petmatz.domain.user.entity.KakaoRegion; import com.petmatz.domain.user.entity.User; @@ -44,7 +47,8 @@ public class AuthService { private final CertificationRepository certificationRepository; private final GeocodingComponent geocodingComponent; private final AwsClient awsClient; // 추후에 수정 - private final JwtProvider jwtProvider; + private final JwtManager jwtManager; + private final CookieComponent cookieComponent; private final AuthenticationComponent authenticationComponent; private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); @@ -80,16 +84,12 @@ public SignUpResponseDto signUp(SignUpInfo info) throws MalformedURLException { public SignInResponseDto signIn(SignInInfo info, HttpServletResponse response) throws AuthenticationException { User user = authenticationComponent.validateSignInCredentials(info); - String token = authenticationComponent.createJwtToken(user); - - ResponseCookie responseCookie = ResponseCookie.from("jwt", token) - .httpOnly(true) // XSS 방지 - .secure(true) // HTTPS만 허용 - .path("/") // 모든 경로에서 접근 가능 - .sameSite("None") // SameSite=None 설정 - .maxAge((3600)) - .build(); - response.addHeader("Set-Cookie", responseCookie.toString()); + String accessToken = authenticationComponent.createJwtAccessToken(user); + String refreshToken = authenticationComponent.createJwtRefreshToken(user); + + cookieComponent.setAccessTokenCookie(response, accessToken); + cookieComponent.setRefreshTokenCookie(response, refreshToken); + // 로그인 성공 응답 반환 return new SignInResponseDto(user); } diff --git a/src/main/java/com/petmatz/domain/user/service/CustomOAuthUserService.java b/src/main/java/com/petmatz/domain/user/service/CustomOAuthUserService.java deleted file mode 100644 index 5ec17e2..0000000 --- a/src/main/java/com/petmatz/domain/user/service/CustomOAuthUserService.java +++ /dev/null @@ -1,75 +0,0 @@ -//package com.petmatz.domain.user.service; -// -//import com.petmatz.domain.user.constant.LoginRole; -//import com.petmatz.domain.user.constant.LoginType; -//import com.petmatz.domain.user.entity.CustomOAuthUser; -//import com.petmatz.domain.user.entity.User; -//import com.petmatz.domain.user.repository.UserRepository; -//import lombok.RequiredArgsConstructor; -//import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -//import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -//import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -//import org.springframework.security.oauth2.core.user.OAuth2User; -//import org.springframework.stereotype.Service; -// -//import java.util.Map; -// -//@Service -//@RequiredArgsConstructor -//public class CustomOAuthUserService extends DefaultOAuth2UserService { -// -// private final UserRepository userRepository; -// -// @Override -// public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { -// OAuth2User oAuth2User = super.loadUser(userRequest); -// Map attributes = oAuth2User.getAttributes(); -// -// // 1. Check the registration ID (ensure it's Kakao) -// String registrationId = userRequest.getClientRegistration().getRegistrationId(); -// if (!"kakao".equals(registrationId)) { -// throw new OAuth2AuthenticationException("Unsupported registrationId: " + registrationId); -// } -// -// // 2. Extract attributes -// String kakaoAccountId = attributes.get("id").toString(); // 고유 ID -// Map kakaoAccount = (Map) attributes.get("kakao_account"); -// -// // Extract email (required for accountId) -// String email = (String) kakaoAccount.get("email"); -// if (email == null || email.isEmpty()) { -// throw new OAuth2AuthenticationException("Email is required for Kakao login."); -// } -// -// // Extract nickname -// Map profile = (Map) kakaoAccount.get("profile"); -// String nickname = (String) profile.getOrDefault("nickname", "Unknown User"); -// -// // Extract profile image (optional) -// String profileImage = (String) profile.getOrDefault("profile_image_url", ""); -// -// // 3. Check if user exists or create new user -// User user = userRepository.findByAccountId(email); -// if (user == null) { -// user = createNewKakaoUser(email, nickname, profileImage); // 신규 사용자 생성 -// } -// -// // 4. Return CustomOAuthUser -// return new CustomOAuthUser(user.getId(), user.getAccountId(), attributes, oAuth2User.getAuthorities()); -// } -// -// private User createNewKakaoUser(String email, String nickname, String profileImage) { -// User newUser = User.builder() -// .accountId(email) // 이메일을 accountId에 저장 -// .password("password") // 기본 비밀번호 설정 (필요시 변경) -// .nickname(nickname) -// .profileImg(profileImage) // 프로필 이미지 저장 -// .loginRole(LoginRole.ROLE_USER) // 기본 역할 설정 -// .loginType(LoginType.KAKAO) // 로그인 타입 -// .careCompletionCount(0) -// .recommendationCount(0) -// .build(); -// -// return userRepository.save(newUser); -// } -//} \ No newline at end of file diff --git a/src/main/java/com/petmatz/domain/user/service/KakaoUserService.java b/src/main/java/com/petmatz/domain/user/service/KakaoUserService.java index 638a3aa..d59444c 100644 --- a/src/main/java/com/petmatz/domain/user/service/KakaoUserService.java +++ b/src/main/java/com/petmatz/domain/user/service/KakaoUserService.java @@ -15,7 +15,7 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import static com.petmatz.domain.user.exception.MatchErrorCode.*; +import static com.petmatz.domain.user.exception.UserErrorCode.*; @Component @RequiredArgsConstructor diff --git a/src/main/java/com/petmatz/infra/email/EmailProviderImpl.java b/src/main/java/com/petmatz/infra/email/EmailProviderImpl.java index a0502cc..b5cda07 100644 --- a/src/main/java/com/petmatz/infra/email/EmailProviderImpl.java +++ b/src/main/java/com/petmatz/infra/email/EmailProviderImpl.java @@ -11,7 +11,7 @@ import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; -import static com.petmatz.domain.user.exception.MatchErrorCode.FAIL_MAIL_SEND; +import static com.petmatz.domain.user.exception.UserErrorCode.FAIL_MAIL_SEND; @Component @RequiredArgsConstructor diff --git a/src/main/java/com/petmatz/infra/email/RePasswordEmailProviderImpl.java b/src/main/java/com/petmatz/infra/email/RePasswordEmailProviderImpl.java index 4a87bba..6f0da76 100644 --- a/src/main/java/com/petmatz/infra/email/RePasswordEmailProviderImpl.java +++ b/src/main/java/com/petmatz/infra/email/RePasswordEmailProviderImpl.java @@ -11,7 +11,7 @@ import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; -import static com.petmatz.domain.user.exception.MatchErrorCode.FAIL_MAIL_SEND; +import static com.petmatz.domain.user.exception.UserErrorCode.FAIL_MAIL_SEND; @Component @RequiredArgsConstructor diff --git a/src/main/java/com/petmatz/infra/redis/component/RedisTokenComponent.java b/src/main/java/com/petmatz/infra/redis/component/RedisTokenComponent.java new file mode 100644 index 0000000..87948ba --- /dev/null +++ b/src/main/java/com/petmatz/infra/redis/component/RedisTokenComponent.java @@ -0,0 +1,26 @@ +package com.petmatz.infra.redis.component; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Component +@RequiredArgsConstructor +public class RedisTokenComponent { + + private final RedisTemplate redisTemplate; + public void saveRefreshToken(Long userId, String refreshToken) { + String redisKey = "refreshToken:" + userId; + try { + redisTemplate.opsForValue().set(redisKey, refreshToken, Duration.ofDays(30)); + } catch (Exception e) { + throw new IllegalStateException("일단은 리프레시 토큰 저장 오류입니다!"); + } + } + public String getRefreshTokenFromRedis(Long userId) { + String redisKey = "refreshToken:" + userId; + return (String) redisTemplate.opsForValue().get(redisKey); + } +}