Skip to content

Commit

Permalink
Merge pull request #97 from depromeet/feature/#96
Browse files Browse the repository at this point in the history
[feat][#96]kakao login 완료
  • Loading branch information
sejoon00 authored Aug 30, 2024
2 parents 25a7a06 + 9d8ad76 commit 8b797f0
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 24 deletions.
3 changes: 3 additions & 0 deletions docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<LoginResponse> socialLogin(
Expand All @@ -41,9 +42,14 @@ public ResponseEntity<LoginResponse> 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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Object> properties,
KakaoAccount kakao_account
) {

public static KakaoOAuthUserInfoResponse from(Map<String, Object> 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<String, Object>) attributes.get("properties"),
KakaoAccount.from((Map<String, Object>) 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<String, Object> attributes) {
return new KakaoAccount(
Boolean.valueOf(String.valueOf(attributes.get("profile_nickname_needs_agreement"))),
Profile.from((Map<String, Object>) 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<String, Object> attributes) {
return new Profile(String.valueOf(attributes.get("nickname")));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand All @@ -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()); //기존 토큰 삭제
Expand Down Expand Up @@ -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);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,31 +36,33 @@ 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);
}
}

// 프론트와 연결끝나면 지워도 됨.
public String getToken(String code) {
public String getGoogleToken(String code) {

Map<String, String> params = new LinkedHashMap<>();
params.put("client_id", googleOAuthConfig.getGoogleClientId());
Expand All @@ -79,4 +81,6 @@ public String getToken(String code) {
assert response != null;
return response.accessToken();
}


}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(params, headers);

// 요청 URL 설정
String url = KakaoOAuthConfig.KAKAO_TOKEN_URI;

// POST 요청 전송 및 응답 수신
ResponseEntity<GoogleTokenServiceResponse> 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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/application-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
6 changes: 6 additions & 0 deletions src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 8b797f0

Please sign in to comment.