diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 7be8af6..1e36f40 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -27,6 +27,9 @@ services: - GOOGLE_LOGIN_CLIENT_ID=${GOOGLE_LOGIN_CLIENT_ID} - GOOGLE_LOGIN_CLIENT_SECRET=${GOOGLE_LOGIN_CLIENT_SECRET} - GOOGLE_LOGIN_REDIRECT_URI=${GOOGLE_LOGIN_REDIRECT_URI} + - KAKAO_LOGIN_CLIENT_ID=${KAKAO_LOGIN_CLIENT_ID} + - KAKAO_LOGIN_CLIENT_SECRET=${KAKAO_LOGIN_CLIENT_SECRET} + - KAKAO_LOGIN_REDIRECT_URI=${KAKAO_LOGIN_REDIRECT_URI} - SENTRY_DSN=${SENTRY_DSN} depends_on: - mysql diff --git a/src/main/java/com/server/bbo_gak/domain/auth/controller/AuthController.java b/src/main/java/com/server/bbo_gak/domain/auth/controller/AuthController.java index 6b96021..ef4e6d0 100644 --- a/src/main/java/com/server/bbo_gak/domain/auth/controller/AuthController.java +++ b/src/main/java/com/server/bbo_gak/domain/auth/controller/AuthController.java @@ -1,10 +1,11 @@ package com.server.bbo_gak.domain.auth.controller; import com.server.bbo_gak.domain.auth.dto.request.LoginRequest; -import com.server.bbo_gak.domain.auth.dto.response.LoginResponse; import com.server.bbo_gak.domain.auth.dto.request.RefreshTokenRequest; +import com.server.bbo_gak.domain.auth.dto.response.LoginResponse; import com.server.bbo_gak.domain.auth.service.AuthService; import com.server.bbo_gak.domain.auth.service.oauth.GoogleService; +import com.server.bbo_gak.domain.auth.service.oauth.KakaoService; import com.server.bbo_gak.domain.user.entity.User; import com.server.bbo_gak.global.annotation.AuthUser; import com.server.bbo_gak.global.security.jwt.dto.TokenDto; @@ -23,10 +24,10 @@ @RequiredArgsConstructor public class AuthController { + private static final String SOCIAL_TOKEN_NAME = "SOCIAL-AUTH-TOKEN"; private final AuthService authService; private final GoogleService googleService; - - private static final String SOCIAL_TOKEN_NAME = "SOCIAL-AUTH-TOKEN"; + private final KakaoService kakaoService; @PostMapping("/social-login") public ResponseEntity socialLogin( @@ -41,9 +42,14 @@ public ResponseEntity socialLogin( /** * 프론트 연결 끝나면 지우기 */ - @PostMapping("/test/access-token") + @PostMapping("/test/google/access-token") public String googleTest(@RequestParam("code") String code) { - return googleService.getToken(code); + return googleService.getGoogleToken(code); + } + + @PostMapping("/test/kakao/access-token") + public String kakaoTest(@RequestParam("code") String code) { + return kakaoService.getKakaoToken(code); } @PostMapping("/test/login") diff --git a/src/main/java/com/server/bbo_gak/domain/auth/dto/response/oauth/KakaoOAuthUserInfoResponse.java b/src/main/java/com/server/bbo_gak/domain/auth/dto/response/oauth/KakaoOAuthUserInfoResponse.java new file mode 100644 index 0000000..73d0307 --- /dev/null +++ b/src/main/java/com/server/bbo_gak/domain/auth/dto/response/oauth/KakaoOAuthUserInfoResponse.java @@ -0,0 +1,68 @@ +package com.server.bbo_gak.domain.auth.dto.response.oauth; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Map; + +public record KakaoOAuthUserInfoResponse( + Long id, + LocalDateTime connected_at, + Map properties, + KakaoAccount kakao_account +) { + + public static KakaoOAuthUserInfoResponse from(Map attributes) { + return new KakaoOAuthUserInfoResponse( + Long.valueOf(String.valueOf(attributes.get("id"))), + LocalDateTime.parse( + String.valueOf(attributes.get("connected_at")), + DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.systemDefault()) + ), + (Map) attributes.get("properties"), + KakaoAccount.from((Map) attributes.get("kakao_account")) + ); + } + + public String email() { + return this.kakao_account().email(); + } + + public String nickname() { + return this.kakao_account().nickname(); + } + + public record KakaoAccount( + Boolean profileNicknameNeedsAgreement, + Profile profile, + Boolean hasEmail, + Boolean emailNeedsAgreement, + Boolean isEmailValid, + Boolean isEmailVerified, + String email + ) { + + public static KakaoAccount from(Map attributes) { + return new KakaoAccount( + Boolean.valueOf(String.valueOf(attributes.get("profile_nickname_needs_agreement"))), + Profile.from((Map) attributes.get("profile")), + Boolean.valueOf(String.valueOf(attributes.get("has_email"))), + Boolean.valueOf(String.valueOf(attributes.get("email_needs_agreement"))), + Boolean.valueOf(String.valueOf(attributes.get("is_email_valid"))), + Boolean.valueOf(String.valueOf(attributes.get("is_email_verified"))), + String.valueOf(attributes.get("email")) + ); + } + + public String nickname() { + return this.profile().nickname(); + } + + public record Profile(String nickname) { + + public static Profile from(Map attributes) { + return new Profile(String.valueOf(attributes.get("nickname"))); + } + } + } +} diff --git a/src/main/java/com/server/bbo_gak/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/server/bbo_gak/domain/auth/service/AuthServiceImpl.java index 6c0823d..ecd210e 100644 --- a/src/main/java/com/server/bbo_gak/domain/auth/service/AuthServiceImpl.java +++ b/src/main/java/com/server/bbo_gak/domain/auth/service/AuthServiceImpl.java @@ -1,12 +1,13 @@ package com.server.bbo_gak.domain.auth.service; import com.server.bbo_gak.domain.auth.dto.request.LoginRequest; +import com.server.bbo_gak.domain.auth.dto.request.RefreshTokenRequest; import com.server.bbo_gak.domain.auth.dto.response.LoginResponse; import com.server.bbo_gak.domain.auth.dto.response.oauth.OauthUserInfoResponse; -import com.server.bbo_gak.domain.auth.dto.request.RefreshTokenRequest; import com.server.bbo_gak.domain.auth.entity.AuthTestUser; import com.server.bbo_gak.domain.auth.entity.AuthTestUserRepository; import com.server.bbo_gak.domain.auth.service.oauth.GoogleService; +import com.server.bbo_gak.domain.auth.service.oauth.KakaoService; import com.server.bbo_gak.domain.user.entity.Job; import com.server.bbo_gak.domain.user.entity.OauthProvider; import com.server.bbo_gak.domain.user.entity.User; @@ -32,6 +33,7 @@ public class AuthServiceImpl implements AuthService { private final RefreshTokenRepository refreshTokenRepository; private final JwtTokenService jwtTokenService; private final GoogleService googleService; + private final KakaoService kakaoService; private final UserService userService; private final UserRepository userRepository; @@ -44,8 +46,7 @@ public LoginResponse socialLogin(String socialAccessToken, String provider) { // DB에서 회원 찾기 User user = userRepository.findUserByOauthInfo(oauthUserInfo.toEntity()) - .orElseGet(() -> userService.createUser(oauthUserInfo)); //DB에 회원이 없으면 회원가입 - + .orElseGet(() -> userService.createUser(oauthUserInfo)); //DB에 회원이 없으면 회원가입 if (refreshTokenRepository.existsRefreshTokenByMemberId(user.getId())) { refreshTokenRepository.deleteById(user.getId()); //기존 토큰 삭제 @@ -104,6 +105,7 @@ public void logout(User user) { private OauthUserInfoResponse getMemberInfo(String socialAccessToken, OauthProvider provider) { return switch (provider) { case GOOGLE -> googleService.getOauthUserInfo(socialAccessToken); + case KAKAO -> kakaoService.getOauthUserInfo(socialAccessToken); }; } } diff --git a/src/main/java/com/server/bbo_gak/domain/auth/service/oauth/GoogleService.java b/src/main/java/com/server/bbo_gak/domain/auth/service/oauth/GoogleService.java index e5841f2..e43db74 100644 --- a/src/main/java/com/server/bbo_gak/domain/auth/service/oauth/GoogleService.java +++ b/src/main/java/com/server/bbo_gak/domain/auth/service/oauth/GoogleService.java @@ -36,23 +36,25 @@ public OauthUserInfoResponse getOauthUserInfo(String accessToken) { return new OauthUserInfoResponse(response.id(), response.email(), response.name(), GOOGLE); } - private GoogleOauthUserInfoResponse getGoogleOauthUserInfo(String accessToken){ + private GoogleOauthUserInfoResponse getGoogleOauthUserInfo(String accessToken) { try { RestClient restClient = RestClient.create(); return restClient.get() - .uri(GOOGLE_USER_INFO_URI) - .header(AUTHORIZATION, TOKEN_PREFIX + accessToken) - .header("Content-type", "application/x-www-form-urlencoded;charset=utf-8") - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, - (googleRequest, googleResponse) -> { - throw new BusinessException("Client error: " + googleResponse.getStatusCode(), AUTH_GET_USER_INFO_FAILED); - }) - .onStatus(HttpStatusCode::is5xxServerError, (googleRequest, googleResponse) -> { - throw new BusinessException("Server error: " + googleResponse.getStatusCode(), AUTH_GET_USER_INFO_FAILED); + .uri(GOOGLE_USER_INFO_URI) + .header(AUTHORIZATION, TOKEN_PREFIX + accessToken) + .header("Content-type", "application/x-www-form-urlencoded;charset=utf-8") + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, + (googleRequest, googleResponse) -> { + throw new BusinessException("Client error: " + googleResponse.getStatusCode(), + AUTH_GET_USER_INFO_FAILED); }) - .body(GoogleOauthUserInfoResponse.class); - } catch (RestClientException e) { // RestClient 관련 에러 + .onStatus(HttpStatusCode::is5xxServerError, (googleRequest, googleResponse) -> { + throw new BusinessException("Server error: " + googleResponse.getStatusCode(), + AUTH_GET_USER_INFO_FAILED); + }) + .body(GoogleOauthUserInfoResponse.class); + } catch (RestClientException e) { // RestClient 관련 에러 throw new BusinessException("RestClientException: " + e.getMessage(), AUTH_GET_USER_INFO_FAILED); } catch (Exception e) { // 그 외 일반적인 예외 throw new BusinessException("Unexpected error: " + e.getMessage(), AUTH_GET_USER_INFO_FAILED); @@ -60,7 +62,7 @@ private GoogleOauthUserInfoResponse getGoogleOauthUserInfo(String accessToken){ } // 프론트와 연결끝나면 지워도 됨. - public String getToken(String code) { + public String getGoogleToken(String code) { Map params = new LinkedHashMap<>(); params.put("client_id", googleOAuthConfig.getGoogleClientId()); @@ -79,4 +81,6 @@ public String getToken(String code) { assert response != null; return response.accessToken(); } + + } diff --git a/src/main/java/com/server/bbo_gak/domain/auth/service/oauth/KakaoService.java b/src/main/java/com/server/bbo_gak/domain/auth/service/oauth/KakaoService.java new file mode 100644 index 0000000..7c77522 --- /dev/null +++ b/src/main/java/com/server/bbo_gak/domain/auth/service/oauth/KakaoService.java @@ -0,0 +1,99 @@ +package com.server.bbo_gak.domain.auth.service.oauth; + +import static com.server.bbo_gak.domain.user.entity.OauthProvider.KAKAO; +import static com.server.bbo_gak.global.config.oauth.GoogleOAuthConfig.AUTHORIZATION; +import static com.server.bbo_gak.global.error.exception.ErrorCode.AUTH_GET_USER_INFO_FAILED; + +import com.server.bbo_gak.domain.auth.dto.response.oauth.GoogleTokenServiceResponse; +import com.server.bbo_gak.domain.auth.dto.response.oauth.KakaoOAuthUserInfoResponse; +import com.server.bbo_gak.domain.auth.dto.response.oauth.OauthUserInfoResponse; +import com.server.bbo_gak.global.config.oauth.KakaoOAuthConfig; +import com.server.bbo_gak.global.error.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@RequiredArgsConstructor +@Service +public class KakaoService implements OauthService { + + private final KakaoOAuthConfig kakaoOAuthConfig; + + @Override + public OauthUserInfoResponse getOauthUserInfo(String accessToken) { + KakaoOAuthUserInfoResponse response = getKakaoOauthUserInfo(accessToken); + return new OauthUserInfoResponse(response.id().toString(), response.email(), response.nickname(), KAKAO); + } + + private KakaoOAuthUserInfoResponse getKakaoOauthUserInfo(String accessToken) { + try { + RestClient restClient = RestClient.create(); + return restClient.get() + .uri(KakaoOAuthConfig.KAKAO_USER_INFO_URI) + .header(AUTHORIZATION, KakaoOAuthConfig.TOKEN_PREFIX + accessToken) + .header("Content-type", "application/x-www-form-urlencoded;charset=utf-8") + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, + (googleRequest, googleResponse) -> { + throw new BusinessException("Client error: " + googleResponse.getStatusCode(), + AUTH_GET_USER_INFO_FAILED); + }) + .onStatus(HttpStatusCode::is5xxServerError, (googleRequest, googleResponse) -> { + throw new BusinessException("Server error: " + googleResponse.getStatusCode(), + AUTH_GET_USER_INFO_FAILED); + }) + .body(KakaoOAuthUserInfoResponse.class); + } catch (RestClientException e) { // RestClient 관련 에러 + throw new BusinessException("RestClientException: " + e.getMessage(), AUTH_GET_USER_INFO_FAILED); + } catch (Exception e) { // 그 외 일반적인 예외 + throw new BusinessException("Unexpected error: " + e.getMessage(), AUTH_GET_USER_INFO_FAILED); + } + + + } + + // 프론트와 연결끝나면 지워도 됨. + public String getKakaoToken(String code) { + RestTemplate restTemplate = new RestTemplate(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("client_id", kakaoOAuthConfig.getKakaoClientId()); + params.add("client_secret", kakaoOAuthConfig.getKakaoClientSecret()); + params.add("code", code); + params.add("grant_type", "authorization_code"); + params.add("redirect_uri", kakaoOAuthConfig.getKakaoRedirectUri()); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + // HttpEntity 생성 + HttpEntity> requestEntity = new HttpEntity<>(params, headers); + + // 요청 URL 설정 + String url = KakaoOAuthConfig.KAKAO_TOKEN_URI; + + // POST 요청 전송 및 응답 수신 + ResponseEntity responseEntity = restTemplate.postForEntity(url, requestEntity, + GoogleTokenServiceResponse.class); + + // 응답 검증 + if (responseEntity.getStatusCode().is2xxSuccessful()) { + GoogleTokenServiceResponse response = responseEntity.getBody(); + assert response != null; + return response.accessToken(); + } else { + // 오류 처리 로직 추가 + throw new RuntimeException("Failed to retrieve token: " + responseEntity.getStatusCode()); + } + } +} diff --git a/src/main/java/com/server/bbo_gak/domain/user/entity/OauthProvider.java b/src/main/java/com/server/bbo_gak/domain/user/entity/OauthProvider.java index 09bebf0..4d6b237 100644 --- a/src/main/java/com/server/bbo_gak/domain/user/entity/OauthProvider.java +++ b/src/main/java/com/server/bbo_gak/domain/user/entity/OauthProvider.java @@ -5,7 +5,8 @@ import java.util.Arrays; public enum OauthProvider { - GOOGLE; + GOOGLE, + KAKAO; public static OauthProvider findByName(String name) { return Arrays.stream(OauthProvider.values()) diff --git a/src/main/java/com/server/bbo_gak/global/config/oauth/KakaoOAuthConfig.java b/src/main/java/com/server/bbo_gak/global/config/oauth/KakaoOAuthConfig.java new file mode 100644 index 0000000..79cfa0b --- /dev/null +++ b/src/main/java/com/server/bbo_gak/global/config/oauth/KakaoOAuthConfig.java @@ -0,0 +1,25 @@ +package com.server.bbo_gak.global.config.oauth; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Getter +@Configuration +public class KakaoOAuthConfig { + + public static final String AUTHORIZATION = "Authorization"; + public static final String TOKEN_PREFIX = "Bearer "; + public static final String KAKAO_CODE_URI = "https://kauth.kakao.com/oauth/authorize"; + public static final String KAKAO_TOKEN_URI = "https://kauth.kakao.com/oauth/token"; + public static final String KAKAO_USER_INFO_URI = "https://kapi.kakao.com/v2/user/me"; + + @Value("${kakao.login.client_id}") + private String kakaoClientId; + + @Value("${kakao.login.client_secret}") + private String kakaoClientSecret; + + @Value("${kakao.login.redirect_uri}") + private String kakaoRedirectUri; +} diff --git a/src/main/java/com/server/bbo_gak/global/config/security/SecurityConfig.java b/src/main/java/com/server/bbo_gak/global/config/security/SecurityConfig.java index 6de60df..55d2bd6 100644 --- a/src/main/java/com/server/bbo_gak/global/config/security/SecurityConfig.java +++ b/src/main/java/com/server/bbo_gak/global/config/security/SecurityConfig.java @@ -28,7 +28,7 @@ public class SecurityConfig { private final JwtTokenService jwtTokenService; private String[] allowUrls = {"/", "/api/v1/users/test/login", "/docs/**", "/v3/**", "/favicon.ico", - "/api/v1/users/refreshToken", "/api/v1/users/social-login", "/api/v1/users/test/access-token", + "/api/v1/users/refreshToken", "/api/v1/users/social-login", "/api/v1/users/test/**", "/api/docs/**", "/api/v3/**", "/api/health-check/**"}; @Bean diff --git a/src/main/resources/application-security.yml b/src/main/resources/application-security.yml index c58a09b..7af77de 100644 --- a/src/main/resources/application-security.yml +++ b/src/main/resources/application-security.yml @@ -10,3 +10,9 @@ google: client_id: ${GOOGLE_LOGIN_CLIENT_ID} client_secret: ${GOOGLE_LOGIN_CLIENT_SECRET} redirect_uri: ${GOOGLE_LOGIN_REDIRECT_URI} + +kakao: + login: + client_id: ${KAKAO_LOGIN_CLIENT_ID} + client_secret: ${KAKAO_LOGIN_CLIENT_SECRET} + redirect_uri: ${KAKAO_LOGIN_REDIRECT_URI} \ No newline at end of file diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 075d1ee..443db87 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -44,6 +44,12 @@ google: client_secret: test redirect_uri: test +kakao: + login: + client_id: test + client_secret: test + redirect_uri: test + scheduler: thread: pool: