Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

카카오 로그인 API 구현 #33

Merged
merged 5 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ dependencies {
// Spring Configuration Processor
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

// Spring Open Feign (Feign Client)
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.0'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
import org.springframework.web.bind.annotation.RestController;

import com.ajou.hertz.common.auth.dto.JwtTokenInfoDto;
import com.ajou.hertz.common.auth.dto.request.KakaoLoginRequest;
import com.ajou.hertz.common.auth.dto.request.LoginRequest;
import com.ajou.hertz.common.auth.dto.response.JwtTokenInfoResponse;
import com.ajou.hertz.common.auth.service.AuthService;
import com.ajou.hertz.common.kakao.service.KakaoService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand All @@ -26,19 +29,41 @@
public class AuthControllerV1 {

private final AuthService authService;
private final KakaoService kakaoService;

@Operation(
summary = "로그인",
description = "이메일과 비밀번호를 전달받아 로그인을 진행합니다."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "400", description = "[2003] 비밀번호가 일치하지 않는 경우"),
@ApiResponse(responseCode = "404", description = "[2202] 이메일에 해당하는 유저를 찾을 수 없는 경우")
@ApiResponse(responseCode = "400", description = "[2003] 비밀번호가 일치하지 않는 경우", content = @Content),
@ApiResponse(responseCode = "404", description = "[2202] 이메일에 해당하는 유저를 찾을 수 없는 경우", content = @Content)
})
@PostMapping(value = "/login", headers = API_MINOR_VERSION_HEADER_NAME + "=" + 1)
public JwtTokenInfoResponse loginV1_1(@RequestBody @Valid LoginRequest loginRequest) {
JwtTokenInfoDto jwtTokenInfoDto = authService.login(loginRequest);
return JwtTokenInfoResponse.from(jwtTokenInfoDto);
}

