카카오 소셜 로그인 With Front (React)
프로젝트를 진행하며 가입 및 로그인 과정이 길어서 불편하다는 피드백을 받고 소셜 로그인을 도입하게 되었다.
카풀을 주제로 한 프로젝트 였기에 아무래도 사용자의 신원 확인을 위해 기본적으로 요구하는 정보가 조금 많았다..ㅠㅠ
프로젝트 대상이 아무래도 스키 & 스노우보드를 좋아하는 특정 사람들이 되다 보니 홍보의 경우 오픈 채팅방에서 주로 이루어지기 때문에 카카오 소셜 로그인과 네이버 소셜 로그인을 도입하기로 했다.
( 네이버는.. 검수 미통과로 구현 & 테스트는 다 되었으나 배포는 하지 못함..ㅠㅠ )
소셜 로그인 도입의 장점
- 기존의 사용자에게 직접 입력 받던 성별, 나이대 항목에 대하여 검증이 불가했지만 카카오 로그인에서는 검증된 값을 가져올 수 있다.
- 회원 가입 & 로그인에서 아이디, 닉네임, 비밀번호를 설정하는 과정이 생략되고 동의하기 한 번 클릭으로 가입이 가능해서 과정이 매우 축소되었고 간단해졌다. -> 실제로 더 편하다는 피드백을 많이 얻을 수 있었다.
소셜 로그인 구현 과정
이번 프로젝트의 경우 Front(React)와 Back(Spring)이 나눠져 있었기에 프론트로 접속한 유저의 카카오 정보를 백에서 어떻게 받아올지에 대해 많은 고민을 했고 공식 문서를 참고하며 구현을 진행하였다.
네이버 구현도 했지만.. 공식 문서는 카카오가 보기 쉽게 정말 잘 정리되어 있다.
https://developers.kakao.com/docs/latest/ko/kakaologin/common
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
동작 흐름
[ 1 ] 카카오 서버에서 인가 코드 받아오기 (Redirect URI를 프론트 주소로 할 것)
- 로컬이라면 Redirect URI : http://localhost:3000/user/kakao/callback 이런 식으로 적어주면 된다.
- 배포를 한다면 Redirect URI : 배포주소/user/kakao/callback 이렇게 인가 코드를 돌려받을 URI를 작성해준다.
React Code - Login.js
import {NAVER_AUTH_URL} from "../share/kakaoAuth"
function Login() {
const kakaoLogin = () => {
window.location.href = KAKAO_AUTH_URL;
}
return (
<div>
<button onClick={kakaoLogin}>카카오 로그인</button>
</div>
);
}
export default Login;
React Code - kakaoAuth.js
const CLIENT_ID = 'af4c2105b17debc9c5ba96f70c6ee0b9'
const REDIRECT_URI =
'http://localhost:3000/user/kakao/callback'
export const KAKAO_AUTH_URL =
`https://kauth.kakao.com/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code`
[ 2, 3, 7, 8 ] 카카오 서버에서 받은 인가 코드를 백엔드 서버에 전송하기 & 전용 토큰 받기
- 1번의 요청을 보낸 후, Redirect URI로 들어오는 code를 받는다.
- 백엔드와 약속된 API로 인가 코드를 전송한다.
- 백엔드에서 가입이 끝난 후, 우리 사이트의 전용 토큰을 받아온다.
React Code - App.js
import {
BrowserRouter as Router,
Switch,
Route,
} from "react-router-dom"
import Login from "./routes/Login"
import KakaoAuthHandle from "./components/KakaoAuthHandle"
function App() {
return <Router>
<Switch>
<Route path="/login">
<Login />
</Route>
<Route /* 인가 코드 받기.. */
exact
path="/user/kakao/callback"
component={KakaoAuthHandle}
/>
</Switch>
</Router>;
}
export default App;
React Code - KakaoAuthHandle.jsx
import axios from 'axios'
import { useEffect } from 'react'
import styled from 'styled-components'
import {KAKAO_ADD_PROPERTIES} from "../share/kakaoAuth"
const KakaoAuthHandle = (props) => {
useEffect(() => {
let code = new URL(window.location.href).searchParams.get('code')
const kakaoLogin = async () => {
await axios
.get(`http://localhost:8080/user/kakao/callback?code=${code}`)
.then((res) => {
localStorage.setItem('token', res.headers.authorization)
window.location.href = "/";
})
}
kakaoLogin()
}, [props.history])
return (
<>
<Container></Container>
</>
)
}
export default KakaoAuthHandle
const Container = styled.div`
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
`
[ 4, 5, 6 ] 인가 토큰으로 카카오 서버에서 사용자 확인 후, DB에 User 저장, 전용 토큰 발행
- 클라이언트로부터 인가 코드를 받는다.
- 받은 인가 코드로 카카오 서버에 엑세스 토큰을 요청한다.
(이 때 Redirect URI는 프론트와 같아야 한다. 프론트 쪽에 맞추면 된다!) - 발급받은 엑세스 토큰으로 카카오 서버에 유저 정보를 받아온다.
- 필요 시에 회원 가입을 진행한다.
- DB에 없을 경우 : User 새로 생성 후 DB에 저장
- DB에 이미 존재할 경우 : 다음 단계로 넘어가기 - 우리 사이트 전용 JWT 토큰을 발행한다.
Spring Code - SocailLoginController.java
// 카카오 회원가입
@GetMapping("/user/kakao/callback")
public Long kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException {
// code: 카카오 서버로부터 받은 인가 코드
SignupSocialDto signupKakaoDto = kakaoUserService.kakaoLogin(code);
response.addHeader(AUTH_HEADER, signupKakaoDto.getToken());
return signupKakaoDto.getUserId();
}
Spring Code - KakaoUserService.java
package com.ppjt10.skifriend.service;
@RequiredArgsConstructor
@Service
public class KakaoUserService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
@Value("${kakao.client.id}")
private String clientId;
@Transactional
public SignupSocialDto kakaoLogin(String code) throws JsonProcessingException {
// 1. "인가 코드"로 "액세스 토큰" 요청
String accessToken = getAccessToken(code, "http://localhost:3000/user/kakao/callback");
// 2. 필요시에 회원가입
User kakaoUser = registerKakaoUserIfNeeded(accessToken);
// 3. 로그인 JWT 토큰 발행
String token = jwtTokenCreate(kakaoUser);
return SignupSocialDto.builder()
.token(token)
.userId(kakaoUser.getId())
.build();
}
private String getAccessToken(String code, String redirect_uri) throws JsonProcessingException {
// HTTP Header 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// HTTP Body 생성
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", clientId);
body.add("redirect_uri", redirect_uri);
body.add("code", code);
// HTTP 요청 보내기
HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest = new HttpEntity<>(body, headers);
RestTemplate rt = new RestTemplate();
ResponseEntity<String> response = rt.exchange(
"https://kauth.kakao.com/oauth/token",
HttpMethod.POST,
kakaoTokenRequest,
String.class
);
// HTTP 응답 (JSON) -> 액세스 토큰 파싱
String responseBody = response.getBody();
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(responseBody);
return jsonNode.get("access_token").asText();
}
// 젤 처음 로그인 시, 회원 가입 안되어 있으면 회원 가입 시켜주기
private User registerKakaoUserIfNeeded(String accessToken) throws JsonProcessingException {
JsonNode jsonNode = getKakaoUserInfo(accessToken);
// DB 에 중복된 Kakao Id 가 있는지 확인
String kakaoId = String.valueOf(jsonNode.get("id").asLong());
User kakaoUser = userRepository.findByUsername(kakaoId).orElse(null);
// 회원가입
if (kakaoUser == null) {
String kakaoNick = jsonNode.get("properties").get("nickname").asText();
// password: random UUID
String password = UUID.randomUUID().toString();
String encodedPassword = passwordEncoder.encode(password);
kakaoUser = new User(kakaoId, kakaoNick, encodedPassword);
userRepository.save(kakaoUser);
}
return kakaoUser;
}
// 카카오에서 동의 항목 가져오기
private JsonNode getKakaoUserInfo(String accessToken) throws JsonProcessingException {
// HTTP Header 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + accessToken);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// HTTP 요청 보내기
HttpEntity<MultiValueMap<String, String>> kakaoUserInfoRequest = new HttpEntity<>(headers);
RestTemplate rt = new RestTemplate();
ResponseEntity<String> response = rt.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.POST,
kakaoUserInfoRequest,
String.class
);
String responseBody = response.getBody();
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readTree(responseBody);
}
// JWT 토큰 생성
private String jwtTokenCreate(User kakaoUser) {
String TOKEN_TYPE = "BEARER";
UserDetails userDetails = new UserDetailsImpl(kakaoUser);
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetailsImpl userDetails1 = ((UserDetailsImpl) authentication.getPrincipal());
final String token = JwtTokenUtils.generateJwtToken(userDetails1);
return TOKEN_TYPE + " " + token;
}
}
방법만 알고 공식 문서를 보는 것에 조금만 익숙해지면 그렇게 어렵지 않은 과정이었다.
프로젝트를 진행하며, 기존의 자체 구현 회원가입, 로그인의 경우 유효성 검사와 로직 등 신경써서 만든 부분인데 없애는 것이 조금 아쉬웠지만 그래도 소셜 로그인을 구현하고 보니 훨씬 간편하고 사용성이 좋아져서 만족한다 :)
주의사항
- Redirect URI와 접속할 도메인을 내 어플리케이션에서 설정해주어야 한다!
사이트 도메인 등록
- 카카오 서버로 요청을 보내는 모든 사이트 도메인을 적어주면 된다.. 테스트용과 배포용 둘 다 적어둬서 조금 많다ㅎ
Redirect URI 등록
- 마찬가지로 사용할 Redirect URI를 등록해준다. 배포용, 테스트용...
- Redirect URI의 경우 프론트와 백이 다른 것이 아니라 프론트 쪽 URI로 쓴다는 것을 기억하기!