[NodeJS] Kakao REST APIs 활용한 로그인, 로그아웃, 사용자 정보 가져오기, 연결해제

Onam Kwon·2023년 2월 8일
2

Node JS

목록 보기
17/25

Kakao REST API

  • 시작하기에 앞서 해당 포스팅에 나오는 코드의 서버에서 카카오로 API 요청을 할때는 require('request-promise') 모듈을 사용합니다.
  • 해당 게시글은 서비스에서 로그인 기능을 카카오 REST API를 사용해 구현하는 내용입니다.
  • 또한 모든 클라이언트의 요청은 클라이언트에서 바로 실행되는게 아닌 서버를 거쳐 서버의 미들웨어에서 카카오 서버로 수행됩니다.
  • 에제와 관련된 카카오 REST API 구현을 위해 필요한 코드는 함수로 정리를 해놨습니다.
// kakaoService.js

const path = require('path');
const rp = require('request-promise');

require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }); 

/**
 * Kakao REST API 토큰받기
 */
exports.getToken = async (AUTHORIZE_CODE) => {
    const options = {
        uri: 'https://kauth.kakao.com/oauth/token?response_type=code&client_id='+process.env.REST_API_KEY+'&redirect_uri='+process.env.REDIRECT_URI,
        method: 'POST',
        /**
         * form means that `content-type : x-www-form-urlencoded`.
         * Use `form` format instead of `body` format since Kakao REST API only accepts `form` format.
         */
        form: {
            "grant_type": "authorization_code",  // authorization_code is a particular type of string.
            "client_id": process.env.REST_API_KEY, // REST API key value in app setting.
            "redirect_uri": process.env.REDIRECT_URI,  // Redirect_uri value in app setting.
            "code": AUTHORIZE_CODE,  // AUTHORIZE_CODE that received from the user.
            "client_secret": process.env.CLIENT_SECRET  // In order to enhance security when the authorization server issues a token. 
        },
        json: true
    }
    return await rp(options);
}

/**
 * Kakao REST API 사용자 정보 가져오기
 */
exports.getUserInfo = async (access_token) => {
    const options = {
        uri: 'https://kapi.kakao.com/v2/user/me',
        method: 'GET',
        headers: {
            "Content-Type": 'application/x-www-form-urlencoded;charset=utf-8',
            "Authorization": "Bearer "+access_token
        },
        json: true
    };
    try {
        const body = await rp(options);
        const nickname = body.properties.nickname;
        const profile_image = body.properties.profile_image;
        return {nickname, profile_image};
    } catch(err) {
        throw Error(err);
    }
}


/**
 * Kakao REST API 로그아웃
 * This function expires an access_token and a refresh_token in the session DB.
 * Not deleting tokens in the session DB. You will also need to delete tokens in session DB.
 * Kakao account and your service are still connected even though you use this function.
 */
exports.logout = async (access_token) => {
    const options = {
        uri: 'https://kapi.kakao.com/v1/user/logout',
        method: 'POST',
        headers: {
            "Authorization": "Bearer "+access_token
        },
        json: true
    }
    try {
        return await rp(options);
    } catch(err) {
        throw Error(err);
    }
}


/**
 * Kakao REST API 연결끊기
 */
exports.unlink = async (access_token) => {
    const options = {
        uri: 'https://kapi.kakao.com/v1/user/unlink',
        method: 'POST',
        headers: {
            "Authorization": "Bearer "+access_token
        },
        json: true
    }
    try {
        return await rp(options);
    } catch(err) {
        throw Error(err);
    }
}


/**
 * Kakao REST API 카카오계정과 함께 로그아웃
 * 미완성
 */
exports.logoutWithKakaoAccount = async () => {
    const options = {
        uri: 'https://kauth.kakao.com/oauth/logout?client_id='+process.env.REST_API_KEY+'&logout_redirect_uri='+process.env.LOGOUT_REDIRECT,
        method: 'GET',
        // headers: {
            
        // },
        json: true
    }
    try {
        return await rp(options);
    } catch(err) {
        throw Error(err);
    }
}

/**
 * Kakao REST API 토큰 갱신하기.
 * Not tested yet.
 */
exports.renewExpiredToken = async (refresh_token) => {
    const options = {
        uri: 'https://kauth.kakao.com/oauth/token',
        method: 'POST',
        form: {
            // "Content-Type": 'application/x-www-form-urlencoded',
            "client_id": process.env.REST_API_KEY,
            "refresh_token": refresh_token,
            "client_secret": process.env.CLIENT_SECRET
        },
        json: true
    }
    try {
        return await rp(options);
    } catch(err) {
        throw Error(err);
    }
}

로그인


그림[1]

  • 그림[1]은 로그인 과정을 나타내는 사진이며 이 과정을 설명하겠습니다. (회원가입은 미구현).

카카오 로그인 요청