@Operation(
summary = "카카오 로그인",
description = "카카오 로그인을 진행합니다."
)
@ApiResponses({
@ApiResponse(responseCode = "200"),
@ApiResponse(
responseCode = "409", content = @Content,
description = """
<p>[2200] 이미 다른 사용자가 사용 중인 이메일로 신규 회원을 등록하려고 하는 경우
<p>[2203] 이미 다른 사용자가 사용 중인 전화번호로 신규 회원을 등록하려고 하는 경우
"""
),
@ApiResponse(responseCode = "Any", description = "[10000] 카카오 서버와의 통신 중 오류가 발생한 경우. Http status code는 kakao에서 응답받은 것과 동일하게 설정하여 응답한다.", content = @Content)
})
@PostMapping(value = "/kakao/login", headers = API_MINOR_VERSION_HEADER_NAME + "=" + 1)
public JwtTokenInfoResponse kakaoLoginV1_1(@RequestBody @Valid KakaoLoginRequest kakaoLoginRequest) {
JwtTokenInfoDto jwtTokenInfoDto = kakaoService.login(kakaoLoginRequest);
return JwtTokenInfoResponse.from(jwtTokenInfoDto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.ajou.hertz.common.auth.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class KakaoLoginRequest {

@Schema(description = "카카오 '인가 코드 받기'를 통해 얻은 인가 코드", example = "v9rZPkB2nyKTFX26E8ByNzUojqi6w9bQRyfBQCFDzRFhxcoUUSVpI_3HgPIKlwzSAAABjbztS6bkNSpXBP-m7E")
@NotBlank
private String authorizationCode;

@Schema(description = "인가 코드가 리다이렉트된 uri", example = "https://hertz.com/kakao-login-redirection")
@NotBlank
private String redirectUri;
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public class SecurityConfig {
private static final Map<String, HttpMethod> AUTH_WHITE_LIST = Map.of(
"/v*/users", POST,
"/v*/users/existence", GET,
"/v*/auth/login", POST
"/v*/auth/login", POST,
"/v*/auth/kakao/login", POST
);

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.ajou.hertz.common.config.feign;

import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;

@EnableFeignClients(basePackages = "com.ajou.hertz")
@Configuration
public class FeignConfig {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.ajou.hertz.common.config.feign;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;

import feign.Logger;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import feign.form.spring.SpringFormEncoder;

public class KakaoFeignConfig {

@Bean
public Encoder encoder(ObjectFactory<HttpMessageConverters> converters) {
return new SpringFormEncoder(new SpringEncoder(converters));
}

@Bean
public ErrorDecoder errorDecoder() {
return new KakaoFeignErrorDecoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.ajou.hertz.common.config.feign;

import java.io.IOException;
import java.io.InputStream;

import org.springframework.http.HttpStatus;

import com.ajou.hertz.common.exception.IOProcessingException;
import com.ajou.hertz.common.kakao.dto.response.KakaoErrorResponse;
import com.ajou.hertz.common.kakao.exception.KakaoClientException;
import com.fasterxml.jackson.databind.ObjectMapper;

import feign.Response;
import feign.codec.ErrorDecoder;

public class KakaoFeignErrorDecoder implements ErrorDecoder {

private final ObjectMapper mapper;

public KakaoFeignErrorDecoder() {
this.mapper = new ObjectMapper();
}

@Override
public Exception decode(String methodKey, Response response) {
try (InputStream responseBody = response.body().asInputStream()) {
return new KakaoClientException(
HttpStatus.valueOf(response.status()),
mapper.readValue(responseBody, KakaoErrorResponse.class)
);
} catch (IOException e) {
return new IOProcessingException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ajou.hertz.common.exception;

import java.io.IOException;

import com.ajou.hertz.common.exception.constant.CustomExceptionType;

/**
* <p>I/O 작업에서 문제가 발생했을 때 사용하는 일반적인 exception class.
* <p>Checked exception을 {@link IOException}을 감싸 처리하는 용도로도 사용한다.
*
* @see IOException
*/
public class IOProcessingException extends InternalServerException {

public IOProcessingException(Throwable cause) {
super(CustomExceptionType.IO_PROCESSING, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
public enum CustomExceptionType {

MULTIPART_FILE_NOT_READABLE(1000, "파일을 읽을 수 없습니다. 올바른 파일인지 다시 확인 후 요청해주세요."),
IO_PROCESSING(1001, "입출력 처리 중 오류가 발생했습니다."),

/**
* 로그인, 인증 관련 예외
Expand All @@ -30,9 +31,15 @@ public enum CustomExceptionType {
/**
* 유저 관련 예외
*/
USER_EMAIL_DUPLICATION(2200, "이미 다른 회원이 사용 중인 이메일입니다."),
USER_EMAIL_DUPLICATION(2200, "이미 사용 중인 이메일입니다."),
USER_NOT_FOUND_BY_ID(2201, "일치하는 회원을 찾을 수 없습니다."),
USER_NOT_FOUND_BY_EMAIL(2202, "일치하는 회원을 찾을 수 없습니다.");
USER_NOT_FOUND_BY_EMAIL(2202, "일치하는 회원을 찾을 수 없습니다."),
USER_PHONE_DUPLICATION(2203, "이미 사용 중인 전화번호입니다."),
USER_KAKAO_UID_DUPLICATION(2204, "이미 가입한 계정입니다."),
USER_NOT_FOUND_BY_KAKAO_UID(2205, "일치하는 회원을 찾을 수 없습니다."),

KAKAO_CLIENT(10000, "카카오 서버와의 통신 중 오류가 발생했습니다."),
;

private final Integer code;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.ajou.hertz.common.kakao.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import com.ajou.hertz.common.config.feign.KakaoFeignConfig;
import com.ajou.hertz.common.kakao.dto.request.IssueKakaoAccessTokenRequest;
import com.ajou.hertz.common.kakao.dto.response.KakaoTokenResponse;

@FeignClient(
name = "kakaoAuthClient",
url = "https://kauth.kakao.com",
configuration = KakaoFeignConfig.class
)
public interface KakaoAuthClient {

@PostMapping(
value = "/oauth/token",
headers = "Content-type=application/x-www-form-urlencoded;charset=utf-8"
)
KakaoTokenResponse issueAccessToken(@RequestBody IssueKakaoAccessTokenRequest issueTokenRequest);
}
26 changes: 26 additions & 0 deletions src/main/java/com/ajou/hertz/common/kakao/client/KakaoClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.ajou.hertz.common.kakao.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;

import com.ajou.hertz.common.config.feign.KakaoFeignConfig;
import com.ajou.hertz.common.kakao.dto.response.KakaoUserInfoResponse;

@FeignClient(
name = "kakaoClient",
url = "https://kapi.kakao.com",
configuration = KakaoFeignConfig.class
)
public interface KakaoClient {

@GetMapping(
value = "/v2/user/me",
headers = "Content-type=application/x-www-form-urlencoded;charset=utf-8"
)
KakaoUserInfoResponse getUserInfo(
@RequestHeader("Authorization") String authorizationHeader,
@RequestParam("secure_resource") Boolean secureResource
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ajou.hertz.common.kakao.dto.request;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class IssueKakaoAccessTokenRequest {

private String grant_type;
private String client_id;
private String redirect_uri;
private String code;
private String client_secret;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ajou.hertz.common.kakao.dto.response;

import com.fasterxml.jackson.annotation.JsonProperty;

public record KakaoErrorResponse(
String error,
@JsonProperty("error_description") String errorDescription,
@JsonProperty("error_code") String errorCode
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.ajou.hertz.common.kakao.dto.response;

import com.fasterxml.jackson.annotation.JsonProperty;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class KakaoTokenResponse {

@JsonProperty("token_type")
private String tokenType;

@JsonProperty("access_token")
private String accessToken;

@JsonProperty("expires_in")
private Integer expiresIn;

@JsonProperty("refresh_token")
private String refreshToken;

@JsonProperty("refresh_token_expires_in")
private Integer refreshTokenExpiresIn;
}
Loading
Loading