์นด์นด์ค ๊ณต์ ๋ฌธ์๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์ฑ
https://developers.kakao.com/docs/latest/ko/kakaologin/common
๊ตฌํ ์ฝ๋
https://github.com/DDonghyeo/kakao-login
์ฐธ๊ณ
์ด ๊ธ์ ์ ์ฐจ์ ๋ฐ๋ผ ์ฝ๋๋ฅผ ์์ฑํด ๋๊ฐ๋ ๊ณผ์ ์ ๋๋ค. ์ ์ฒด ์ฝ๋๋ ๊นํ๋ธ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์.
ํ๋ก ํธ(ํด๋ผ์ด์ธํธ) ๋ถ๋ถ์ thymeleaf๋ก ๊ตฌํํ์์ต๋๋ค.
Spring Boot 3.x.x ๋ฒ์ ๊ธฐ์ค์ ๋๋ค.
๋จผ์ , ์นด์นด์ค ๋ก๊ทธ์ธ ๋ฐฉ์์ ๋ํด ์ดํดํ๊ณ ์์ํด์ผ ํ๋ค.
์๋์ฒ๋ผ ์ ๋ฆฌํ๊ณ ์งํํ๊ฒ ๋ค.
- ์นด์นด์ค ๋ก๊ทธ์ธ ์์ฒญ
- ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ์ ์์ฒญํ๋ค. ๋ฏธ๋ฆฌ ์ค์ ํด๋ client_id (REST API KEY) + redirect URI ๊ฐ ์ค์ ๋ ๋งํฌ๋ก ๋ค์ด๊ฐ๋ค.
- ์ฌ์ฉ์๊ฐ ์นด์นด์ค ๋ก๊ทธ์ธ
- ์นด์นด์ค ๋ก๊ทธ์ธ ์ฐฝ์ผ๋ก redirect ๋๋ฉฐ ์ฌ์ฉ์๊ฐ ์นด์นด์ค ์์ด๋๋ก ๋ก๊ทธ์ธํ๋ค.
- ์ฑ์ ๋ฑ๋ก๋ Redirect URI์ ๊ฐ์ด code ์ ๋ฌ ๋ฐ๊ธฐ
- ๋ก๊ทธ์ธ์ด ์๋ฃ๋๋ฉด ๋ฏธ๋ฆฌ ์ค์ ํด๋ redirect uri๋ก ๋์์ค๋ฉฐ, ํ๋ผ๋ฏธํฐ code๋ก ์ธ๊ฐ์ฝ๋๋ฅผ ๋ฐ๋๋ค.
- ์ธ๊ฐ์ฝ๋๋ ์นด์นด์ค๊ฐ ์ ๋ฌํด์ฃผ๋ ์ฝ๋์ด๋ค.
- code๋ฅผ ํตํด ํ ํฐ ๋ฐ๊ธ ์์ฒญ
- ๋ฐ์ ์ธ๊ฐ์ฝ๋๋ฅผ ์นด์นด์ค์ ๋ณด๋ด๋ฉด access token, refresh token์ ๋ฐ๋๋ค.
- ๋ฐ์ ํ ํฐ์ผ๋ก ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
- ๋ฐ์ access token์ ์นด์นด์ค์๊ฒ ๋ณด๋ด์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์์ฒญํ๋ค.
- ํ์๊ฐ์
, ๋๋ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
- ๋ฐ์ ์ฌ์ฉ์ ์ ๋ณด๋ก ์๋ฒ ๋ด์์ ํ์๊ฐ์ , ๋ก๊ทธ์ธ ์ฒ๋ฆฌ๋ฅผ ํ๋ค.
0. Settings
0-1. ์ ํ๋ฆฌ์ผ์ด์ ์์ฑ
๋จผ์ , ์นด์นด์ค์์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ถ๊ฐํด์ผ ํ๋ค.
์นด์นด์ค Developer์์ ๋ก๊ทธ์ธ์ ํ๊ณ ๋ด ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค์ด๊ฐ์.
์ ํ๋ฆฌ์ผ์ด์ ์ถ๊ฐํ๊ธฐ๋ฅผ ๋๋ฅธ๋ค.
์ํ๋ ์ด๋ฆ์ ์์ฑํ๊ณ ์ ์ฅ์ ๋๋ฅธ๋ค.
์ ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋ง๋ค์ด์ก๋ค.
์ ํ๋ฆฌ์ผ์ด์
์ ๋๋ฌ์ ๋ค์ด๊ฐ์ ๋, ์ฑ ํค ์นธ์์ ํค๋ฅผ ๋ณผ ์ ์๋ค.
์ฐ๋ฆฌ๋ REST API๋ฅผ ์ด์ฉํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ REST API ํค๋ฅผ ๋ณต์ฌํด๋์.
0-2. ๋์ ํญ๋ชฉ ์ค์
๋์ํญ๋ชฉ์ ๋ค์ด๊ฐ ์นด์นด์ค ๋ก๊ทธ์ธ ๊ณผ์ ์์ ์ฌ์ฉ์๋ก๋ถํฐ ๋ฐ์ ์ ๋ณด๋ฅผ ์ค์ ํ ์ ์๋ค.
์ผ๋ถ ํญ๋ชฉ์ ํ์ ๋์๋ก ์ค์ ํ ์ ์์ง๋ง, ์ผ๋ถ๋ ์นด์นด์ค ๋ด์์ ๊ฒ์๊ฐ ํ์ํ๋ค.
๋ค๋ฅธ ํญ๋ชฉ ๋ํ ์ค์ ํ๊ณ ์ถ๋ค๋ฉด, ๋น์ฆ ์ฑ์ผ๋ก ์ ํํ๋ฉด ์ค์ ํ ์ ์๋ค.
์ผ๋จ์ ๋๋ค์๊ณผ ํ๋กํ ์ฌ์ง์ ํ์ ๋์๋ก ์ค์ ํ๊ณ ๋์ด๊ฐ๊ฒ ๋ค.
โผ๏ธ Email์ ๋ฐ์ ์ ์์ผ๋ฉด, ์๋ฒ ๋ด์์ ์ฌ์ฉ์ ์๋ณ์ ์ด๋ป๊ฒ ํ๋์?
๋๋ค์์ uniqueํ ๊ฐ์ด ์๋๊ธฐ ๋๋ฌธ์ ํ์ ์๋ณ์ฉ์ผ๋ก ์ฌ์ฉํ ์ ์์ง๋ง,
์นด์นด์ค ๋ด์ auth_id๋ก ์ฌ์ฉ์๋ฅผ ์๋ณํ ์ ์๋ค.
0-3. RedirectURI ๋ฑ๋ก
์นด์นด์ค๋ก๊ทธ์ธ ํญ์ ๋ค์ด๊ฐ์ ํ์ฑํ๋ฅผ ์ค์ ํด์ฃผ๋ฉด, RedirectURI๋ฅผ ๋ฑ๋กํ ์ ์๋ค.
์ฌ์ฉ์๊ฐ ์นด์นด์ค ๋ก๊ทธ์ธ์ ์ฑ๊ณตํ๋ฉด, ์นด์นด์ค ์ธก์์ Client๋ฅผ Redirect ํด์ค URI๋ฅผ ์ค์ ํด์ฃผ๋ ๊ฒ์ด๋ค.
ํ ๋ง๋๋ก, ์นด์นด์ค ์ธก์์ "์ฐ๋ฆฌ ๋ก๊ทธ์ธ ์๋ฃ๋๋ฉด ์ด๋๋ก ๋ณด๋ด์ค๊น?" ๋ฅผ ์ ํด๋๋ ๊ฒ์ด๋ค.
*์ฌ๊ธฐ์ ๋ฑ๋กํ RedirectURI๋ง ์ฌ์ฉํ ์ ์๋ค. ์ฌ์ฉํ ์๋ฒ์ ๋๋ฉ์ธ URI์ค ํ๋๋ฅผ ๋ฑ๋กํ์.
*์ฌ๊ธฐ์ ์ฌ์ฉํ RedirectURI๋ ์ ๊ณผ์ ์ค 3๋ฒ -> 4๋ฒ ๊ณผ์ ์์ ์ฌ์ฉํ๋ URI์ ํด๋นํ๋ค.
์์ ํ
์คํธ ์ฉ์ผ๋ก ์์ ๊ฐ์ด ๋ฑ๋กํ๋ค.
0-4. ์์กด์ฑ ์ถ๊ฐ
- build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
//Webflux
implementation 'org.springframework.boot:spring-boot-starter-webflux'
//thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}
HTTP ์์ฒญ์ Webflux Webclient๋ก ๊ตฌํํ๋ค.
๋ง์ฝ ์นด์นด์ค ๋ก๊ทธ์ธ ํ์ด์ง๋ฅผ ์ง์ ๊ตฌํํ์ง ์์ ์์ ์ด๋ผ๋ฉด, thymeleaf๋ ์ถ๊ฐํ์ง ์์๋ ๋๋ค.
1. ์นด์นด์ค ๋ก๊ทธ์ธ ์์ฒญ
์ฌ์ฉ์๊ฐ ์นด์นด์ค ๋ก๊ทธ์ธ์ ํ๋ ๊ณผ์ ์ด๋ค.
์ด ๋ถ๋ถ์ ํ๋ก ํธ์ ๊ฐ์ ์ด ๋ฐ๋์ ํ์ํ๋ค.
์ฌ์ฉ์๊ฐ ๋ธ๋ผ์ฐ์ ๋ฅผ ํตํด ์นด์นด์ค ๋ก๊ทธ์ธ ๋งํฌ๋ฅผ ๋ค์ด๊ฐ ๋ก๊ทธ์ธ ๊ณผ์ ์ ๊ฑฐ์ณ์ผ ํ๊ธฐ ๋๋ฌธ์ด๋ค.
1-1. ๋ก๊ทธ์ธ ํ์ด์ง ๋ง๋ค๊ธฐ
https://developers.kakao.com/docs/latest/ko/kakaologin/design-guide
์นด์นด์ค์์ ๋ฒํผ ๋์์ธ์ ๋ํ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํ๋ค.
png๋ฅผ ์ง์ ๋ค์ด๋ฐ๊ณ , thymeleaf๋ฅผ ์จ์ ๊ตฌํํด ๋ณด์๋ค.
- build.gradle ์์กด์ฑ ์ถ๊ฐ
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
- application.yml -> ์ด๊ธฐ์ ์ธํ ํ๋ REST API ํค, Redirect URI๋ฅผ ๋ฑ๋ก
kakao:
client_id: {REST API KEY}
redirect_uri: http://localhost:8080/callback
1-2. Page Controller
- Page Controller
๋ก๊ทธ์ธ ํ์ด์ง๋ฅผ ๋ฐํํด์ค ์ปจํธ๋กค๋ฌ
@Controller
@RequestMapping("/login")
public class KakaoLoginPageController {
@Value("${kakao.client_id}")
private String client_id;
@Value("${kakao.redirect_uri}")
private String redirect_uri;
@GetMapping("/page")
public String loginPage(Model model) {
String location = "https://kauth.kakao.com/oauth/authorize?response_type=code&client_id="+client_id+"&redirect_uri="+redirect_uri;
model.addAttribute("location", location);
return "login";
}
}
1-3. Login Page HTML
- resources/templates/login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>KakaoLogin</title>
</head>
<body>
<div class="container" style="display: flex; justify-content: center; align-content: center; align-items: center; flex-direction: column; margin: 200px auto; ">
<h1>์นด์นด์ค ๋ก๊ทธ์ธ</h1>
<a th:href="${location}">
<img src="/kakao_login_medium_narrow.png" >
</a>
</div>
</body>
</html>
/resources/static ๊ฒฝ๋ก์ pngํ์ผ์ ์ถ๊ฐ
์ด์ , http://localhost:8080/login/page ์ ์ ์ํ๋ฉด
2. ์ฌ์ฉ์๊ฐ ์นด์นด์ค ๋ก๊ทธ์ธ
์นด์นด์ค ๋ก๊ทธ์ธ์ ํ๋ผ๋ฏธํฐ๋ก REST APIํค์ redirect uri๊ฐ ์ฒจ๋ถ๋ ์นด์นด์ค ๋ก๊ทธ์ธ ๋งํฌ๋ก ๋ค์ด๊ฐ๋ฉฐ ์์๋๋ค.
https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}
๋ง์ฝ thymeleaf๋ก ๊ตฌํํ์ง ์์๋ค๋ฉด, ๊ทธ๋ฅ ๋งํฌ๋ฅผ ๋ธ๋ผ์ฐ์ ์์ ์ง์ ์ ์ํ ์๋ ์๋ค.
โผ๏ธ Tip
์ฌ๋ฌ๋ฒ ํ ์คํธํ๋ฉด ์บ์๋๋ฌธ์ ์๋ ๋ก๊ทธ์ธ ๋๋ ์บ์ ์ญ์ ํ ํ ์คํธํ๊ฑฐ๋ ๋ธ๋ผ์ฐ์ ์ํฌ๋ฆฟ ๋ชจ๋๋ก ํ์ฉ
ํด๋น ๋งํฌ๋ก ๋ค์ด๊ฐ๋ณด๋ฉด, ์ด๊ธฐ์ ์ค์ ํ๋ ๋์ํญ๋ชฉ๊ณผ ํจ๊ป ๋ก๊ทธ์ธ์ ์งํํ ์ ์๋ค.
๋ฑ๋กํด๋ Redirect URI์ ํ๋ผ๋ฏธํฐ๋ก code์ ํจ๊ป Redirect๋๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
3. ์ฑ์ ๋ฑ๋ก๋ Redirect URI์ ๊ฐ์ด code ์ ๋ฌ ๋ฐ๊ธฐ
๊ทธ๋ผ ์ด์ , Redirect๋ URI์ ์ ๋ฌ๋ code๋ฅผ ๊ฐ์ ธ์๋ณด์.
3-1. KakaoLoginController
- KakaoLoginController (์ถํ์ ๋ ์์ ๋จ)
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("")
public class KakaoLoginController {
@GetMapping("/callback")
public ResponseEntity<?> callback(@RequestParam("code") String code) {
return new ResponseEntity<>(HttpStatus.OK);
}
}
์์ ๊ฐ์ด, ๋ฑ๋กํด๋ URI์ GetMapping์ ํด๋๋ฉด ํ๋ผ๋ฏธํฐ code์ ํจ๊ป ๋ฐ์ ์ ์์ ๊ฒ์ด๋ค.
4. code๋ฅผ ํตํด ํ ํฐ ๋ฐ๊ธ ์์ฒญ
์ด์ ์นด์นด์ค๋ก๋ถํฐ ๋ฐ์ code๋ฅผ ์นด์นด์ค์ ํ ํฐ ๋ฐ๊ธ ์์ฒญ์ ํ๋ฉด, ์ฌ์ฉ์ ์ ๋ณด๊ฐ ๋ด๊ฒจ์๋ ํ ํฐ์ ๋ฐ์ ์ ์๋ค.
https://kauth.kakao.com/oauth/token URL๋ก POST ์์ฒญ์ ๋ณด๋ด๋ฉด, ํ ํฐ์ ๋ฐ์ ์ ์๋ค.
ํ ํฐ๊น์ง ๋ฐ์์ผ ๋ก๊ทธ์ธ์ด ์นด์นด์ค ๋ก๊ทธ์ธ์ด ์๋ฃ๋๋ค!
4-1. KakaoService
- KakaoTokenResponseDto.java
@Getter
@NoArgsConstructor //์ญ์ง๋ ฌํ๋ฅผ ์ํ ๊ธฐ๋ณธ ์์ฑ์
@JsonIgnoreProperties(ignoreUnknown = true)
public class KakaoTokenResponseDto {
@JsonProperty("token_type")
public String tokenType;
@JsonProperty("access_token")
public String accessToken;
@JsonProperty("id_token")
public String idToken;
@JsonProperty("expires_in")
public Integer expiresIn;
@JsonProperty("refresh_token")
public String refreshToken;
@JsonProperty("refresh_token_expires_in")
public Integer refreshTokenExpiresIn;
@JsonProperty("scope")
public String scope;
}
์นด์นด์ค๋ก๋ถํฐ ๋ฐ์ Response DTO๋ฅผ ๋ฏธ๋ฆฌ ๋ง๋ค์ด ๋๋ค.
์ญ์ง๋ ฌํ๋ฅผ ์ํด @JsonProperty๋ฅผ ์ด์ฉํ์ฌ ๋งคํํด์ฃผ๊ณ , default ์์ฑ์๋ฅผ ์ ์ธํด๋๋ค.
์์ธํ API๋ https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token ์์ ํ์ธ
- kakaoService.java (์ถํ์ ๋ ์ถ๊ฐ๋จ)
@Slf4j
@RequiredArgsConstructor
@Service
public class KakaoService {
private String clientId;
private final String KAUTH_TOKEN_URL_HOST;
private final String KAUTH_USER_URL_HOST;
@Autowired
public KakaoService(@Value("${kakao.client_id}") String clientId) {
this.clientId = clientId;
KAUTH_TOKEN_URL_HOST ="https://kauth.kakao.com";
KAUTH_USER_URL_HOST = "https://kapi.kakao.com";
}
public String getAccessTokenFromKakao(String code) {
KakaoTokenResponseDto kakaoTokenResponseDto = WebClient.create(KAUTH_TOKEN_URL_HOST).post()
.uri(uriBuilder -> uriBuilder
.scheme("https")
.path("/oauth/token")
.queryParam("grant_type", "authorization_code")
.queryParam("client_id", clientId)
.queryParam("code", code)
.build(true))
.header(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString())
.retrieve()
//TODO : Custom Exception
.onStatus(HttpStatusCode::is4xxClientError, clientResponse -> Mono.error(new RuntimeException("Invalid Parameter")))
.onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new RuntimeException("Internal Server Error")))
.bodyToMono(KakaoTokenResponseDto.class)
.block();
log.info(" [Kakao Service] Access Token ------> {}", kakaoTokenResponseDto.getAccessToken());
log.info(" [Kakao Service] Refresh Token ------> {}", kakaoTokenResponseDto.getRefreshToken());
//์ ๊ณต ์กฐ๊ฑด: OpenID Connect๊ฐ ํ์ฑํ ๋ ์ฑ์ ํ ํฐ ๋ฐ๊ธ ์์ฒญ์ธ ๊ฒฝ์ฐ ๋๋ scope์ openid๋ฅผ ํฌํจํ ์ถ๊ฐ ํญ๋ชฉ ๋์ ๋ฐ๊ธฐ ์์ฒญ์ ๊ฑฐ์น ํ ํฐ ๋ฐ๊ธ ์์ฒญ์ธ ๊ฒฝ์ฐ
log.info(" [Kakao Service] Id Token ------> {}", kakaoTokenResponseDto.getIdToken());
log.info(" [Kakao Service] Scope ------> {}", kakaoTokenResponseDto.getScope());
return kakaoTokenResponseDto.getAccessToken();
}
}
Webclient๋ก HTTP ์์ฒญ์ ๊ตฌํํ๋ค.
๊ฐ ์๋น์ค๋ง๋ค Custom Exception์ด ์๋ค๋ฉด onStatus ๋ฉ์๋์ ์ง์ ํ๋ฉด ๋๊ฒ ๋ค.
HTTP ์์ฒญ์ ๋ฐ์์ค๋ฉด .retrieve() ๋ฉ์๋ ๋ถํฐ, Request Body ๋ด์ฉ์ด
๋ฏธ๋ฆฌ ์ง์ ํด๋ KakaoTokenResponseDto์ json์ด ์ง๋ ฌํ๋์ด ๋ค์ด๊ฐ๊ฒ ๋๋ค.
https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id=${client_id}&code=${code}
์ url์ POST ์์ฒญ์ ๋ณด๋ด๋ฉด, ๋ฐ์ Response์ Body์ ์๋ ๋ด์ฉ์ ๋ฐ์ ์ ์๋ค.
- ์์ฒญ ์์
curl -v -X POST "https://kauth.kakao.com/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=${REST_API_KEY}" \ --data-urlencode "redirect_uri=${REDIRECT_URI}" \ -d "code=${AUTHORIZE_CODE}"
- ์๋ต ์์
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
"token_type":"bearer",
"access_token":"${ACCESS_TOKEN}",
"expires_in":43199,
"refresh_token":"${REFRESH_TOKEN}",
"refresh_token_expires_in":5184000,
"scope":"account_email profile"
}
์ฌ๊ธฐ์ expire์ ๊ธฐํ,
scope๋ ์ฌ์ฉ์๋ก๋ถํฐ ๋ฐ์ ์ ๋ณด๋ค์ ํญ๋ชฉ์ด๋ค.
์ด๋ฅผ ํตํด ์นด์นด์ค๋ก๋ถํฐ ํ ํฐ์ ๋ฐ๊ธ๋ฐ์๋ค.
๊ตฌํํ Service๋ฅผ Controller์์ ์ฐ๋ํ์.
4-2. KakaoLoginController
- KakaoLoginController.java (์ถํ์ ๋ ์์ ๋จ)
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("")
public class KakaoLoginController {
private final KakaoService kakaoService;
@GetMapping("/callback")
public ResponseEntity<?> callback(@RequestParam("code") String code) throws IOException {
String accessToken = kakaoService.getAccessTokenFromKakao(code);
return new ResponseEntity<>(HttpStatus.OK);
}
}
์ด์ code๋ฅผ ์ด์ฉํด์ accessToken์ ๋ฐ์์ฌ ์ ์๊ฒ ๋๋ค.
โผ๏ธ ์ฌ๊ธฐ๊น์ง ํ ํฐ์ ๋ฐ์์ผ๋ฏ๋ก, ์นด์นด์ค ๋ก๊ทธ์ธ์ด ์๋ฃ๋ ์ํฉ์ด๋ค.
์ฌ๊ธฐ์ ๋ฉ์ถฐ์ ๋ฐ์ ํ ํฐ์ ํตํด ์ธ์ฆ, ์ธ๊ฐ๋ฅผ ๊ตฌํํด๋ ๋์ง๋ง,
์ถ๊ฐ๋ก ํ ํฐ์ ์ด์ฉํ์ฌ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐ์ ํ ์๋ฒ์ ํ์๊ฐ์ ์ ๊ตฌํํ๋ ค๊ณ ํ๋ค.
5. ๋ฐ์ ํ ํฐ์ผ๋ก ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
์ด์ , ์นด์นด์ค์์ ๋ฐ์ access token์ผ๋ก ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค์.
5-1. KakaoService (์ถ๊ฐ)
- KakaoUserInfoResponseDto.java
@Getter
@NoArgsConstructor //์ญ์ง๋ ฌํ๋ฅผ ์ํ ๊ธฐ๋ณธ ์์ฑ์
@JsonIgnoreProperties(ignoreUnknown = true)
public class KakaoUserInfoResponseDto {
//ํ์ ๋ฒํธ
@JsonProperty("id")
public Long id;
//์๋ ์ฐ๊ฒฐ ์ค์ ์ ๋นํ์ฑํํ ๊ฒฝ์ฐ๋ง ์กด์ฌ.
//true : ์ฐ๊ฒฐ ์ํ, false : ์ฐ๊ฒฐ ๋๊ธฐ ์ํ
@JsonProperty("has_signed_up")
public Boolean hasSignedUp;
//์๋น์ค์ ์ฐ๊ฒฐ ์๋ฃ๋ ์๊ฐ. UTC
@JsonProperty("connected_at")
public Date connectedAt;
//์นด์นด์ค์ฑํฌ ๊ฐํธ๊ฐ์
์ ํตํด ๋ก๊ทธ์ธํ ์๊ฐ. UTC
@JsonProperty("synched_at")
public Date synchedAt;
//์ฌ์ฉ์ ํ๋กํผํฐ
@JsonProperty("properties")
public HashMap<String, String> properties;
//์นด์นด์ค ๊ณ์ ์ ๋ณด
@JsonProperty("kakao_account")
public KakaoAccount kakaoAccount;
//uuid ๋ฑ ์ถ๊ฐ ์ ๋ณด
@JsonProperty("for_partner")
public Partner partner;
@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class KakaoAccount {
//ํ๋กํ ์ ๋ณด ์ ๊ณต ๋์ ์ฌ๋ถ
@JsonProperty("profile_needs_agreement")
public Boolean isProfileAgree;
//๋๋ค์ ์ ๊ณต ๋์ ์ฌ๋ถ
@JsonProperty("profile_nickname_needs_agreement")
public Boolean isNickNameAgree;
//ํ๋กํ ์ฌ์ง ์ ๊ณต ๋์ ์ฌ๋ถ
@JsonProperty("profile_image_needs_agreement")
public Boolean isProfileImageAgree;
//์ฌ์ฉ์ ํ๋กํ ์ ๋ณด
@JsonProperty("profile")
public Profile profile;
//์ด๋ฆ ์ ๊ณต ๋์ ์ฌ๋ถ
@JsonProperty("name_needs_agreement")
public Boolean isNameAgree;
//์นด์นด์ค๊ณ์ ์ด๋ฆ
@JsonProperty("name")
public String name;
//์ด๋ฉ์ผ ์ ๊ณต ๋์ ์ฌ๋ถ
@JsonProperty("email_needs_agreement")
public Boolean isEmailAgree;
//์ด๋ฉ์ผ์ด ์ ํจ ์ฌ๋ถ
// true : ์ ํจํ ์ด๋ฉ์ผ, false : ์ด๋ฉ์ผ์ด ๋ค๋ฅธ ์นด์นด์ค ๊ณ์ ์ ์ฌ์ฉ๋ผ ๋ง๋ฃ
@JsonProperty("is_email_valid")
public Boolean isEmailValid;
//์ด๋ฉ์ผ์ด ์ธ์ฆ ์ฌ๋ถ
//true : ์ธ์ฆ๋ ์ด๋ฉ์ผ, false : ์ธ์ฆ๋์ง ์์ ์ด๋ฉ์ผ
@JsonProperty("is_email_verified")
public Boolean isEmailVerified;
//์นด์นด์ค๊ณ์ ๋ํ ์ด๋ฉ์ผ
@JsonProperty("email")
public String email;
//์ฐ๋ น๋ ์ ๊ณต ๋์ ์ฌ๋ถ
@JsonProperty("age_range_needs_agreement")
public Boolean isAgeAgree;
//์ฐ๋ น๋
//์ฐธ๊ณ https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info
@JsonProperty("age_range")
public String ageRange;
//์ถ์ ์ฐ๋ ์ ๊ณต ๋์ ์ฌ๋ถ
@JsonProperty("birthyear_needs_agreement")
public Boolean isBirthYearAgree;
//์ถ์ ์ฐ๋ (YYYY ํ์)
@JsonProperty("birthyear")
public String birthYear;
//์์ผ ์ ๊ณต ๋์ ์ฌ๋ถ
@JsonProperty("birthday_needs_agreement")
public Boolean isBirthDayAgree;
//์์ผ (MMDD ํ์)
@JsonProperty("birthday")
public String birthDay;
//์์ผ ํ์
// SOLAR(์๋ ฅ) ํน์ LUNAR(์๋ ฅ)
@JsonProperty("birthday_type")
public String birthDayType;
//์ฑ๋ณ ์ ๊ณต ๋์ ์ฌ๋ถ
@JsonProperty("gender_needs_agreement")
public Boolean isGenderAgree;
//์ฑ๋ณ
@JsonProperty("gender")
public String gender;
//์ ํ๋ฒํธ ์ ๊ณต ๋์ ์ฌ๋ถ
@JsonProperty("phone_number_needs_agreement")
public Boolean isPhoneNumberAgree;
//์ ํ๋ฒํธ
//๊ตญ๋ด ๋ฒํธ์ธ ๊ฒฝ์ฐ +82 00-0000-0000 ํ์
@JsonProperty("phone_number")
public String phoneNumber;
//CI ๋์ ์ฌ๋ถ
@JsonProperty("ci_needs_agreement")
public Boolean isCIAgree;
//CI, ์ฐ๊ณ ์ ๋ณด
@JsonProperty("ci")
public String ci;
//CI ๋ฐ๊ธ ์๊ฐ, UTC
@JsonProperty("ci_authenticated_at")
public Date ciCreatedAt;
@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Profile {
//๋๋ค์
@JsonProperty("nickname")
public String nickName;
//ํ๋กํ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ด๋ฏธ์ง URL
@JsonProperty("thumbnail_image_url")
public String thumbnailImageUrl;
//ํ๋กํ ์ฌ์ง URL
@JsonProperty("profile_image_url")
public String profileImageUrl;
//ํ๋กํ ์ฌ์ง URL ๊ธฐ๋ณธ ํ๋กํ์ธ์ง ์ฌ๋ถ
//true : ๊ธฐ๋ณธ ํ๋กํ, false : ์ฌ์ฉ์ ๋ฑ๋ก
@JsonProperty("is_default_image")
public String isDefaultImage;
//๋๋ค์์ด ๊ธฐ๋ณธ ๋๋ค์์ธ์ง ์ฌ๋ถ
//true : ๊ธฐ๋ณธ ๋๋ค์, false : ์ฌ์ฉ์ ๋ฑ๋ก
@JsonProperty("is_default_nickname")
public Boolean isDefaultNickName;
}
}
@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Partner {
//๊ณ ์ ID
@JsonProperty("uuid")
public String uuid;
}
}
์นด์นด์ค์์ ๋ฐ์ ์ ์๋ ๋ด์ฉ์ ๋ชจ๋ ๊ธฐ์ฌํ๋ค.
๊ฐ๋จํ ์ค๋ช ์ ์ฃผ์์ผ๋ก ์ถ๊ฐํด ๋์๋ค.
์ฌ๊ธฐ์, ์ฌ์ฉ์๊ฐ ๋์ํ ํญ๋ชฉ๋ง ๋ด๊ฒจ์ง๊ณ , ๋์ํ์ง ์์ ํญ๋ชฉ์ null๋ก ๋ค์ด์ค๊ฒ ๋๋ค.
๋์ ํญ๋ชฉ์ ๊ณ ๋ คํ์ฌ ์์ ๋กญ๊ฒ ์ปค์คํ ํ๋ฉด ๋๊ฒ ๋ค.
- KakaoService์ ์ถ๊ฐ
public KakaoUserInfoResponseDto getUserInfo(String accessToken) {
KakaoUserInfoResponseDto userInfo = WebClient.create(KAUTH_USER_URL_HOST)
.get()
.uri(uriBuilder -> uriBuilder
.scheme("https")
.path("/v2/user/me")
.build(true))
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) // access token ์ธ๊ฐ
.header(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString())
.retrieve()
//TODO : Custom Exception
.onStatus(HttpStatusCode::is4xxClientError, clientResponse -> Mono.error(new RuntimeException("Invalid Parameter")))
.onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new RuntimeException("Internal Server Error")))
.bodyToMono(KakaoUserInfoResponseDto.class)
.block();
log.info("[ Kakao Service ] Auth ID ---> {} ", userInfo.getId());
log.info("[ Kakao Service ] NickName ---> {} ", userInfo.getKakaoAccount().getProfile().getNickName());
log.info("[ Kakao Service ] ProfileImageUrl ---> {} ", userInfo.getKakaoAccount().getProfile().getProfileImageUrl());
return userInfo;
}
๋ง์ฐฌ๊ฐ์ง๋ก Webclient๋ก ๊ตฌํํ๋ค.
์ ๋ก์ง์
https://kapi.kakao.com/v2/user/me
์ URL์ ํค๋ Authorization์ Bearer token์ ์ถ๊ฐํด์ GET์์ฒญ์ ๋ณด๋ด๋ฉด, Response์ ์๋์ ๊ฐ์ Body๋ฅผ ๋ฐ์ ์ ์๋ค.
์์ธํ๊ฑด https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info ์์ ํ์ธ
- Response Body (์์)
HTTP/1.1 200 OK
{
"id":123456789,
"connected_at": "2022-04-11T01:45:28Z",
"kakao_account": {
// ํ๋กํ ๋๋ ๋๋ค์ ๋์ํญ๋ชฉ ํ์
"profile_nickname_needs_agreement": false,
// ํ๋กํ ๋๋ ํ๋กํ ์ฌ์ง ๋์ํญ๋ชฉ ํ์
"profile_image_needs_agreement": false,
"profile": {
// ํ๋กํ ๋๋ ๋๋ค์ ๋์ํญ๋ชฉ ํ์
"nickname": "ํ๊ธธ๋",
// ํ๋กํ ๋๋ ํ๋กํ ์ฌ์ง ๋์ํญ๋ชฉ ํ์
"thumbnail_image_url": "http://yyy.kakao.com/.../img_110x110.jpg",
"profile_image_url": "http://yyy.kakao.com/dn/.../img_640x640.jpg",
"is_default_image":false,
"is_default_nickname": false
},
// ์ด๋ฆ ๋์ํญ๋ชฉ ํ์
"name_needs_agreement":false,
"name":"ํ๊ธธ๋",
// ์นด์นด์ค๊ณ์ (์ด๋ฉ์ผ) ๋์ํญ๋ชฉ ํ์
"email_needs_agreement":false,
"is_email_valid": true,
"is_email_verified": true,
"email": "sample@sample.com",
// ์ฐ๋ น๋ ๋์ํญ๋ชฉ ํ์
"age_range_needs_agreement":false,
"age_range":"20~29",
// ์ถ์ ์ฐ๋ ๋์ํญ๋ชฉ ํ์
"birthyear_needs_agreement": false,
"birthyear": "2002",
// ์์ผ ๋์ํญ๋ชฉ ํ์
"birthday_needs_agreement":false,
"birthday":"1130",
"birthday_type":"SOLAR",
// ์ฑ๋ณ ๋์ํญ๋ชฉ ํ์
"gender_needs_agreement":false,
"gender":"female",
// ์นด์นด์ค๊ณ์ (์ ํ๋ฒํธ) ๋์ํญ๋ชฉ ํ์
"phone_number_needs_agreement": false,
"phone_number": "+82 010-1234-5678",
// CI(์ฐ๊ณ์ ๋ณด) ๋์ํญ๋ชฉ ํ์
"ci_needs_agreement": false,
"ci": "${CI}",
"ci_authenticated_at": "2019-03-11T11:25:22Z",
},
"properties":{
"${CUSTOM_PROPERTY_KEY}": "${CUSTOM_PROPERTY_VALUE}",
...
},
"for_partner": {
"uuid": "${UUID}"
}
}
์ json์ ๋ด๊ฒจ์๋ ์ ๋ณด๋ ๋ง์ฐฌ๊ฐ์ง๋ก ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ฌ์ฉ์๋ก๋ถํฐ ์ด๋ ์ ๋ณด๋ฅผ ๋์ ๋ฐ์๋์ง ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง ์ ์๋ค.
๋์ํ์ง ์์๋ค๋ฉด ๋ง์ฐฌ๊ฐ์ง๋ก null์ด ๋ค์ด์ค๋ ๊ฒ์ฌํ๋ ๋ก์ง์ ๊ผญ ์ถ๊ฐํด์ผ ํ๋ค.
์๋ฅผ ๋ค์ด, ์ฑ์ ๋๋ค์๋ง ๋์ ํญ๋ชฉ์ ์ถ๊ฐํ ๊ฒฝ์ฐ Response๋ ์๋์ ๊ฐ๋ค.
HTTP/1.1 200 OK
{
"id":123456789,
"connected_at": "2022-04-11T01:45:28Z",
"kakao_account": {
"profile_nickname_needs_agreement": false,
"profile": {
"nickname": "ํ๊ธธ๋"
}
},
"properties":{
"${CUSTOM_PROPERTY_KEY}": "${CUSTOM_PROPERTY_VALUE}",
...
}
}
์ ๋ฐ์ดํฐ๋ฅผ ํ์ฑํด์ ํ์ํ ์ ๋ณด๋ค์ ์ด์ฉํ์ฌ ์๋ฒ์ ํ์๊ฐ์
์ ํ๋ฉด ๋๊ฒ ๋ค.
5-2. KakaoLoginController (์ต์ข )
- KakaoLoginController (์ต์ข )
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("")
public class KakaoLoginController {
private final KakaoService kakaoService;
@GetMapping("/callback")
public ResponseEntity<?> callback(@RequestParam("code") String code) {
String accessToken = kakaoService.getAccessTokenFromKakao(code);
KakaoUserInfoResponseDto userInfo = kakaoService.getUserInfo(accessToken);
// ์ฌ๊ธฐ์ ์๋ฒ ์ฌ์ฉ์ ๋ก๊ทธ์ธ(์ธ์ฆ) ๋๋ ํ์๊ฐ์
๋ก์ง ์ถ๊ฐ
return new ResponseEntity<>(HttpStatus.OK);
}
}
์ด์ ๋ก๊ทธ์ธ์ ํตํด ์ฌ์ฉ์๊ฐ ๋์ํ ์ ๋ณด๋ค์ ์ป์ ์ ์๋ค.
์นด์นด์ค๋ก๋ถํฐ ๋ฐ์ ์ ๋ณด๋ค์ ํตํด์ ์๋ฒ์ ํ์๊ฐ์ ๋ก์ง์ ์ถ๊ฐํ๋ฉด ๋๊ฒ ๋ค.
์นด์นด์ค์์ ๋ฐ์ ํ ํฐ์ผ๋ก ์ธ๊ฐ๋ฅผ ํด๋ ๋๊ณ , ์๋ฒ ๋ด์์ ์ฌ์ฉ์ค์ธ ์ธ๊ฐ ๋ฐฉ๋ฒ์ด ์๋ค๋ฉด ์๋ก ๋ฐ๊ธํด์ค๋ ๋๊ฒ ๋ค.
์นด์นด์ค ๊ณต์ ๋ฌธ์๋ฅผ ๋ณด๋ฉด,
- ์ถ๊ฐ ํญ๋ชฉ ๋์ ๋ฐ๊ธฐ
- ์นด์นด์คํก์์ ์๋ ๋ก๊ทธ์ธํ๊ธฐ
- ์ฝ๊ด ์ ํํด ๋์ ๋ฐ๊ธฐ
- OpenID Connect ID ํ ํฐ ๋ฐ๊ธํ๊ธฐ
- ๊ธฐ์กด ๋ก๊ทธ์ธ ์ฌ๋ถ์ ์๊ด์์ด ๋ก๊ทธ์ธํ๊ธฐ
- ์นด์นด์ค๊ณ์ ๊ฐ์ ํ ๋ก๊ทธ์ธํ๊ธฐ
- ๋ก๊ทธ์ธ ํํธ ์ฃผ๊ธฐ
- ์นด์นด์ค๊ณ์ ๊ฐํธ ๋ก๊ทธ์ธ
๋ฑ๋ฑ ๊ธฐ๋ฅ์ ์ถ๊ฐ๋ก ์ด์ฉํ ์ ์์ผ๋, ํ์ํ ์๋น์ค๋ ์ฐธ๊ณ ํด์ ์ด์ฉํ๋ฉด ๋๊ฒ ๋ค.
์นด์นด์ค REST API Docs
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#before-you-begin
'BackEnd > Spring Boot' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
JPA/Hibernate Exception๋ค๊ณผ ์์ธ ์ฒ๋ฆฌ (0) | 2024.08.04 |
---|---|
[Spring JPA] save, saveAll, saveAllAndFlush, ์์์ฑ ์ปจํ ์คํธ๋ฅผ ํตํ ํจ์จ๊ณผ ์ฑ๋ฅ ์ฐจ์ด (0) | 2024.06.18 |
[Spring] CORS ๊ฐ๋ ๊ณผ Spring Security 6 ์ค์ ํบ์๋ณด๊ธฐ (0) | 2024.05.19 |
[ Spring ] Spring Data Redis (0) | 2024.04.14 |
[Kafka] JUnit5 Kafka ๋จ์ ๋ฉ์ธ์ง ๋ฐํ, ์ฑ๋ฅ ํ ์คํธ (0) | 2024.04.03 |