diff --git a/build.gradle b/build.gradle index 05fd25b..549096c 100644 --- a/build.gradle +++ b/build.gradle @@ -32,9 +32,12 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-oauth2-client") // JWT - implementation 'io.jsonwebtoken:jjwt-api:0.12.5' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5' + implementation("io.jsonwebtoken:jjwt-api:0.12.5") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.5") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.5") + + // webflux (for WebClient) + implementation 'org.springframework.boot:spring-boot-starter-webflux' // Lombok compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/moongeul/backend/api/member/controller/MemberController.java b/src/main/java/com/moongeul/backend/api/member/controller/MemberController.java index 9cc6989..3b677a6 100644 --- a/src/main/java/com/moongeul/backend/api/member/controller/MemberController.java +++ b/src/main/java/com/moongeul/backend/api/member/controller/MemberController.java @@ -3,15 +3,13 @@ import com.moongeul.backend.api.member.dto.LoginResponseDTO; import com.moongeul.backend.api.member.dto.LoginRequestDTO; import com.moongeul.backend.api.member.dto.UserInfoDTO; -import com.moongeul.backend.api.member.entity.Member; -import com.moongeul.backend.api.member.service.MemeberService; -import com.moongeul.backend.common.exception.BadRequestException; +import com.moongeul.backend.api.member.service.MemberService; import com.moongeul.backend.common.response.ApiResponse; -import com.moongeul.backend.common.response.ErrorStatus; import com.moongeul.backend.common.response.SuccessStatus; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -24,7 +22,7 @@ @RequestMapping("/api/v2/member") public class MemberController { - private final MemeberService memberService; + private final MemberService memberService; @Operation( summary = "로그인 API", @@ -36,11 +34,7 @@ public class MemberController { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "유효하지 않은 엑세스토큰 입니다.") }) @PostMapping("/login") - public ResponseEntity> loginWithGoogle(@RequestBody LoginRequestDTO loginRequestDTO) { - // 엑세스토큰이 입력되지 않았을 경우 예외 처리 - if (loginRequestDTO == null || loginRequestDTO.getCode() == null || loginRequestDTO.getCode().isEmpty()) { - throw new BadRequestException(ErrorStatus.MISSING_GOOGLE_ACCESSTOKEN.getMessage()); - } + public ResponseEntity> loginWithGoogle(@Valid @RequestBody LoginRequestDTO loginRequestDTO) { LoginResponseDTO response = memberService.loginWithGoogle(loginRequestDTO.getCode()); return ApiResponse.success(SuccessStatus.SEND_LOGIN_SUCCESS, response); @@ -56,8 +50,7 @@ public ResponseEntity> loginWithGoogle(@RequestBod }) @GetMapping("/user-info") public ResponseEntity> getUserInfo(@AuthenticationPrincipal UserDetails userDetails){ - Member member = memberService.getMemberByEmail(userDetails.getUsername()); - UserInfoDTO response = memberService.getUserInfo(member); + UserInfoDTO response = memberService.getUserInfo(userDetails.getUsername()); return ApiResponse.success(SuccessStatus.GET_USERINFO_SUCCESS, response); } diff --git a/src/main/java/com/moongeul/backend/api/member/dto/GoogleInfoResponseDTO.java b/src/main/java/com/moongeul/backend/api/member/dto/GoogleInfoResponseDTO.java new file mode 100644 index 0000000..e420aa8 --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/member/dto/GoogleInfoResponseDTO.java @@ -0,0 +1,14 @@ +package com.moongeul.backend.api.member.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class GoogleInfoResponseDTO { + + private String id; + private String email; + private String name; + private String picture; +} diff --git a/src/main/java/com/moongeul/backend/api/member/dto/GoogleTokenResponseDTO.java b/src/main/java/com/moongeul/backend/api/member/dto/GoogleTokenResponseDTO.java new file mode 100644 index 0000000..35b4477 --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/member/dto/GoogleTokenResponseDTO.java @@ -0,0 +1,14 @@ +package com.moongeul.backend.api.member.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class GoogleTokenResponseDTO { + + // JSON의 access_token 필드를 이 변수에 매핑 + @JsonProperty("access_token") + private String accessToken; +} diff --git a/src/main/java/com/moongeul/backend/api/member/dto/LoginRequestDTO.java b/src/main/java/com/moongeul/backend/api/member/dto/LoginRequestDTO.java index ab70de8..2df62f3 100644 --- a/src/main/java/com/moongeul/backend/api/member/dto/LoginRequestDTO.java +++ b/src/main/java/com/moongeul/backend/api/member/dto/LoginRequestDTO.java @@ -1,8 +1,11 @@ package com.moongeul.backend.api.member.dto; +import jakarta.validation.constraints.NotBlank; import lombok.Getter; @Getter public class LoginRequestDTO { + + @NotBlank(message = "구글 엑세스토큰이 입력되지 않았습니다.") private String code; // 인가코드 } diff --git a/src/main/java/com/moongeul/backend/api/member/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/com/moongeul/backend/api/member/jwt/filter/JwtAuthenticationFilter.java index aef2085..4c81e0b 100644 --- a/src/main/java/com/moongeul/backend/api/member/jwt/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/moongeul/backend/api/member/jwt/filter/JwtAuthenticationFilter.java @@ -35,7 +35,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha Authentication authentication = jwtTokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); } - chain.doFilter(request, response); + chain.doFilter(request, response); // 필터 체인(Filter Chain) 내의 다음 단계로 요청을 넘기는 것 } // Request Header에서 토큰 정보 추출 diff --git a/src/main/java/com/moongeul/backend/api/member/service/MemberService.java b/src/main/java/com/moongeul/backend/api/member/service/MemberService.java new file mode 100644 index 0000000..4d250a2 --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/member/service/MemberService.java @@ -0,0 +1,90 @@ +package com.moongeul.backend.api.member.service; + +import com.moongeul.backend.api.member.dto.GoogleInfoResponseDTO; +import com.moongeul.backend.api.member.dto.GoogleTokenResponseDTO; +import com.moongeul.backend.api.member.dto.UserInfoDTO; +import com.moongeul.backend.api.member.entity.Member; +import com.moongeul.backend.api.member.entity.Role; +import com.moongeul.backend.api.member.jwt.dto.JwtTokenDTO; +import com.moongeul.backend.api.member.dto.LoginResponseDTO; +import com.moongeul.backend.api.member.repository.MemberRepository; +import com.moongeul.backend.common.config.jwt.JwtTokenProvider; +import com.moongeul.backend.common.exception.NotFoundException; +import com.moongeul.backend.common.response.ErrorStatus; +import org.springframework.transaction.annotation.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class MemberService { + + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; + private final OAuthService oAuthService; + + // 인가코드 받아 JWT로 교환 및 회원가입/로그인 처리 + @Transactional + public LoginResponseDTO loginWithGoogle(String code){ + + // 1. 인가 코드로 Google Access Token 및 사용자 정보 획득 + GoogleTokenResponseDTO tokenResponse = oAuthService.getGoogleToken(code); + GoogleInfoResponseDTO userInfo = oAuthService.getGoogleUserInfo(tokenResponse.getAccessToken()); + + // 2. 사용자 정보 추출 + String socialId = userInfo.getId(); + String email = userInfo.getEmail(); + String name = userInfo.getName(); + String picture = userInfo.getPicture(); + + // 3. DB 처리 (회원가입 또는 로그인) + Member member = memberRepository.findBySocialId(socialId) + .map(entity -> entity.update(name, picture)) // 이미 있으면 정보 업데이트 + .orElseGet(() -> signUp(socialId, email, name, picture)); // 없으면 신규 회원가입 + + // 4. 자체 JWT 토큰 생성 및 반환 + JwtTokenDTO jwtToken = jwtTokenProvider.generateToken(member); + member.updateRefreshToken(jwtToken.getRefreshToken()); // 생성된 refreshToken DB 저장 + + return LoginResponseDTO.builder() + .role(member.getAuthorityKey()) + .accessToken(jwtToken.getAccessToken()) + .refreshToken(jwtToken.getRefreshToken()) + .build(); + } + + // 신규 회원가입 처리 로직 (DB 저장) + private Member signUp(String socialId, String email, String name, String picture) { + Member newUser = Member.builder() + .email(email) + .name(name) + .profileImage(picture) + .password("OAuth Password") // 임시 패스워드 + .socialId(socialId) // 예시 사용자명 생성 + .socialType("google") + .role(Role.GUEST) // 이후 필요 정보 모두 입력 시 USER 로 승격 + .build(); + return memberRepository.save(newUser); + } + + // 사용자 정보 조회 + @Transactional(readOnly = true) + public UserInfoDTO getUserInfo(String email){ + + Member member = getMemberByEmail(email); + + return UserInfoDTO.builder() + .id(member.getId()) + .name(member.getName()) + .profileImage(member.getProfileImage()) + .nickname(member.getNickname()) + .build(); + } + + private Member getMemberByEmail(String email) { + return memberRepository.findByEmail(email) + .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); + } +} diff --git a/src/main/java/com/moongeul/backend/api/member/service/MemeberService.java b/src/main/java/com/moongeul/backend/api/member/service/MemeberService.java deleted file mode 100644 index f6ad019..0000000 --- a/src/main/java/com/moongeul/backend/api/member/service/MemeberService.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.moongeul.backend.api.member.service; - -import com.moongeul.backend.api.member.dto.UserInfoDTO; -import com.moongeul.backend.api.member.entity.Member; -import com.moongeul.backend.api.member.entity.Role; -import com.moongeul.backend.api.member.jwt.dto.JwtTokenDTO; -import com.moongeul.backend.api.member.dto.LoginResponseDTO; -import com.moongeul.backend.api.member.repository.MemberRepository; -import com.moongeul.backend.common.config.jwt.JwtTokenProvider; -import com.moongeul.backend.common.exception.NotFoundException; -import com.moongeul.backend.common.response.ErrorStatus; -import org.springframework.transaction.annotation.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.*; -import org.springframework.stereotype.Service; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; - -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.Map; - -@Service -@RequiredArgsConstructor -@Slf4j -public class MemeberService { - - private final MemberRepository memberRepository; - private final JwtTokenProvider jwtTokenProvider; - private final RestTemplate restTemplate = new RestTemplate(); // Google 통신 객체 - - @Value("${spring.security.oauth2.client.registration.google.client-id}") - private String clientId; - @Value("${spring.security.oauth2.client.registration.google.client-secret}") - private String clientSecret; - @Value("${spring.security.oauth2.client.registration.google.redirect-uri}") - private String redirectUri; - - // 인가코드 받아 JWT로 교환 및 회원가입/로그인 처리 - @Transactional - public LoginResponseDTO loginWithGoogle(String code){ - - // 1. 인가 코드로 Google Access Token 및 사용자 정보 획득 - Map tokenResponse = getGoogleToken(code); - Map userInfo = getGoogleUserInfo(tokenResponse.get("access_token")); - - // 2. 사용자 정보 추출 - String socialId = (String) userInfo.get("sub"); - String email = (String) userInfo.get("email"); - String name = (String) userInfo.get("name"); - String picture = (String) userInfo.get("picture"); - - // 3. DB 처리 (회원가입 또는 로그인) - Member member = memberRepository.findBySocialId(socialId) - .map(entity -> entity.update(name, picture)) // 이미 있으면 정보 업데이트 - .orElseGet(() -> signUp(socialId, email, name, picture)); // 없으면 신규 회원가입 - - // 4. 자체 JWT 토큰 생성 및 반환 - JwtTokenDTO jwtToken = jwtTokenProvider.generateToken(member); - member.updateRefreshToken(jwtToken.getRefreshToken()); // 생성된 refreshToken DB 저장 - - return LoginResponseDTO.builder() - .role(member.getAuthorityKey()) - .accessToken(jwtToken.getAccessToken()) - .refreshToken(jwtToken.getRefreshToken()) - .build(); - } - - // 신규 회원가입 처리 로직 (DB 저장) - private Member signUp(String socialId, String email, String name, String picture) { - Member newUser = Member.builder() - .email(email) - .name(name) - .profileImage(picture) - .password("OAuth Password") // 임시 패스워드 - .socialId(socialId) // 예시 사용자명 생성 - .socialType("google") - .role(Role.GUEST) // 이후 필요 정보 모두 입력 시 USER 로 승격 - .build(); - return memberRepository.save(newUser); - } - - // Google 토큰 획득 로직 (생략: RestTemplate을 사용해 POST 요청) - private Map getGoogleToken(String code) { - - String decodedCode; - try { - // 인코딩된 code 값(예: %2F)을 원래 값(/)으로 디코딩합니다. - decodedCode = URLDecoder.decode(code, StandardCharsets.UTF_8); - } catch (Exception e) { - // 디코딩 실패 시 원본 코드를 사용하거나, 예외를 다시 던집니다. - log.error("URL Decoding Failed, using original code.", e); - decodedCode = code; - } - - // ... (실제 Google OAuth 2.0 /token 엔드포인트 통신 로직 구현 필요) - // 1. HTTP Body에 전송할 파라미터를 담습니다. - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("code", decodedCode); - params.add("client_id", clientId); - params.add("client_secret", clientSecret); - params.add("redirect_uri", redirectUri); - params.add("grant_type", "authorization_code"); // 인가 코드를 토큰으로 교환함을 명시 - - // 2. HTTP Header 설정 - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); // x-www-form-urlencoded 형식 지정 - - // 3. HttpEntity (Header + Body) 생성 - HttpEntity> httpEntity = new HttpEntity<>(params, headers); - - // 4. Google 토큰 엔드포인트 URL - String GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"; - - // 5. POST 요청 및 응답 받기 - try { - ResponseEntity responseEntity = restTemplate.exchange( - GOOGLE_TOKEN_URL, - HttpMethod.POST, - httpEntity, - Map.class - ); - - // 6. 응답에서 토큰 정보 추출 - if (responseEntity.getStatusCode().is2xxSuccessful() && responseEntity.getBody() != null) { - // Map으로 형변환하여 반환 - return (Map) responseEntity.getBody(); - } - } catch (HttpClientErrorException.BadRequest e) { // 💡 Google 400 응답을 여기서 잡습니다. - log.error("Google 400 Response Body: {}", e.getResponseBodyAsString()); - // 이 응답 본문에는 "invalid_grant"가 포함되어 있을 것입니다. - throw new RuntimeException("Google OAuth token exchange failed.", e); - } catch (Exception e) { - // ... (기타 네트워크 오류 처리) - throw new RuntimeException("Google OAuth token exchange failed.", e); - } - // 예외 발생 시 빈 맵 반환 (혹은 특정 예외 던지기) - throw new RuntimeException("Failed to get Google Token."); - } - - // Google 사용자 정보 획득 로직 (생략: RestTemplate을 사용해 GET 요청) - private Map getGoogleUserInfo(String googleAccessToken) { - String GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"; - - // 1. HTTP Header에 Google Access Token을 담습니다. - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(googleAccessToken); - - HttpEntity entity = new HttpEntity<>(headers); - - // 2. GET 요청 및 응답 받기 - try { - ResponseEntity responseEntity = restTemplate.exchange( - GOOGLE_USERINFO_URL, - HttpMethod.GET, - entity, - Map.class - ); - - if (responseEntity.getStatusCode().is2xxSuccessful() && responseEntity.getBody() != null) { - return (Map) responseEntity.getBody(); - } - } catch (Exception e) { - throw new RuntimeException("Failed to retrieve Google user info.", e); - } - - throw new RuntimeException("Failed to get Google User Info."); - } - - // 사용자 정보 조회 - public UserInfoDTO getUserInfo(Member member){ - - return UserInfoDTO.builder() - .id(member.getId()) - .name(member.getName()) - .profileImage(member.getProfileImage()) - .nickname(member.getNickname()) - .build(); - } - - @Transactional(readOnly = true) - public Member getMemberByEmail(String email) { - return memberRepository.findByEmail(email) - .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); - } -} diff --git a/src/main/java/com/moongeul/backend/api/member/service/OAuthService.java b/src/main/java/com/moongeul/backend/api/member/service/OAuthService.java new file mode 100644 index 0000000..e231172 --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/member/service/OAuthService.java @@ -0,0 +1,100 @@ +package com.moongeul.backend.api.member.service; + +import com.moongeul.backend.api.member.dto.GoogleInfoResponseDTO; +import com.moongeul.backend.api.member.dto.GoogleTokenResponseDTO; +import com.moongeul.backend.common.exception.BadRequestException; +import com.moongeul.backend.common.exception.InternalServerException; +import com.moongeul.backend.common.exception.UnauthorizedException; +import com.moongeul.backend.common.response.ErrorStatus; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; + +@Service +@RequiredArgsConstructor +@Slf4j +public class OAuthService { + + private static final String GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"; + private static final String GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"; + private final WebClient webClient; + + @Value("${spring.security.oauth2.client.registration.google.client-id}") + private String clientId; + @Value("${spring.security.oauth2.client.registration.google.client-secret}") + private String clientSecret; + @Value("${spring.security.oauth2.client.registration.google.redirect-uri}") + private String redirectUri; + + // Google 토큰 획득 로직 (WebClient 방식으로 수정 - 비동기 방식 구현) + public GoogleTokenResponseDTO getGoogleToken(String code) { + + String decodedCode; + try { + // 인코딩된 code 값(예: %2F)을 원래 값(/)으로 디코딩합니다. + decodedCode = URLDecoder.decode(code, StandardCharsets.UTF_8); + } catch (Exception e) { + // 디코딩 실패 시 원본 코드를 사용하거나, 예외를 다시 던집니다. + log.error("URL Decoding Failed, using original code.", e); + decodedCode = code; + } + + // ... (실제 Google OAuth 2.0 /token 엔드포인트 통신 로직 구현 필요) + // HTTP Body에 전송할 파라미터 담기 + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", decodedCode); + params.add("client_id", clientId); + params.add("client_secret", clientSecret); + params.add("redirect_uri", redirectUri); + params.add("grant_type", "authorization_code"); // 인가 코드를 토큰으로 교환함을 명시 + + return webClient.post() + .uri(GOOGLE_TOKEN_URL) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(BodyInserters.fromFormData(params)) + .retrieve() + .onStatus(status -> status.value() == 401, response -> { + throw new UnauthorizedException(ErrorStatus.GOOGLE_AUTH_UNAUTHORIZED.getMessage()); + }) + .onStatus(HttpStatusCode::is4xxClientError, response -> { + throw new BadRequestException(ErrorStatus.INVALID_TOKEN_REQUEST.getMessage()); + }) + .onStatus(HttpStatusCode::is5xxServerError, response -> { + throw new InternalServerException(ErrorStatus.SERVER_ERROR.getMessage()); + }) + .bodyToMono(GoogleTokenResponseDTO.class) + .block(); + } + + // Google 사용자 정보 획득 로직 (생략: RestTemplate을 사용해 GET 요청) + public GoogleInfoResponseDTO getGoogleUserInfo(String googleAccessToken) { + + return webClient.get() + .uri(GOOGLE_USERINFO_URL) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + googleAccessToken) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .onStatus(status -> status.value() == 401, response -> { + throw new UnauthorizedException(ErrorStatus.GOOGLE_AUTH_UNAUTHORIZED.getMessage()); + }) + .onStatus(HttpStatusCode::is4xxClientError, response -> { + throw new BadRequestException(ErrorStatus.INVALID_INFO_REQUEST.getMessage()); + }) + .onStatus(HttpStatusCode::is5xxServerError, response -> { + throw new InternalServerException(ErrorStatus.SERVER_ERROR.getMessage()); + }) + .bodyToMono(GoogleInfoResponseDTO.class) + .block(); // 동기 방식으로 결과 대기 + } +} diff --git a/src/main/java/com/moongeul/backend/common/config/jwt/JwtTokenProvider.java b/src/main/java/com/moongeul/backend/common/config/jwt/JwtTokenProvider.java index 59daaf4..db67875 100644 --- a/src/main/java/com/moongeul/backend/common/config/jwt/JwtTokenProvider.java +++ b/src/main/java/com/moongeul/backend/common/config/jwt/JwtTokenProvider.java @@ -15,7 +15,7 @@ import org.springframework.stereotype.Component; import org.springframework.security.core.GrantedAuthority; -import java.security.Key; +import javax.crypto.SecretKey; import java.util.Arrays; import java.util.Collection; import java.util.Date; @@ -29,7 +29,7 @@ @Slf4j @Component public class JwtTokenProvider { - private final Key key; + private final SecretKey key; // application.yml에서 secret 값 가져와서 key에 저장 // 1. 비밀 열쇠 보관 (생성자) : 토큰(신분증)을 만들고 검사할 때 쓰는 비밀 열쇠 @@ -57,16 +57,16 @@ public JwtTokenDTO generateToken(Member member) { // Access Token 생성 Date accessTokenExpiresIn = new Date(now + accessTokenExpirationPeriod); String accessToken = Jwts.builder() - .setSubject(subject) // 이메일 + .subject(subject) // 이메일 .claim("auth", authorities) // 권한 - .setExpiration(accessTokenExpiresIn) - .signWith(key, SignatureAlgorithm.HS256) + .expiration(accessTokenExpiresIn) + .signWith(key) .compact(); // Refresh Token 생성 String refreshToken = Jwts.builder() - .setExpiration(new Date(now + refreshTokenExpirationPeriod)) - .signWith(key, SignatureAlgorithm.HS256) + .expiration(new Date(now + refreshTokenExpirationPeriod)) + .signWith(key) .compact(); return JwtTokenDTO.builder() @@ -81,9 +81,9 @@ public JwtTokenDTO generateToken(Member member) { public boolean validateToken(String token) { try { Jwts.parser() - .setSigningKey(key) + .verifyWith(key) .build() - .parseClaimsJws(token); + .parseSignedClaims(token); return true; } catch (SecurityException | MalformedJwtException e) { log.info("Invalid JWT Token", e); @@ -123,10 +123,10 @@ public Authentication getAuthentication(String accessToken) { private Claims parseClaims(String accessToken) { try { return Jwts.parser() - .setSigningKey(key) + .verifyWith(key) .build() - .parseClaimsJws(accessToken) - .getBody(); + .parseSignedClaims(accessToken) + .getPayload(); } catch (ExpiredJwtException e) { return e.getClaims(); } diff --git a/src/main/java/com/moongeul/backend/common/config/oauth2/PrincipalDetails.java b/src/main/java/com/moongeul/backend/common/config/oauth2/PrincipalDetails.java deleted file mode 100644 index f841ab9..0000000 --- a/src/main/java/com/moongeul/backend/common/config/oauth2/PrincipalDetails.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.moongeul.backend.common.config.oauth2; - -import com.moongeul.backend.api.member.entity.Member; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.oauth2.core.user.OAuth2User; - -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/* -* PrincipalDetails는 데이터베이스의 Member Entity 정보를 담고, -* Spring Security 경비 시스템이 인증과 권한 부여를 처리할 수 있도록 규격화된 명함 역할을 하는 핵심 보안 객체 -*/ - -@Getter -@AllArgsConstructor -@RequiredArgsConstructor -public class PrincipalDetails implements UserDetails, OAuth2User { - - private Member member; - private String username; - private Map attributes; - - //일반 로그인 - public PrincipalDetails(Member member) { - this.member = member; - } - - //OAuth 로그인 - public PrincipalDetails(Member member, Map attributes) { - this.member = member; - } - - @Override - public A getAttribute(String name) { - return OAuth2User.super.getAttribute(name); - } - - @Override - public Map getAttributes() { - return attributes; - } - - @Override - public Collection getAuthorities() { - return List.of(new SimpleGrantedAuthority(member.getAuthorityKey())); - } - - public Optional getMember() { - // member 필드가 null이 될 수 있으므로, ofNullable을 사용하여 Optional로 감싸서 반환합니다. - return Optional.ofNullable(this.member); - } - - @Override - public String getPassword() { - return member.getPassword(); - } - - @Override - public String getUsername() { - return member.getEmail(); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } - - public String getEmail() { - return member.getEmail(); - } - - @Override - public String getName() { - return null; - } -} \ No newline at end of file diff --git a/src/main/java/com/moongeul/backend/common/config/oauth2/SecurityConfig.java b/src/main/java/com/moongeul/backend/common/config/oauth2/SecurityConfig.java index f91b0f3..8c8a538 100644 --- a/src/main/java/com/moongeul/backend/common/config/oauth2/SecurityConfig.java +++ b/src/main/java/com/moongeul/backend/common/config/oauth2/SecurityConfig.java @@ -7,6 +7,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -29,6 +30,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 기존 보안 설정... (예: CSRF 비활성화, 인가 설정) http .csrf(csrf -> csrf.disable()) // 예시 + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 무상태 설정 .headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable())) //h2-console 화면 깨짐 방지(iframe 렌더링 오류) .authorizeHttpRequests(auth -> auth .requestMatchers("/", "/api/v2/member/login", "/h2-console/**").permitAll() diff --git a/src/main/java/com/moongeul/backend/common/config/webclient/WebClientConfig.java b/src/main/java/com/moongeul/backend/common/config/webclient/WebClientConfig.java new file mode 100644 index 0000000..79964c2 --- /dev/null +++ b/src/main/java/com/moongeul/backend/common/config/webclient/WebClientConfig.java @@ -0,0 +1,14 @@ +package com.moongeul.backend.common.config.webclient; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.create(); + } +} \ No newline at end of file diff --git a/src/main/java/com/moongeul/backend/common/response/ErrorStatus.java b/src/main/java/com/moongeul/backend/common/response/ErrorStatus.java index 9f61a82..b6e2dc6 100644 --- a/src/main/java/com/moongeul/backend/common/response/ErrorStatus.java +++ b/src/main/java/com/moongeul/backend/common/response/ErrorStatus.java @@ -15,10 +15,13 @@ public enum ErrorStatus { VALIDATION_REQUEST_MISSING_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 값이 입력되지 않았습니다."), USER_ALREADY_EXISTS_EXCEPTION(HttpStatus.BAD_REQUEST,"이미 존재하는 사용자입니다."), MISSING_GOOGLE_ACCESSTOKEN(HttpStatus.BAD_REQUEST, "구글 엑세스토큰이 입력되지 않았습니다."), + INVALID_TOKEN_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 토큰 요청입니다."), + INVALID_INFO_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 로그인 인증 요청입니다."), /** * 401 UNAUTHORIZED */ + GOOGLE_AUTH_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "구글 인증에 실패했습니다.(토큰 문제)"), /** * 404 NOT_FOUND @@ -29,6 +32,7 @@ public enum ErrorStatus { * 500 SERVER_ERROR */ INTERNAL_SERVER_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR,"서버 내부 오류 발생"), + SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "로그인 서버 오류 발생") ;