<a href="/auth/kakao"><img src="/public/images/kakao.png" />
  • 맨 처음 말했다시피, 클라이언트의 모든 요청은 카카오 서버로 바로 보내는 것이 아닌 서비스의 서버를 거쳐 카카오 서버로 요청을 보낸다.
  • 위처럼 카카오에서 제공하는 Login with Kakao버튼을 만들어 서비스 서버의 /auth/kakao 경로로 get요청을 했다.

인가코드 받기 요청

/**
 * /auth/kakao
 */
exports.authenticate = async (req, res, next) => {
    try {
        const kakaoAuthURL = 'https://kauth.kakao.com/oauth/authorize?response_type=code&client_id='+process.env.REST_API_KEY+'&redirect_uri='+process.env.REDIRECT_URI+'&prompt=login';
        return res.status(302).redirect(kakaoAuthURL);
    } catch(err) {
        return next(err);
    }
}
  • 클라이언트로부터 해당 요청을 받은 서버는 해당 경로의 미들웨어를 실행시키며 이는 위와같다.
  • 해당 미들웨어는 카카오에서 제공하는 경로와 공식 문서에서 요구하는 REST_API_KEY, REDIRECT_URI를 추가해 클라이언트에게 반환한다.

인증 및 동의 요청

  • 서버에서 반환받은 화면을 클라이언트는 위 사진처럼 볼 수 있으며 이는 카카오에서 제공하는 인증 및 동의 요청 화면이다.

로그인 및 동의

  • 아이디와 비밀번호를 입력하고 로그인을 누르면

  • 위처럼 동의 화면이 나타난다.
    • 동의 항목은 원하는 대로 설정할 수 있으며 이는 https://developers.kakao.com 에 들어가
      내 애플리케이션 > 제품 설정 > Kakao Login > 동의 항목 에서 설정할 수 있다.
  • 클라이언트가 동의까지 누른 후 다음 버튼을 누르면 이 정보들은 카카오 서버로 들어간다.

인가 코드 발급

  • 신원을 확인한 카카오 서버는 인가코드를 클라이언트에게 발급해주며 클라이언트는 다시 https://developers.kakao.com내 애플리케이션 > 제품 설정 > Kakao Login에서 설정한 RedirectURI를 통해 해당 미들웨어로 인가 코드와 함께 요청을 보낸다.
    • 이경우 http://localhost/auth/kakao/callback

인가 코드로 토큰 발급 요청

const kakao = require('../kakao/kakaoService');
/**
 *  /auth/kakao/callback
 */
exports.authorize = async (req, res, next) => {
    const AUTHORIZE_CODE = req.query['code'];
    try {
        const {
            access_token,
            id_token,
            refresh_token
        } = await kakao.getToken(AUTHORIZE_CODE);
        req.session.access_token = access_token;
        req.session.id_token = id_token;
        req.session.refresh_token = refresh_token;
        return res.status(200).redirect('/user');
    } catch(err) {
        next(err);
    }
}
  • 위 미들웨어가 카카오 데브의 앱에 등록된 RedirectURI인 /auth/kakao/callback 경로에 해당하는 함수이다.
  • 인증을 마친 클라이언트는 카카오 서버로부터 인가코드를 받고 서비스 서버에 인가코드를 보내며 위 함수처럼 받을 수 있다.
  • 인가코드를 전달받은 서비스 서버는 다시 카카오에게 코드를 포함한 요청을 보내 토큰을 발급받을 수 있다.
    • 위 코드에서 토큰 발급에 관한 함수인 kakao.getToken(AUTHORIZE_CODE)는 아래 토큰발급에서 설명하겠습니다.
  • 토큰을 받은 서버는 req객체의 세션에 토큰들을 저장해 stateful한 상태를 유지할 수 있으며, 마지막으로 본인의 경우/user 경로의 미들웨어를 실행시켜준다.

토큰 발급

const rp = require('request-promise');
/**
 * Kakao REST API 토큰받기
 */
exports.getToken = async (AUTHORIZE_CODE) => {
    const options = {
        uri: 'https://kauth.kakao.com/oauth/token?response_type=code&client_id='+process.env.REST_API_KEY+'&redirect_uri='+process.env.REDIRECT_URI,
        method: 'POST',
        /**
         * form means that `content-type : x-www-form-urlencoded`.
         * Use `form` format instead of `body` format since Kakao REST API only accepts `form` format.
         */
        form: {
            "grant_type": "authorization_code",  // authorization_code is a particular type of string.
            "client_id": process.env.REST_API_KEY, // REST API key value in app setting.
            "redirect_uri": process.env.REDIRECT_URI,  // Redirect_uri value in app setting.
            "code": AUTHORIZE_CODE,  // AUTHORIZE_CODE that received from the user.
            "client_secret": process.env.CLIENT_SECRET  // In order to enhance security when the authorization server issues a token. 
        },
        json: true
    }
    return await rp(options);
}
  • 위의 인가 코드로 토큰 발급을 요청할때 쓰였던 함수인 getToken() 함수이며 request-promise 모듈을 사용해 카카오 서버로 요청을 보냈다.

카카오 로그인 완료, 토큰 정보 조회 및 검증

