Skip to content

Commit

Permalink
feat: OAuth(Kakao) Service를 구현한다. (#15)
Browse files Browse the repository at this point in the history
* feat: RestTemplate Config를 추가한다.

* feat: Kakao 서버에 User Data를 요청하는 Handler를 추가한다.

* feat: KakaoHandler를 호출하는 OAuthService를 추가한다.

* feat: 요청/응답에 필요한 dto 추가

* feat: 신규 ErrorCode를 추가한다.

* feat: 공통 설정을 application.yml로 분리
  • Loading branch information
rlarltj authored Aug 2, 2024
1 parent 1d0890e commit 3ee54ef
Show file tree
Hide file tree
Showing 13 changed files with 288 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.dnd.accompany.domain.auth.oauth.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class KakaoUserData {
@JsonProperty("id")
private Long id;
@JsonProperty("kakao_account")
private KakaoAccount kakaoAccount;

@Getter
@NoArgsConstructor
static class KakaoAccount {
private String email;
private KakaoProfile profile;
}

@Getter
@NoArgsConstructor
static class KakaoProfile {
private String nickname;
}

public String getEmail() {
return kakaoAccount.getEmail();
}

public String getNickname() {
return kakaoAccount.getProfile().getNickname();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.accompany.domain.auth.oauth.dto;

import com.dnd.accompany.domain.auth.oauth.service.OAuthProvider;

public record LoginRequest(
OAuthProvider provider,
String accessToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.dnd.accompany.domain.auth.oauth.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class OAuthUserDataRequest {
private String accessToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.dnd.accompany.domain.auth.oauth.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OAuthUserDataResponse {
private String provider;
private String oauthId;
private String email;
private String nickname;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.dnd.accompany.domain.auth.oauth.exception;

import com.dnd.accompany.global.common.exception.BusinessException;
import com.dnd.accompany.global.common.response.ErrorCode;

public class HttpClientException extends BusinessException {
public HttpClientException(ErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.dnd.accompany.domain.auth.oauth.handler;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;

import com.dnd.accompany.domain.auth.oauth.dto.KakaoUserData;
import com.dnd.accompany.domain.auth.oauth.dto.OAuthUserDataRequest;
import com.dnd.accompany.domain.auth.oauth.dto.OAuthUserDataResponse;
import com.dnd.accompany.domain.auth.oauth.exception.HttpClientException;
import com.dnd.accompany.domain.auth.oauth.service.OAuthProvider;
import com.dnd.accompany.global.common.response.ErrorCode;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class KakaoService implements OAuthAuthenticationHandler {

private final RestTemplate restTemplate;

@Value("${spring.oauth2.kakao.host}")
private String host;

@Override
public OAuthProvider getAuthProvider() {
return OAuthProvider.KAKAO;
}

@Override
public OAuthUserDataResponse getOAuthUserData(OAuthUserDataRequest request) {
String url = host + "/v2/user/me";

HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
httpHeaders.add("Authorization", "Bearer " + request.getAccessToken());

HttpEntity<?> httpRequest = new HttpEntity<>(null, httpHeaders);

try {
ResponseEntity<KakaoUserData> response = restTemplate.exchange(
url,
HttpMethod.GET,
httpRequest,
KakaoUserData.class
);
assert response.getBody() != null;

KakaoUserData userData = response.getBody();
return OAuthUserDataResponse.builder()
.provider(getAuthProvider().toString())
.oauthId(userData.getId().toString())
.email(userData.getEmail())
.nickname(userData.getNickname())
.build();

} catch (RestClientException e) {
log.warn("[KakaoService] failed to get OAuth User Data = {}", request.getAccessToken());

if (e instanceof RestClientResponseException) {
throw new HttpClientException(ErrorCode.INVALID_OAUTH_TOKEN);
}

throw new HttpClientException(ErrorCode.HTTP_CLIENT_REQUEST_FAILED);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.dnd.accompany.domain.auth.oauth.handler;

import com.dnd.accompany.domain.auth.oauth.dto.OAuthUserDataRequest;
import com.dnd.accompany.domain.auth.oauth.dto.OAuthUserDataResponse;
import com.dnd.accompany.domain.auth.oauth.service.OAuthProvider;

public interface OAuthAuthenticationHandler {
OAuthProvider getAuthProvider();

OAuthUserDataResponse getOAuthUserData(OAuthUserDataRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.dnd.accompany.domain.auth.oauth.service;

import java.util.Arrays;

import com.dnd.accompany.global.common.exception.NotFoundException;
import com.dnd.accompany.global.common.response.ErrorCode;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum OAuthProvider {

KAKAO("KAKAO");

private final String name;

public static OAuthProvider get(OAuthProvider oAuthProvider) {
return Arrays.stream(OAuthProvider.values())
.filter(provider -> provider.equals(oAuthProvider))
.findAny()
.orElseThrow(() -> new NotFoundException(ErrorCode.INVALID_PROVIDER));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.dnd.accompany.domain.auth.oauth.service;

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;

import com.dnd.accompany.domain.auth.oauth.dto.LoginRequest;
import com.dnd.accompany.domain.auth.oauth.dto.OAuthUserDataRequest;
import com.dnd.accompany.domain.auth.oauth.dto.OAuthUserDataResponse;
import com.dnd.accompany.domain.auth.oauth.handler.OAuthAuthenticationHandler;

@Service
public class OAuthService {

private final Map<OAuthProvider, OAuthAuthenticationHandler> oAuthAuthenticationHandlers;

public OAuthService(List<OAuthAuthenticationHandler> oAuthAuthenticationHandlers) {
this.oAuthAuthenticationHandlers = oAuthAuthenticationHandlers.stream().collect(
Collectors.toConcurrentMap(OAuthAuthenticationHandler::getAuthProvider, Function.identity())
);
}

public OAuthUserDataResponse login(LoginRequest loginRequest) {
OAuthProvider oAuthProvider = OAuthProvider.get(loginRequest.provider());

OAuthAuthenticationHandler oAuthHandler = this.oAuthAuthenticationHandlers.get(oAuthProvider);

OAuthUserDataRequest request = new OAuthUserDataRequest(
loginRequest.accessToken()
);

return oAuthHandler.getOAuthUserData(request);
}

public void revoke() {
// 회원 탈퇴 구현 시 추가합니다.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ public enum ErrorCode {
INVALID_TOKEN(MatripConstant.UNAUTHORIZED, "TOKEN-001", "유효하지 않은 토큰입니다."),
TOKEN_EXPIRED(MatripConstant.UNAUTHORIZED, "TOKEN-002", "만료된 토큰입니다."),
REFRESH_TOKEN_NOT_FOUND(MatripConstant.NOT_FOUND, "TOKEN-003", "리프레시 토큰을 찾을 수 없습니다."),
REFRESH_TOKEN_EXPIRED(MatripConstant.UNAUTHORIZED, "TOKEN-004", "만료된 리프레시 토큰입니다.");
REFRESH_TOKEN_EXPIRED(MatripConstant.UNAUTHORIZED, "TOKEN-004", "만료된 리프레시 토큰입니다."),

// ---- 로그인 ---- //
INVALID_PROVIDER(MatripConstant.BAD_REQUEST, "LOGIN-001", "유효하지 않은 로그인 수단입니다."),
INVALID_OAUTH_TOKEN(MatripConstant.BAD_REQUEST, "LOGIN-002", "유효하지 않은 OAuth 토큰입니다."),

// ---- 네트워크 ---- //
HTTP_CLIENT_REQUEST_FAILED(MatripConstant.INTERNAL_SERVER_ERROR, "NETWORK-001", "서버 요청에 실패하였습니다.");

private final Integer status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.dnd.accompany.global.config.resttemplate;

import java.time.Duration;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Primary
@Bean(name = "defaultClient")
public RestTemplate defaultRestTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofSeconds(3))
.setReadTimeout(Duration.ofSeconds(5))
.build();
}
}
21 changes: 0 additions & 21 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,3 @@
server:
port: 8080
shutdown: graceful

spring:
jpa:
database-platform: org.hibernate.dialect.MySQLDialect
open-in-view: false
show-sql: true
properties:
hibernate:
format_sql: true
hbm2ddl.auto: update

datasource:
db:
pool-name: accompany
Expand Down Expand Up @@ -43,13 +29,6 @@ datasource:
usePipelineAuth: false
useBatchMultiSend: false

management:
endpoints:
web:
exposure:
include: "*"
exclude: env, beans

jwt:
issuer: MATE_TRIP
secret-key: thisisthejwtsecretkeyforlocalenvironment
Expand Down
23 changes: 23 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
server:
port: 8080
shutdown: graceful

spring:
jpa:
database-platform: org.hibernate.dialect.MySQLDialect
open-in-view: false
show-sql: true
properties:
hibernate:
format_sql: true
hbm2ddl.auto: update
oauth2:
kakao:
host: https://kapi.kakao.com

management:
endpoints:
web:
exposure:
include: "*"
exclude: env, beans

0 comments on commit 3ee54ef

Please sign in to comment.