Skip to content

Commit

Permalink
Merge pull request #33 from Ajou-Hertz/feature/#25-kakao-login
Browse files Browse the repository at this point in the history
카카오 로그인 API 구현
  • Loading branch information
tinon1004 authored Feb 19, 2024
2 parents c794a65 + 7e77626 commit d6c4ec4
Show file tree
Hide file tree
Showing 32 changed files with 1,070 additions and 30 deletions.
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

0 comments on commit d6c4ec4

Please sign in to comment.