// Main login page.
exports.showMain = async (req, res) => {
    // Kakao REST API login.
   if(req.session.access_token) {
        const {nickname, profile_image} = await kakao.getUserInfo(req.session.access_token);
        const user = {
            id: nickname,
            address: profile_image
        };
        return res.render(path.join(__dirname, '../../views/user/user'), {user:user});
    }
    return res.render(path.join(__dirname, '../../views/user/loginPage'));
}
  • 인가코드로 토큰 발급 요청 에서 발급받은 토큰을 세션에 저장한 뒤 /user 경로의 미들웨어에서 req.session객체에 접근해 토큰을 가져와 이곳에서 유저 정보를 카카오 서버에게 요청한다.
const rp = require('request-promise');
/**
 * Kakao REST API 사용자 정보 가져오기
 */
exports.getUserInfo = async (access_token) => {
    const options = {
        uri: 'https://kapi.kakao.com/v2/user/me',
        method: 'GET',
        headers: {
            "Content-Type": 'application/x-www-form-urlencoded;charset=utf-8',
            "Authorization": "Bearer "+access_token
        },
        json: true
    };
    try {
        const body = await rp(options);
        const nickname = body.properties.nickname;
        const profile_image = body.properties.profile_image;
        return {nickname, profile_image};
    } catch(err) {
        throw Error(err);
    }
}
  • getUserInfo(access_token) 함수는 이런식으로 만들었다.

  • 로그인을 마치면 위처럼 실제 카카오톡 어플에서 설정해 놓은 이름과 프로필 사진의 주소를 (로그인 할때 동의한) 클라이언트에 보낼 수 있다.
    • 프로필 주소는 프론트의 글씨체가 너무 커 잘린 상태로 나와있는거같다.

로그아웃

  • 사용자 액세스 토큰과 리프레시 토큰을 모두 만료시킨다.
  • 로그아웃을 해도 카카오 계정과 서비스간의 세션은 만료되지 않으며, 로그아웃을 호출한 앱의 토큰만 만료된다.
  • 카카오 계정과 서비스와의 연결을 아예 끊고싶으면 추가로 연결끊기도 해아한다.
<form id="signOut">
	<button type="button" id="logOut" onclick="signOut()">Log out</button><br><br>
</form>
function signOut() {
  $.ajax({
    type: "delete",
    url: '/user/logout',
    data: {},
    dataType:'text',
    success: function(res) {
      location.reload();
    }
  });
}
  • Log out 버튼을 통해 로그아웃 요청을 수행할 수 있다.
// Sign out.
exports.signOut = async (req, res, next) => {
  // Sign out for Kakao.
  /**
   * kakao logout function makes tokens in session DB to be expired.
   * But they still exist in the session DB as a junk even though the function has expired it.
   */
  const body = kakao.logout(req.session.access_token);
  /**
  * Deleting junk tokens in the session DB.
  */
  req.session.destroy();
  return res.status(200).end();
}
  • 위 코드가 로그아웃 버튼을 클릭했을 시 실행되는 미들웨어 이며 카카오 서버로 로그아웃 요청을 보낸 후 세션 DB의 토큰들을 삭제한다.
  • 카카오 서버로 로그아웃 요청을 수행하면 카카오는 해당 access_token과 refresh_token을 만료시킨다.
  • 그 후 서비스 서버의 세션DB에 남아있는 만료되어있는 토큰들을 req.session.destroy()를 통해 지워준다.
const rp = require('request-promise');
/**
 * Kakao REST API 로그아웃
 * This function expires an access_token and a refresh_token in the session DB.
 * Not deleting tokens in the session DB. You will also need to delete tokens in session DB.
 * Kakao account and your service are still connected even though you use this function.
 */
exports.logout = async (access_token) => {
    const options = {
        uri: 'https://kapi.kakao.com/v1/user/logout',
        method: 'POST',
        headers: {
            "Authorization": "Bearer "+access_token
        },
        json: true
    }
    try {
        return await rp(options);
    } catch(err) {
        throw Error(err);
    }
}
  • 위는 서버에서 카카오로 로그아웃 요청을 보낼 때 사용한 함수이다.

연결끊기

  • 카카오계정과 서비스간의 연결을 끊는 기능.
exports.disconnectKakao = async (req, res, next) => {
    try {
        const body = await kakao.unlink(req.session.access_token);
        req.session.destroy();
        return res.status(302).redirect('/user');
    } catch(err) {
        throw Error(err);
    }
}
const rp = require('request-promise');
/**
 * Kakao REST API 연결끊기
 */
exports.unlink = async (access_token) => {
    const options = {
        uri: 'https://kapi.kakao.com/v1/user/unlink',
        method: 'POST',
        headers: {
            "Authorization": "Bearer "+access_token
        },
        json: true
    }
    try {
        return await rp(options);
    } catch(err) {
        throw Error(err);
    }
}
  • 로그아웃과 마찬가지로 카카오에 요청을 보낸 후 서버의 세션DB에 남아있는 토큰을 삭제했다.

References

Github

profile
권오남 / Onam Kwon

0개의 댓글