From 155c43364ec9170416e002d33e76adb7d035835c Mon Sep 17 00:00:00 2001 From: "y.tnwjd" Date: Wed, 13 Aug 2025 22:14:49 +0900 Subject: [PATCH 1/2] refactor: JWT constant centralization and token provider improvements --- .../jwt/JwtAuthenticationFilter.java | 38 ++++-- .../jwt/JwtAuthenticationResponse.java | 7 + .../java/com/ajouchong/jwt/JwtConstants.java | 39 ++++++ .../java/com/ajouchong/jwt/JwtTokenDto.java | 6 +- .../com/ajouchong/jwt/JwtTokenProvider.java | 121 +++++++++++------- 5 files changed, 148 insertions(+), 63 deletions(-) create mode 100644 src/main/java/com/ajouchong/jwt/JwtConstants.java diff --git a/src/main/java/com/ajouchong/jwt/JwtAuthenticationFilter.java b/src/main/java/com/ajouchong/jwt/JwtAuthenticationFilter.java index 3d18aee..2c3995a 100644 --- a/src/main/java/com/ajouchong/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/ajouchong/jwt/JwtAuthenticationFilter.java @@ -9,6 +9,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -19,15 +20,20 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; +@Slf4j @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; private final MemberRepository memberRepository; @Override - protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { - + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String token = extractToken(request); if (token == null) { @@ -36,15 +42,16 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull Ht } try { - // 토큰 만료 여부 확인 if (jwtTokenProvider.isExpired(token)) { + log.warn("만료된 토큰으로 인증 시도: {}", token.substring(0, Math.min(token.length(), 20)) + "..."); sendErrorResponse(response, "Token has expired."); return; } - // 인증 정보 설정 setAuthentication(token); + log.debug("JWT 인증 성공: {}", request.getRequestURI()); } catch (Exception e) { + log.warn("JWT 인증 실패: {} - {}", request.getRequestURI(), e.getMessage()); sendErrorResponse(response, "Invalid token: " + e.getMessage()); return; } @@ -53,25 +60,26 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull Ht } private String extractToken(HttpServletRequest request) { - String authorization = request.getHeader("Authorization"); - if (authorization != null && authorization.startsWith("Bearer ")) { - return authorization.substring(7); + String authorization = request.getHeader(JwtConstants.AUTHORIZATION_HEADER); + if (authorization != null && authorization.startsWith(JwtConstants.BEARER_PREFIX)) { + return authorization.substring(JwtConstants.BEARER_PREFIX.length()); } return extractTokenFromCookies(request); } private String extractTokenFromCookies(HttpServletRequest request) { - if (request.getCookies() == null) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { return null; } - for (Cookie cookie : request.getCookies()) { - if ("accessToken".equals(cookie.getName())) { + for (Cookie cookie : cookies) { + if (JwtConstants.ACCESS_TOKEN_COOKIE_NAME.equals(cookie.getName())) { try { return java.net.URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8); } catch (Exception e) { - System.err.println("Invalid Cookie Format: " + e.getMessage()); + log.warn("쿠키 디코딩 실패: {}", e.getMessage()); return null; } } @@ -79,13 +87,15 @@ private String extractTokenFromCookies(HttpServletRequest request) { return null; } - private void setAuthentication(String token) { String email = jwtTokenProvider.getEmailFromToken(token); String role = jwtTokenProvider.getRoleFromToken(token); Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> new IllegalArgumentException("User not found: " + email)); + .orElseThrow(() -> { + log.error("토큰의 이메일로 회원을 찾을 수 없음: {}", email); + return new IllegalArgumentException("User not found: " + email); + }); Authentication authentication = new UsernamePasswordAuthenticationToken( member, @@ -100,6 +110,6 @@ private void sendErrorResponse(HttpServletResponse response, String message) thr response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); - response.getWriter().write("{\"error\": \"Unauthorized\", \"message\": \"" + message + "\"}"); + response.getWriter().write(String.format(JwtConstants.ERROR_RESPONSE_TEMPLATE, message)); } } diff --git a/src/main/java/com/ajouchong/jwt/JwtAuthenticationResponse.java b/src/main/java/com/ajouchong/jwt/JwtAuthenticationResponse.java index c516a02..9d4c65d 100644 --- a/src/main/java/com/ajouchong/jwt/JwtAuthenticationResponse.java +++ b/src/main/java/com/ajouchong/jwt/JwtAuthenticationResponse.java @@ -8,9 +8,16 @@ public class JwtAuthenticationResponse { private String tokenType = "Bearer"; private String accessToken; + private String refreshToken; + private Long expiresIn; public JwtAuthenticationResponse(String accessToken) { this.accessToken = accessToken; } + public JwtAuthenticationResponse(String accessToken, String refreshToken, Long expiresIn) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.expiresIn = expiresIn; + } } diff --git a/src/main/java/com/ajouchong/jwt/JwtConstants.java b/src/main/java/com/ajouchong/jwt/JwtConstants.java new file mode 100644 index 0000000..7e000d4 --- /dev/null +++ b/src/main/java/com/ajouchong/jwt/JwtConstants.java @@ -0,0 +1,39 @@ +package com.ajouchong.jwt; + +public final class JwtConstants { + + // 토큰 타입 + public static final String ACCESS_TOKEN_SUBJECT = "accessToken"; + public static final String REFRESH_TOKEN_SUBJECT = "refreshToken"; + + // 클레임 키 + public static final String EMAIL_CLAIM = "email"; + public static final String NAME_CLAIM = "name"; + public static final String ROLE_CLAIM = "role"; + + // 쿠키 이름 + public static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken"; + public static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; + + // 토큰 유효기간 (밀리초) + public static final long ACCESS_TOKEN_VALIDITY = 1000L * 60 * 60 * 24; // 1일 + public static final long REFRESH_TOKEN_VALIDITY = 1000L * 60 * 60 * 24 * 7; // 7일 + + // 쿠키 유효기간 (초) + public static final int ACCESS_TOKEN_COOKIE_MAX_AGE = 60 * 120; // 120분 + public static final int REFRESH_TOKEN_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7일 + + // 보안 설정 + public static final int MIN_SECRET_KEY_LENGTH = 32; + + // HTTP 헤더 + public static final String AUTHORIZATION_HEADER = "Authorization"; + public static final String BEARER_PREFIX = "Bearer "; + + // 응답 메시지 + public static final String ERROR_RESPONSE_TEMPLATE = "{\"error\": \"Unauthorized\", \"message\": \"%s\"}"; + + private JwtConstants() { + // 유틸리티 클래스이므로 인스턴스화 방지 + } +} \ No newline at end of file diff --git a/src/main/java/com/ajouchong/jwt/JwtTokenDto.java b/src/main/java/com/ajouchong/jwt/JwtTokenDto.java index 4f14195..bb685aa 100644 --- a/src/main/java/com/ajouchong/jwt/JwtTokenDto.java +++ b/src/main/java/com/ajouchong/jwt/JwtTokenDto.java @@ -3,11 +3,15 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; -@Builder @Data +@Builder +@NoArgsConstructor @AllArgsConstructor public class JwtTokenDto { private String grantType; private String accessToken; + private String refreshToken; + private Long expiresIn; } diff --git a/src/main/java/com/ajouchong/jwt/JwtTokenProvider.java b/src/main/java/com/ajouchong/jwt/JwtTokenProvider.java index 60caf88..d9d4235 100644 --- a/src/main/java/com/ajouchong/jwt/JwtTokenProvider.java +++ b/src/main/java/com/ajouchong/jwt/JwtTokenProvider.java @@ -21,54 +21,60 @@ public class JwtTokenProvider { private static SecretKey secretKey = null; - private static final long accessTokenValidity = 1000L * 60 * 60 * 24; // 1일 public JwtTokenProvider(@Value("${jwt.secret}") String secret) { - byte[] keyBytes = Base64.getDecoder().decode(secret); - if (keyBytes.length < 32) { // 최소 길이 검증 - throw new IllegalArgumentException("JWT Secret key는 최소 32 bytes이어야 합니다."); + validateAndInitializeSecretKey(secret); + } + + private void validateAndInitializeSecretKey(String secret) { + try { + byte[] keyBytes = Base64.getDecoder().decode(secret); + if (keyBytes.length < JwtConstants.MIN_SECRET_KEY_LENGTH) { + throw new IllegalArgumentException("JWT Secret key는 최소 " + JwtConstants.MIN_SECRET_KEY_LENGTH + " bytes이어야 합니다."); + } + secretKey = Keys.hmacShaKeyFor(keyBytes); + } catch (IllegalArgumentException e) { + log.error("JWT Secret key 초기화 실패: {}", e.getMessage()); + throw e; } - secretKey = Keys.hmacShaKeyFor(keyBytes); } public static String createAccessToken(Member member) { Date now = new Date(); - Date expiryDate = new Date(now.getTime() + accessTokenValidity); + Date expiryDate = new Date(now.getTime() + JwtConstants.ACCESS_TOKEN_VALIDITY); return Jwts.builder() - .setSubject("accessToken") + .setSubject(JwtConstants.ACCESS_TOKEN_SUBJECT) .setClaims(createAccessTokenClaims(member)) .setExpiration(expiryDate) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); } - private static Map createAccessTokenClaims (Member member) { - Map map = new HashMap<>(); - map.put("email", member.getEmail()); - map.put("name", member.getName()); - map.put("role", member.getRole()); - return map; + private static Map createAccessTokenClaims(Member member) { + Map claims = new HashMap<>(); + claims.put(JwtConstants.EMAIL_CLAIM, member.getEmail()); + claims.put(JwtConstants.NAME_CLAIM, member.getName()); + claims.put(JwtConstants.ROLE_CLAIM, member.getRole()); + return claims; } public String createRefreshToken(Member member) { Date now = new Date(); - - long refreshTokenValidity = 1000L * 60 * 60 * 24 * 7; - Date expiryDate = new Date(now.getTime() + refreshTokenValidity); + Date expiryDate = new Date(now.getTime() + JwtConstants.REFRESH_TOKEN_VALIDITY); return Jwts.builder() - .setSubject("refreshToken") + .setSubject(JwtConstants.REFRESH_TOKEN_SUBJECT) .setClaims(createRefreshTokenClaims(member)) .setExpiration(expiryDate) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); } - private static Map createRefreshTokenClaims (Member member) { - Map map = new HashMap<>(); - map.put("email", member.getEmail()); - return map; + private static Map createRefreshTokenClaims(Member member) { + Map claims = new HashMap<>(); + claims.put(JwtConstants.EMAIL_CLAIM, member.getEmail()); + return claims; } public boolean isExpired(String token) { @@ -76,22 +82,24 @@ public boolean isExpired(String token) { Date expiration = getClaimsFromToken(token).getExpiration(); return expiration.before(new Date()); } catch (ExpiredJwtException e) { - return true; // 만료된 경우 true 반환 + log.debug("JWT 토큰이 만료되었습니다: {}", e.getMessage()); + return true; } catch (JwtException e) { - return true; // 유효하지 않은 토큰도 만료된 것으로 처리 + log.debug("유효하지 않은 JWT 토큰입니다: {}", e.getMessage()); + return true; } } public String getEmailFromToken(String token) { - return getClaimsFromToken(token).get("email", String.class); + return getClaimsFromToken(token).get(JwtConstants.EMAIL_CLAIM, String.class); } public String getRoleFromToken(String token) { - return getClaimsFromToken(token).get("role", String.class); + return getClaimsFromToken(token).get(JwtConstants.ROLE_CLAIM, String.class); } public String getNameFromToken(String token) { - return getClaimsFromToken(token).get("name", String.class); + return getClaimsFromToken(token).get(JwtConstants.NAME_CLAIM, String.class); } private Claims getClaimsFromToken(String token) { @@ -102,30 +110,31 @@ private Claims getClaimsFromToken(String token) { .parseClaimsJws(token) .getBody(); } catch (ExpiredJwtException e) { + log.warn("토큰이 만료되었습니다: {}", e.getMessage()); throw new InvalidJwtException("토큰이 만료되었습니다."); } catch (JwtException e) { + log.warn("유효하지 않은 JWT 토큰입니다: {}", e.getMessage()); throw new InvalidJwtException("유효하지 않은 JWT 토큰입니다."); } } public void setJwtCookie(HttpServletResponse response, String accessToken, String refreshToken) { - Cookie accessCookie = new Cookie("accessToken", accessToken); - accessCookie.setHttpOnly(true); - accessCookie.setSecure(true); // HTTPS 환경에서만 전송 - accessCookie.setPath("/"); - accessCookie.setMaxAge(60 * 120); // 120분 후 만료 - response.addCookie(accessCookie); - + setCookie(response, JwtConstants.ACCESS_TOKEN_COOKIE_NAME, accessToken, JwtConstants.ACCESS_TOKEN_COOKIE_MAX_AGE); + if (refreshToken != null) { - Cookie refreshCookie = new Cookie("refreshToken", refreshToken); - refreshCookie.setHttpOnly(true); - refreshCookie.setSecure(true); - refreshCookie.setPath("/"); - refreshCookie.setMaxAge(60 * 60 * 24 * 7); // 7일 후 만료 - response.addCookie(refreshCookie); + setCookie(response, JwtConstants.REFRESH_TOKEN_COOKIE_NAME, refreshToken, JwtConstants.REFRESH_TOKEN_COOKIE_MAX_AGE); } } + private void setCookie(HttpServletResponse response, String name, String value, int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setPath("/"); + cookie.setMaxAge(maxAge); + response.addCookie(cookie); + } + public boolean validateToken(String token) { try { Jwts.parserBuilder() @@ -134,31 +143,47 @@ public boolean validateToken(String token) { .parseClaimsJws(token); return true; } catch (ExpiredJwtException e) { - log.error("JWT 토큰이 만료되었습니다: {}", e.getMessage()); + log.debug("JWT 토큰이 만료되었습니다: {}", e.getMessage()); } catch (UnsupportedJwtException e) { - log.error("지원되지 않는 JWT 토큰입니다: {}", e.getMessage()); + log.warn("지원되지 않는 JWT 토큰입니다: {}", e.getMessage()); } catch (MalformedJwtException e) { - log.error("잘못된 JWT 서명입니다: {}", e.getMessage()); - } catch (SignatureException e) { - log.error("JWT 서명 검증 실패: {}", e.getMessage()); + log.warn("잘못된 JWT 서명입니다: {}", e.getMessage()); + } catch (SecurityException e) { + log.warn("JWT 서명 검증 실패: {}", e.getMessage()); } catch (JwtException e) { - log.error("유효하지 않은 JWT 토큰입니다: {}", e.getMessage()); + log.warn("유효하지 않은 JWT 토큰입니다: {}", e.getMessage()); } return false; } public String getRefreshTokenFromCookie(HttpServletRequest request) { - if (request.getCookies() == null) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { return null; } - for (Cookie cookie : request.getCookies()) { - if ("refreshToken".equals(cookie.getName())) { + + for (Cookie cookie : cookies) { + if (JwtConstants.REFRESH_TOKEN_COOKIE_NAME.equals(cookie.getName())) { return cookie.getValue(); } } return null; } + public void clearJwtCookies(HttpServletResponse response) { + clearCookie(response, JwtConstants.ACCESS_TOKEN_COOKIE_NAME); + clearCookie(response, JwtConstants.REFRESH_TOKEN_COOKIE_NAME); + } + + private void clearCookie(HttpServletResponse response, String name) { + Cookie cookie = new Cookie(name, null); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setPath("/"); + cookie.setMaxAge(0); + response.addCookie(cookie); + } + public static class InvalidJwtException extends RuntimeException { public InvalidJwtException(String message) { super(message); From 99216ebfbfe9484ec914857545bdecb0577df844 Mon Sep 17 00:00:00 2001 From: "y.tnwjd" Date: Wed, 13 Aug 2025 22:15:33 +0900 Subject: [PATCH 2/2] refactor: introducing the OAuth service interface and improving the controller --- .../ajouchong/oauth/GoogleOAuthService.java | 43 +++- .../com/ajouchong/oauth/GoogleUserDto.java | 8 +- .../com/ajouchong/oauth/OAuthAttributes.java | 6 +- .../com/ajouchong/oauth/OAuthController.java | 205 ++++++++++-------- .../com/ajouchong/oauth/OAuthException.java | 12 + .../com/ajouchong/oauth/OAuthRequestDto.java | 7 +- .../com/ajouchong/oauth/OAuthResponseDto.java | 9 + .../com/ajouchong/oauth/OAuthService.java | 5 + 8 files changed, 192 insertions(+), 103 deletions(-) create mode 100644 src/main/java/com/ajouchong/oauth/OAuthException.java create mode 100644 src/main/java/com/ajouchong/oauth/OAuthService.java diff --git a/src/main/java/com/ajouchong/oauth/GoogleOAuthService.java b/src/main/java/com/ajouchong/oauth/GoogleOAuthService.java index c6cd0a0..383350e 100644 --- a/src/main/java/com/ajouchong/oauth/GoogleOAuthService.java +++ b/src/main/java/com/ajouchong/oauth/GoogleOAuthService.java @@ -7,25 +7,48 @@ import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; @Slf4j @Service @RequiredArgsConstructor -public class GoogleOAuthService { +public class GoogleOAuthService implements OAuthService { + private static final String GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"; + private final RestTemplate restTemplate = new RestTemplate(); + @Override public GoogleUserDto getUserInfo(String accessToken) { - String url = "https://www.googleapis.com/oauth2/v3/userinfo"; + log.debug("Google 사용자 정보 요청 시작"); + + try { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + HttpEntity entity = new HttpEntity<>(headers); - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(accessToken); - HttpEntity entity = new HttpEntity<>(headers); - - ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, GoogleUserDto.class); - log.debug("respone: {}", response); - - return response.getBody(); + ResponseEntity response = restTemplate.exchange( + GOOGLE_USERINFO_URL, + HttpMethod.GET, + entity, + GoogleUserDto.class + ); + + GoogleUserDto userInfo = response.getBody(); + if (userInfo == null) { + throw new OAuthException("Google API에서 사용자 정보를 받지 못했습니다."); + } + + log.debug("Google 사용자 정보 조회 성공: {}", userInfo.getEmail()); + return userInfo; + + } catch (RestClientException e) { + log.error("Google API 호출 실패: {}", e.getMessage()); + throw new OAuthException("Google 사용자 정보 조회에 실패했습니다.", e); + } catch (Exception e) { + log.error("Google 사용자 정보 처리 중 예외 발생: {}", e.getMessage()); + throw new OAuthException("사용자 정보 처리 중 오류가 발생했습니다.", e); + } } } diff --git a/src/main/java/com/ajouchong/oauth/GoogleUserDto.java b/src/main/java/com/ajouchong/oauth/GoogleUserDto.java index 3f8546c..d3b63c8 100644 --- a/src/main/java/com/ajouchong/oauth/GoogleUserDto.java +++ b/src/main/java/com/ajouchong/oauth/GoogleUserDto.java @@ -1,10 +1,16 @@ package com.ajouchong.oauth; import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; @Data +@NoArgsConstructor +@AllArgsConstructor public class GoogleUserDto { private String email; private String name; - private String role; + private String picture; + private String locale; + private String sub; } diff --git a/src/main/java/com/ajouchong/oauth/OAuthAttributes.java b/src/main/java/com/ajouchong/oauth/OAuthAttributes.java index 484b809..f13360b 100644 --- a/src/main/java/com/ajouchong/oauth/OAuthAttributes.java +++ b/src/main/java/com/ajouchong/oauth/OAuthAttributes.java @@ -14,11 +14,12 @@ public class OAuthAttributes { private String nameAttributeKey; private String name; private String email; + private String picture; + private String locale; public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map attributes) { - return ofGoogle(userNameAttributeName, attributes); } @@ -27,6 +28,8 @@ private static OAuthAttributes ofGoogle(String usernameAttributeName, return OAuthAttributes.builder() .name((String) attributes.get("name")) .email((String) attributes.get("email")) + .picture((String) attributes.get("picture")) + .locale((String) attributes.get("locale")) .attributes(attributes) .nameAttributeKey(usernameAttributeName) .build(); @@ -39,5 +42,4 @@ public Member toEntity() { .role(MemberRole.STUDENT) .build(); } - } diff --git a/src/main/java/com/ajouchong/oauth/OAuthController.java b/src/main/java/com/ajouchong/oauth/OAuthController.java index aea22e2..6ce777a 100644 --- a/src/main/java/com/ajouchong/oauth/OAuthController.java +++ b/src/main/java/com/ajouchong/oauth/OAuthController.java @@ -6,7 +6,6 @@ import com.ajouchong.entity.enumClass.MemberRole; import com.ajouchong.jwt.JwtTokenProvider; import com.ajouchong.repository.MemberRepository; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -21,126 +20,154 @@ @Slf4j public class OAuthController { - private final GoogleOAuthService googleOAuthService; + private final OAuthService oAuthService; private final JwtTokenProvider jwtTokenProvider; private final MemberRepository memberRepository; @PostMapping("/oauth") - public ApiResponse googleLogin(@RequestBody Map requestBody, HttpServletResponse response) { - log.info("[Google Login] 요청 수신: {}", requestBody); + public ApiResponse googleLogin(@RequestBody Map requestBody, + HttpServletResponse response) { + log.info("[Google Login] 요청 수신"); + + try { + String accessToken = validateAndExtractAccessToken(requestBody); + String refreshToken = requestBody.get("refreshToken"); + + GoogleUserDto googleUser = oAuthService.getUserInfo(accessToken); + Member member = findOrCreateMember(googleUser); + + String newJwtAccessToken = createNewAccessToken(member); + String newJwtRefreshToken = handleRefreshToken(refreshToken, member, response); + + jwtTokenProvider.setJwtCookie(response, newJwtAccessToken, newJwtRefreshToken); - String accessToken = requestBody.get("accessToken"); - // String jwtToken = requestBody.get("jwtToken"); - String refreshToken = requestBody.get("refreshToken"); + OAuthResponseDto responseDto = new OAuthResponseDto(newJwtAccessToken, member); + log.info("Google 로그인 성공: {}", member.getEmail()); + + return new ApiResponse<>(1, "Google login 성공", responseDto); + + } catch (IllegalArgumentException e) { + log.warn("Google 로그인 요청 검증 실패: {}", e.getMessage()); + return new ApiResponse<>(0, e.getMessage(), null); + } catch (OAuthException e) { + log.error("OAuth 처리 중 오류 발생: {}", e.getMessage()); + return new ApiResponse<>(0, e.getMessage(), null); + } catch (Exception e) { + log.error("Google 로그인 처리 중 오류 발생: {}", e.getMessage(), e); + return new ApiResponse<>(0, "로그인 처리 중 오류가 발생했습니다.", null); + } + } + + @GetMapping("/info") + public ApiResponse getUserInfo(@CookieValue(value = "accessToken", required = false) String accessToken, + HttpServletRequest request, + HttpServletResponse response) { + log.info("[회원 정보 조회] 요청 수신"); + try { + if (accessToken == null || accessToken.isEmpty()) { + log.warn("accessToken 쿠키가 없음"); + return new ApiResponse<>(0, "accessToken이 유효하지 않거나 없습니다.", null); + } + + String email = jwtTokenProvider.getEmailFromToken(accessToken); + + if (jwtTokenProvider.isExpired(accessToken)) { + log.warn("JWT 만료됨. 리프레시 토큰 검토 중..."); + accessToken = handleTokenRefresh(request, response, email); + if (accessToken == null) { + return new ApiResponse<>(0, "세션이 만료되었습니다. 다시 로그인하세요.", null); + } + } + + Member member = findMemberByEmail(email); + ProfileResponseDto responseDto = buildProfileResponse(member); + + log.info("회원 정보 조회 성공: {}", member.getEmail()); + return new ApiResponse<>(1, "회원 정보 조회 성공", responseDto); + + } catch (JwtTokenProvider.InvalidJwtException e) { + log.error("JWT 파싱 실패: {}", e.getMessage()); + return new ApiResponse<>(0, "Invalid token", null); + } catch (Exception e) { + log.error("회원 정보 조회 중 오류 발생: {}", e.getMessage(), e); + return new ApiResponse<>(0, "회원 정보 조회 중 오류가 발생했습니다.", null); + } + } + + @PostMapping("logout") + public ApiResponse logout(HttpServletResponse response) { + try { + jwtTokenProvider.clearJwtCookies(response); + log.info("로그아웃 성공"); + return new ApiResponse<>(1, "로그아웃 되었습니다.", null); + } catch (Exception e) { + log.error("로그아웃 처리 중 오류 발생: {}", e.getMessage(), e); + return new ApiResponse<>(0, "로그아웃 처리 중 오류가 발생했습니다.", null); + } + } + + private String validateAndExtractAccessToken(Map requestBody) { + String accessToken = requestBody.get("accessToken"); if (accessToken == null || accessToken.isEmpty()) { - log.warn("AccessToken이 없습니다."); throw new IllegalArgumentException("AccessToken is missing."); } + return accessToken; + } - GoogleUserDto googleUser = googleOAuthService.getUserInfo(accessToken); - - Member member = memberRepository.findByEmail(googleUser.getEmail()) + private Member findOrCreateMember(GoogleUserDto googleUser) { + return memberRepository.findByEmail(googleUser.getEmail()) .orElseGet(() -> { Member newMember = new Member(googleUser); - newMember.setRole(MemberRole.STUDENT); // 기본 역할 지정 - return memberRepository.save(newMember); + newMember.setRole(MemberRole.STUDENT); + Member savedMember = memberRepository.save(newMember); + log.info("새로운 회원 생성: {}", savedMember.getEmail()); + return savedMember; }); + } - String newJwtAccessToken; - String newJwtRefreshToken = null; + private String createNewAccessToken(Member member) { + return JwtTokenProvider.createAccessToken(member); + } + private String handleRefreshToken(String refreshToken, Member member, HttpServletResponse response) { if (refreshToken != null && jwtTokenProvider.validateToken(refreshToken)) { log.info("유효한 리프레시 토큰 존재. 새로운 JWT 액세스 토큰 발급 중..."); - newJwtAccessToken = JwtTokenProvider.createAccessToken(member); - jwtTokenProvider.setJwtCookie(response, newJwtAccessToken, refreshToken); + return null; // 기존 리프레시 토큰 재사용 } else { log.info("리프레시 토큰 없음 또는 유효하지 않음. 새로운 JWT 토큰 발급..."); - newJwtAccessToken = JwtTokenProvider.createAccessToken(member); - newJwtRefreshToken = jwtTokenProvider.createRefreshToken(member); - jwtTokenProvider.setJwtCookie(response, newJwtAccessToken, newJwtRefreshToken); + return jwtTokenProvider.createRefreshToken(member); } - - OAuthResponseDto responseDto = new OAuthResponseDto(newJwtAccessToken, member); - log.info("신규 JWT 발급 완료: {}", newJwtAccessToken); - log.info("res {}", responseDto); - - return new ApiResponse<>(1, "Google login 성공", responseDto); } - @GetMapping("/info") - public ApiResponse getUserInfo(@CookieValue(value = "accessToken", required = false) String accessToken, - HttpServletRequest request, HttpServletResponse response) { - - log.info("[회원 정보 조회] 요청 수신. accessToken 쿠키 값: {}", accessToken); - - if (accessToken == null || accessToken.isEmpty()) { - log.warn("accessToken 쿠키가 없음."); - return new ApiResponse<>(0, "accessToken이 유효하지 않거나 없습니다.", null); - } - - String email; - try { - email = jwtTokenProvider.getEmailFromToken(accessToken); - } catch (JwtTokenProvider.InvalidJwtException e) { - log.error("JWT 파싱 실패: {}", e.getMessage()); - return new ApiResponse<>(0, "Invalid token", null); - } - - if (jwtTokenProvider.isExpired(accessToken)) { - log.warn("JWT 만료됨. 리프레시 토큰 검토 중..."); - String refreshToken = jwtTokenProvider.getRefreshTokenFromCookie(request); - - if (refreshToken != null && jwtTokenProvider.validateToken(refreshToken)) { - log.info("유효한 리프레시 토큰 확인. 새로운 JWT 액세스 토큰 발급 중..."); - Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> { - log.error("회원 정보 없음: {}", email); - return new RuntimeException("회원 정보가 없습니다."); - }); - - accessToken = JwtTokenProvider.createAccessToken(member); - jwtTokenProvider.setJwtCookie(response, accessToken, refreshToken); // 쿠키에 새로운 accessToken 저장 - } else { - log.error("리프레시 토큰이 없거나 유효하지 않음."); - return new ApiResponse<>(0, "세션이 만료되었습니다. 다시 로그인하세요.", null); - } + private String handleTokenRefresh(HttpServletRequest request, HttpServletResponse response, String email) { + String refreshToken = jwtTokenProvider.getRefreshTokenFromCookie(request); + + if (refreshToken != null && jwtTokenProvider.validateToken(refreshToken)) { + log.info("유효한 리프레시 토큰 확인. 새로운 JWT 액세스 토큰 발급 중..."); + Member member = findMemberByEmail(email); + String newAccessToken = JwtTokenProvider.createAccessToken(member); + jwtTokenProvider.setJwtCookie(response, newAccessToken, refreshToken); + return newAccessToken; + } else { + log.error("리프레시 토큰이 없거나 유효하지 않음."); + return null; } + } - Member member = memberRepository.findByEmail(email) + private Member findMemberByEmail(String email) { + return memberRepository.findByEmail(email) .orElseThrow(() -> { log.error("회원 정보 없음: {}", email); return new RuntimeException("회원 정보가 없습니다."); }); + } - ProfileResponseDto responseDto = ProfileResponseDto.builder() + private ProfileResponseDto buildProfileResponse(Member member) { + return ProfileResponseDto.builder() .name(member.getName()) .email(member.getEmail()) .role(member.getRole().name()) .build(); - - log.info("성공"); - return new ApiResponse<>(1, "회원 정보 조회 성공", responseDto); } - - @PostMapping("logout") - public ApiResponse logout(HttpServletResponse response) { - Cookie refreshToken = new Cookie("refreshToken", null); - refreshToken.setHttpOnly(true); - refreshToken.setSecure(true); - refreshToken.setPath("/"); - refreshToken.setMaxAge(0); // 쿠키 즉시 만료 - - Cookie accessToken = new Cookie("accessToken", null); - accessToken.setHttpOnly(true); - accessToken.setSecure(true); - accessToken.setPath("/"); - accessToken.setMaxAge(0); // 쿠키 즉시 만료 - - response.addCookie(refreshToken); - response.addCookie(accessToken); - - return new ApiResponse<>(1, "로그아웃 되었습니다.", null); - } - } diff --git a/src/main/java/com/ajouchong/oauth/OAuthException.java b/src/main/java/com/ajouchong/oauth/OAuthException.java new file mode 100644 index 0000000..701faad --- /dev/null +++ b/src/main/java/com/ajouchong/oauth/OAuthException.java @@ -0,0 +1,12 @@ +package com.ajouchong.oauth; + +public class OAuthException extends RuntimeException { + + public OAuthException(String message) { + super(message); + } + + public OAuthException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/ajouchong/oauth/OAuthRequestDto.java b/src/main/java/com/ajouchong/oauth/OAuthRequestDto.java index 731ddab..0f50d76 100644 --- a/src/main/java/com/ajouchong/oauth/OAuthRequestDto.java +++ b/src/main/java/com/ajouchong/oauth/OAuthRequestDto.java @@ -1,9 +1,14 @@ package com.ajouchong.oauth; import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; @Data +@NoArgsConstructor +@AllArgsConstructor public class OAuthRequestDto { private String accessToken; -// private String expiresIn; + private String refreshToken; + private String expiresIn; } diff --git a/src/main/java/com/ajouchong/oauth/OAuthResponseDto.java b/src/main/java/com/ajouchong/oauth/OAuthResponseDto.java index 2b038b3..4a11222 100644 --- a/src/main/java/com/ajouchong/oauth/OAuthResponseDto.java +++ b/src/main/java/com/ajouchong/oauth/OAuthResponseDto.java @@ -3,10 +3,19 @@ import com.ajouchong.entity.Member; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor @AllArgsConstructor public class OAuthResponseDto { private String jwtToken; + private String refreshToken; private Member member; + private Long expiresIn; + + public OAuthResponseDto(String jwtToken, Member member) { + this.jwtToken = jwtToken; + this.member = member; + } } diff --git a/src/main/java/com/ajouchong/oauth/OAuthService.java b/src/main/java/com/ajouchong/oauth/OAuthService.java new file mode 100644 index 0000000..6e33b0b --- /dev/null +++ b/src/main/java/com/ajouchong/oauth/OAuthService.java @@ -0,0 +1,5 @@ +package com.ajouchong.oauth; + +public interface OAuthService { + GoogleUserDto getUserInfo(String accessToken); +} \ No newline at end of file