나는 프로젝트에서 소셜 로그인을 구현하는 첫 시도를 하게 되었다.
안타깝게도 소설 로그인에 대해 구글링을 하자 나의 눈높이에 맞는 설명의 글이 없어 보였다.
그래서 나는 기본기가 약하고, 요소 하나하나 설명이 필요한 자의 수준으로 소셜 로그인 API 구현 과정을 공부 겸 정리해 보려고 한다.
또한 소셜 로그인 기능을 짜는데 있어 편리한 passport 모듈이 있으나 OAuth의 이해를 더 하고자 passport 모듈을 사용하지 않으려 한다.
기능을 구현하는데 정말 많은 블로그를 참고했지만
juno97님의 블로그가 대부분의 베이스가 되었다.
먼저 다음 사진은 공식 카카오 개발자 홈페이지에 나와있는 로그인 과정이다.
소셜 로그인을 구현하고자 하면 프론트엔드 또는 백엔드가 혼자 모두 구현이 가능하다고는 하다.
하지만 그렇게 되면 cors error 가 발생할 수도 있다고 하니 '인가코드 받기' 또는 '토큰 받기'를 기준으로 역할을 나누는것이 좋다고 한다.
우리 프로젝트에서 소셜 로그인을 담당한 프론트 팀원(Client)과 나(Server)는 다음과 같이 역할을 나누기로 했고 이게 가장 이상적인 분담같다.
나는 백엔드 분야로 공부하는 초보 중의 왕초보이기 때문에 프론트에서 뭘 하는지 자세히 설명할 여력은 없을 것 같다ㅠㅠ
1. 로그인창 요청
먼저 프론트에서 만든 UI와 그곳에 입력한 카카오 아이디와 비밀번호로 카카오 서버에 로그인 요청(res) 한다.
2. Access Token 발급
로그인에 성공하게 되면 카카오에서 프론트쪽으로 Access Token을 응답(req)해준다.
3. Access Token 전달
프론트는 받은 Access Token을 백엔드에게 전달한다.
4. Access Token 검증
백엔드는 받은 Access Token이 제대로 된 값인지 확인하고 사용자의 정보를 받아오기 위해 Access Token을 보내 정보를 요청(res)한다.
5-1. 사용자 정보 전달
카카오는 클라이언트로 부여한 Access Token과 서버로 받은 Access Token이 일치하면 토큰의 해당하는 유저의 정보를 응답(res)하고 백엔드는 받은 유저 정보를 데이터베이스에 저장한다.
전달받는 유저의 정보는 동의 항목에서 사용하기로 설정한 값만 받는다.(추후 설명)
5-2. JWT 발행
백엔드는 유저의 정보를 받으면 JSON WEB TOKEN을 발행한다.
JWT(JSON WEB TOKEN / 유저를 인증하고 식별하기 위한 토큰, 서버가 아닌 클라이언트에 저장함)
‼️ 여기서 카카오의 Access Token을 사용하지 않는 이유는 카카오의 토큰은 엄연히 카카오의 비밀정보(유저의 개인정보)이기 때문에 함부로 사용해서는 안된다.
따라서 JWT로 새롭게 토큰을 발행한다.
6. JWT 전달
백엔드는 발행한 JWT토큰을 프론트로 전달한다.
Kakao Developers으로 가서 카카오 로그인의 제품소개로 이동한다. (로그인 필요)
하단의 API 사용하기의 시작하기로 이동
애플리케이션 추가하기를 누른다
앱 아이콘: 앱 아이콘 이미지 첨부(유저 정보중 카카오 이메일이 필요하다면 반드시 아이콘을 등록해 Biz 등록을 해야만 이메일 정보를 받아올 수 있다.)
앱 이름: 애플리케이션 이름
사업자명: 프로젝트용 애플리케이션이라면 아무거나 적어도 무방
만든 애플리케이션으로 들어가면 다음과 같이 보이는데 다음 4가지 앱 키는 절대 노출이 되어서는 안되고 노출이 됐다면 왼쪽의 앱 키 탭으로가 재발급을 받아야 한다.
나는 node.js로 API를 만들것이기 때문에 REST API 키가 필요하고 나중에 복사해 사용하게 될 예정이다.
먼저 앱 설정의 플랫폼 탭으로 이동해 맨 아래 Web항목의 사이트 도메인을 등록해야 한다.
혼자 테스트를 한다면 다음과 같이 http://localhost에 자신의 포트를 :3000 과 같이 적으면 된다.
왼쪽의 제품 설정의 카카오 로그인 탭으로 이동해 활성화 설정을 ON으로 바꾸고 맨 하단의 Redirect URI에 위에서 적은 자신의 도메인과 엔드포인트를 추가 하면 된다.
나의 엔드포인트는 참조한 다른 블로그대로 따라 썼고 본인 설정대로 지정하면 된다.
REST API Key 란... 정말 간단히 설명하자면 보안에 관련된 거라고 생각하면 된다.
더 궁금하다면 조대협님 블로그를 참고하면 될 것 같다.
그리고 Redirect URI는 카카오 서버에서 어느 주소로 REST API GET 요청을 보내는지에 대한 주소이다.
Redirect URI에 더 자세히 알고 싶다면 Showerbugs 님의 깃헙를 참고해 OAuth를 먼저 알아야 할 듯 하다.
다음으로 제품 설정의 동의항목 탭으로 가 자신이 받고자 하는 유저 정보를 설정하면 된다.
나의 경우 오직 소셜 로그인으로만 로그인 할 예정이라 유저의 프로필 사진과 카카오계정(이메일)이 필요해 두 가지만 필수 동의 설정을 해 두었다.
여기서 카카오 계정(이메일)은 나의 애플리케이션이 비즈 앱으로 전환해 주어야만 정보를 필수값으로 받을 수 있게된다.
나의 애플리케이션을 비즈 앱으로 전환하려면 앱 설정의 비즈니스 탭으로 이동한다.
나는 프로젝트로 진행하기 때문에 개인 개발자 비즈 앱 전환을 선택한다.
마침 내 목적과 맞게 이메일 필수 동의 선택이 있다.
목적에 맞게 선택후 전환을 누르자
비즈 앱으로 전환 되었음을 확인할 수 있다.
본래 백엔드의 Server는 먼저 요청을 보낼 수 없으나 axios 모듈을 이용해 카카오 서버 측에 토큰으로 요청을 보낼 수 있게 된다.
작업 폴더로 가서 axios를 설치 해준다.
$ npm install axios
const express = require("express");
const router = express.Router();
const userRouter = require("./userRouter")
router.use("/auth", userRouter)
module.exports = router
소셜 로그인 엔드포인트는 /auth로 지정해 주었다.
const express = require("express");
const router = express.Router();
const { userController } = require("../controllers");
router.post('/kakao/signin', userController.signInKakao)
module.exports = router
http GET 메소드를 사용해야 하는지 POST 메소드를 사용해야 하는지 의문이 있었지만 지금 하는 작업에서는 문제 될건 딱히 없다고 한다.
이 API의 궁극적인 목표는 JWT 토큰을 받아오는 것이기 때문에 느낌상 GET이 더 가깝다고도 한다.
엔드포인트를 /auth/kakao/signin 으로 지정했다.
controller에서는 카카오 token을 가져와 service 단으로 넘겨주는 로직으로 구성돼있다.
const { userService } = require("../services")
const { asyncWrap } = require("../middleware/errorControl");
const signInKakao = asyncWrap(async (req, res) => {
const headers = req.headers["authorization"];
const kakaoToken = headers.split(" ")[1];
const accessToken = await userService.signInKakao(kakaoToken);
return res.status(200).json({ accessToken: accessToken });
});
module.exports = {
signInKakao
}
다음은 카카오 사용자 정보 가져오기 공식문서이다.
여기서 우리가 필요한건 Authorization: Bearer ${ACCESS_TOKEN} 에서 ${ACCESS_TOKEN} 이 필요하다.
GET/POST /v2/user/me HTTP/1.1
Host: kapi.kakao.com
Authorization: Bearer ${ACCESS_TOKEN}/KakaoAK ${APP_ADMIN_KEY}
Content-type: application/x-www-form-urlencoded;charset=utf-8
그래서 "authorization" 를 헤더에 담아 띄어쓰기(" ") 로 나눈뒤 1번 인덱스에 있는 ${ACCESS_TOKEN} 값만 kakaoToken 변수에 넣어주었다.
const headers = req.headers["authorization"];
const kakaoToken = headers.split(" ")[1];
그렇게 카카오 토큰을 Service단의 signInKakao함수로 보냈다.
service에서는 카카오 토큰으로 카카오 서버로 요청을 보내고 유저 정보를 담아와 필요한 데이터 값을 골라 이미 데이터베이스에 들어가 있는 카카오 id 라면 즉시 jwt 토큰을 발급, 데이터베이스에 없는 카카오 id 라면 고른 데이터 값들을 Dao 단으로 넘긴 후 jwt 토큰을 발급하는 로직으로 구성돼있다.
const { userDao } = require("../models");
const axios = require("axios");
const jwt = require("jsonwebtoken");
const signInKakao = async (kakaoToken) => {
const result = await axios.get("https://kapi.kakao.com/v2/user/me", {
headers: {
Authorization: `Bearer ${kakaoToken}`,
},
});
const {data} = result
const name = data.properties.nickname;
const email = data.kakao_account.email;
const kakaoId = data.id;
const profileImage = data.properties.profile_image;
if (!name || !email || !kakaoId) throw new error("KEY_ERROR", 400);
const user = await userDao.getUserById(kakaoId);
if (!user) {
await userDao.signUp(email, name, kakaoId, profileImage);
}
return jwt.sign({ kakao_id: user[0].kakao_id }, process.env.TOKKENSECRET);
};
module.exports = {
signInKakao
}
const result = await axios.get("https://kapi.kakao.com/v2/user/me", {
headers: {
Authorization: `Bearer ${kakaoToken}`,
},
});
유저의 정보를 가져왔으면 내가 필요한 유저 정보를 쇽쇽 골라 챙겨주자.
나의 경우
name:유저의 닉네임,
email: 카카오 계정 이메일,
kakaoId: 카카오 유저 id값,
profileImage: 카톡 프로필 사진
을 각각 변수에 저장해 주었다.
자신이 필요한 키 값들을 알고 싶다면 유저 정보를 가져온 result를 console.log 찍어서 보도록 하자.
가장 먼저 만약 필수값인 name, email, kakaoId 의 값이 없을 경우 KEY ERROR 에러를 던지도록 했고
그 다음 dao 단의 유저의 kakaoId를 조회하는 getUserById 함수로 데이터베이스에 있는 kakaoId 인지 조회한다.
(우리 프로젝트에선 오직 카카오 소셜 로그인으로만 로그인 가능하도록 해서 kakaoId로 가입여부를 조회한다.
여러 소셜 로그인을 구현한다면 users 테이블의 provider 컬럼에 kakao나 naver, goolge 같은 값을 넣고 이메일 같은걸로 조회를 해야한다.)
만약 데이터베이스에 없는 kakaoId 라면 위에서 골라낸 유저 정보를 Dao 단의 signUp 함수로 유저 정보를 저장하게 한다.
마지막으로 리턴값으로 jwt 토큰을 지급해준다.
const {data} = result
const name = data.properties.nickname;
const email = data.kakao_account.email;
const kakaoId = data.id;
const profileImage = data.properties.profile_image;
if (!name || !email || !kakaoId) throw new error("KEY_ERROR", 400);
const user = await userDao.getUserById(kakaoId);
if (!user) {
await userDao.signUp(email, name, kakaoId, profileImage);
}
return jwt.sign({ kakao_id: user[0].kakao_id }, process.env.TOKKENSECRET);
Dao에는 유저의 가입 여부를 조회하는 로직과 가입되지 않은 유저의 정보를 데이터베이스에 저장하는 로직으로 구성돼있다.
const appDataSource = require('./dataSource')
const getUserById = async(kakaoId) => {
return await appDataSource.query(`
SELECT
kakao_id,
account_email,
name,
profile_image
FROM users
WHERE kakao_id=?`
, [kakaoId]
);
}
const signUp = async (email, name, kakaoId, profileImage) => {
return await appDataSource.query(`
INSERT INTO users(
account_email,
name,
kakao_id,
profile_image
) VALUES (?, ?, ?, ?)`
, [email, name, kakaoId, profileImage]
)
}
module.exports = {
getUserById,
signUp
}
드디어 마지막 단계다!
처음에는 나는 토큰을 프론트로부터 받아온 이후 작동하는 API를 구현했기 때문에 혼자서 테스트가 불가능할 것이라고 생각했다.
하지만 방법은 있었으니
카카오 개발자 페이지의 상단 메뉴바를 보면 도구가 있다.
도구에서 REST API 테스트로 들어간다.
그럼 토큰을 발급 받을 수 있는 페이지가 나오는데
인증 앱을 우리가 만든 애플리케이션으로 바꿔줘야 한다.
내 애플리케이션으로 바꾸고 토큰 발급을 누르면 토큰이 생성된다.
우리 프론트에서 하는 역할을 이 페이지가 대신 해준다!
그리고 이 토큰은 절대 유출되면 안된다!!
발급된 토큰을 복사해서 아래 코드 부분의 headers 와 kakaoToken 을 주석처리하고
새로 kakaoToken 변수에 복사한 토큰을 붙여 넣으면 된다.
const signInKakao = asyncWrap(async (req, res) => {
// const headers = req.headers["authorization"];
// const kakaoToken = headers.split(" ")[1];
const kakaoToken = 이곳에 발급된 토큰을 붙여넣기 하세요
const accessToken = await userService.signInKakao(kakaoToken);
return res.status(200).json({ accessToken: accessToken });
});
이것은 기능을 테스트 하기 위해 한 임시 방편이기 때문에 실제로 테스트를 할땐 다시 원상태로 돌려줘야 한다. 토큰 지우는거 깜빡하면 안된다!
이제 서버를 열고 터미널에 우리가 지정한 엔드포인트를 입력해주면 정상적으로 유저 정보를 가져와 유저 정보를 저장하고 jwt 토큰을 발급 받는것을 확인할 수 있다.
http -v POST http://127.0.0.1:3000/auth/kakao/signin
처음에는 정말 이해가 안되고 어떻게 해야할지 감이 안잡혔지만 막상 코드를 모두 작성한 뒤 되돌아보면 참 간단한 API 인 것 같다.
앞으로 새로이 만들게 될 API 들도 이런식이겠지.