요즘 어느 페이지를 들어가나 보이는 버튼이 있습니다.
바로 '소셜 계정으로 로그인' 입니다.
(이미지 출처 : https://developers.naver.com)
이 버튼을 통해 로그인 하게 되면 사이트가 직접 사용자를 인증하지 않고, 다른 사이트를 통해 우회적으로 사용자가 인증됩니다.
사이트에서 네이버로 이 사용자가 등록된 사용자가 맞는지 인증을 요청하면 네이버는 사용자 확인 후 해당 사이트에 접근 토큰을 넘겨줍니다.
이것이 바로 OAuth(Open Authorization) 입니다.
사용자가 클라이언트 웹 페이지에 비밀번호를 넘겨줄 필요가 없으므로 개발자는 비밀번호 보호에 대해 고려하지 않아도 된다는 장점이 있습니다.
OAuth 2.0은 1.0에서 알려진 보안문제를 개선한 버전입니다.
오픈 API 요청 시 클라이언트 인증 방법으로 서명 대신 HTTPS를 사용하도록 의무화하여 안전성을 높였습니다.
1.0에서는 웹 애플리케이션만 지원했지만 2.0에서는 다양한 유형의 클라이언트와 이를 고려한 권한 승인 방법을 정의하여 더 넓은 범위에서 사용할 수 있게 되었습니다.
접근 토큰 재발급을 위한 재발급 토큰(Refresh Token)이 도입되었습니다. 접근 토큰은 유출을 막기 위해 비교적 짧은 만료 시간을 가지는데, 재발급 토큰이 있다면 사용자가 다시 로그인했을 때 재발급 토큰을 이용해 접근 토큰을 다시 발급받을 수 있습니다.
카카오를 이용한 OAuth 로그인을 만들어봅시다.
시작하기 전에, 다음 페이지를 참고해 개발자 등록과 사이트 등록을 마쳐주시기 바랍니다.
https://developers.kakao.com/docs/latest/ko/getting-started/app
실습에서는 http://localhost:3000 을 통해 접근하고, http://localhost:3000/auth/kakao/callback 를 Redirect URI로 사용할 예정입니다.
서버를 열어야 하니 express가 필요하고, OAuth 로그인을 하기 위한 passport와 express-session이 필요합니다. 그리고 따로 관리해야할 값을 설정하는 dotenv, html을 쉽게 만들기 위한 nunjucks도 설치합니다.
passport는 node용 인증 미들웨어입니다. 각 페이지에 알맞은 Strategy를 설정하고나면 간단하게 인증을 확인할 수 있습니다.
쿠키와 세션
둘 모두 사용자가 웹사이트에 방문할경우 정보를 저장하는 것이지만 저장되는 곳과 저장 기한이 다릅니다.
쿠키 : 사용자의 컴퓨터에 저장되는 키-값 정보파일. 만료일이 되면 컴퓨터에서 삭제됨.
세션: 서버에 저장되는 정보. 브라우저가 꺼지면 삭제됨
+) 세션도 쿠키를 사용합니다. 페이지를 이동할 때 서버는 세션id를 쿠키에 담고, 다음 페아지에서 세션 id로 데이터를 이어받습니다. 하지만 브라우저가 꺼지면 세션id는 쓸모없는 정보가 되므로 보안적인 측면에서 쿠키보다 안전합니다.
// ./App.js
const express = require("express");
const nunjucks = require("nunjucks");
const passport = require("passport");
const session = require("express-session");
class App {
constructor() {
this.app = express();
this.app.use(express.static("./template"));
this.app.use(require("./routes"));
this.setViewEngine();
this.setSession();
}
setSession() {
this.app.use(
session({
secret: "hokim",
resave: false,
saveUninitialized: true,
cookie: {
maxAge: 2000 * 60 * 60,
},
})
);
this.app.use(passport.initialize());
this.app.use(passport.session());
}
setViewEngine() {
nunjucks.configure("template", {
autoescape: true,
express: this.app,
});
}
}
module.exports = new App().app;
setViewEngine과 static 폴더 설정은 nunjucks와 css를 사용하지 않는다면 필요하지 않습니다.
기능만 생각하면 express, 라우팅, 세션 설정만 해도 됩니다.
// ./server.js
const app = require("./app.js");
const port = 3000;
const server = app.listen(port, function () {
console.log("Express listening on port", port);
});
기본설정을 해둔 App.js 내부의 App을 가져와서 3000번 포트를 열어줍니다.
이렇게 해야 만든 페이지에 localhost:3000으로 접근 할 수 있습니다.
'/'으로 접근하면 로그인 페이지(login.html)를 띄웁니다.
'auth/kakao'로 접근하면 카카오쪽으로 로그인을 넘깁니다.
'auth/kakao/callback'으로 응답을 받습니다.
성공응답이면 'auth/success', 실패 응답이면 'auth/fail'로 이동합니다.
// ./routes/index.js
const express = require("express");
const router = express.Router();
router.get("/", (req, res) => {
res.render("./login.html");
});
router.use("/auth", require("./auth"));
module.exports = router;
인증 관련한것은 모두 auth뒤에 있으므로 일괄적으로 auth로 라우팅 시키기 위해 router.use("/auth", require("./auth"));
를 사용했습니다.
// ./routes/auth/index.js
const express = require("express");
const router = express.Router();
const passport = require("../passport-kakao.js");
router.get("/kakao", passport.authenticate("kakao"));
router.get(
"/kakao/callback",
passport.authenticate("kakao", {
successRedirect: "/auth/success",
failureRedirect: "/auth/fail",
})
);
router.get(
"/success",
(req, res, next) => {
if (!req.isAuthenticated()) res.redirect("/");
else next();
},
(req, res) => {
res.render("success.html");
}
);
router.get("/fail", (req, res) => {
res.render("fail.html");
});
module.exports = router;
passport.authenticate("kakao"));
을 통해 카카오 로그인 페이지로 이동하고, 사전에 입력해둔 주소로 결과를 보내줍니다.
저는 결과를 받을 주소를 "/kakao/callback"
으로 해두었습니다. 결과를 받은 뒤에 성공이면 successRedirect로 이동하고, 실패면 failureRedirect로 이동합니다.
성공하지 않은 경우에도 url을 통해 성공 페이지에 접근할 수 있습니다. 이를 막기 위해 req.isAuthenticated()
을 체크해서 인증된 사용자인지 확인한 후, 인증되지 않았을 경우 카카오 로그인 버튼이 있는 페이지로 사용자를 이동시킵니다.
인증된 사용자인 경우에는 res.render("success.html");
로 성공 페이지를 보여줍니다.
실패한 경우에는 res.render("fail.html");
로 실패 페이지를 보여줍니다.
라우팅 설정을 했으니 실제로 카카오측으로 로그인을 넘길 수 있도록 passport를 작성해봅시다.
// ./routes/passport-kakao.js
const express = require("express");
const passport = require("passport");
const dotenv = require("dotenv");
const KakaoStrategy = require("passport-kakao").Strategy;
dotenv.config();
passport.use(
new KakaoStrategy(
{
clientID: process.env.KAKAO_KEY,
callbackURL: "http://localhost:3000/auth/kakao/callback"
},
async (accessToken, refreshToken, profile, done) => {
//console.log(profile);
try {
user = accessToken;
console.log(user);
console.log(refreshToken);
return done(null, user);
} catch (e) {
console.log(e);
}
}
)
);
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
done(null, user);
});
module.exports = passport;
.env 파일 안에 KAKAO_KEY의 값을 입력해 두었습니다. 그리고나서 dotenv.config();
를 선언해주면 process.env.keyName
으로 상수처럼 사용할 수 있습니다.
passport를 사용하기 위해서는 사용하려는 인증 사이트에 대한 Strategy가 필요합니다. 보통 passport-sitename 형식으로 되어있어 npm으로 다운받으면 됩니다.
Strategy를 지정할때는 passport.use를 사용합니다.
clientID
와 callbackURL
이 카카오 개발자 페이지에 적힌것과 다르다면 오류가 발생하므로 주의해야 합니다.
로그인이 실행되고 나면
async (accessToken, refreshToken, profile, done) => {}));
가 실행됩니다. 여기서 done에 유저 정보를 넣어 반환해야하는데, 이 경우에는 반환할 유저 정보가 없어서 그냥 access token을 넣었습니다.
done이란?
Passport는 자격을 확인 한 후 확인 콜백을 호출합니다. 이 확인 콜백이 done입니다.
1. 에러가 발생했을 경우(예 : 데이터베이스를 사용할 수없는 경우)에는 다음을 반환합니다.return done(err);
2. 자격 증명이 유효할 경우 done에 사용자 데이터를 담아 반환합니다.return done(null, user);
3-1. 자격 증명이 유효하지 않은 경우 사용자 대신을 false를 담아 반환합니다..return done(null, false);
3-2. 실패 이유를 나타내는 추가 정보 메시지를 제공 할 수도 있습니다.return done(null, false, { message: 'Incorrect password.'});`
기본적인 기능은 모두 끝났습니다!
이제는 로그인 페이지로 이동할 버튼과 성공페이지, 실패 페이지만 작성하면 됩니다.
nunjucks는 아주 편리한 툴입니다. html을 일일이 작성할 필요 없이 베이스 페이지를 하나 만들면 템플릿처럼 불러와서 쓸 수 있습니다.
예를들어 페이지의 기본 설정은 비슷한데 내용만 바꾸고 싶다면 nunjucks의 block을 활용하면 됩니다.
<!-- ./template/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" type="text/css" href="/my.css" />
</head>
<body>
<div class="box">{% block content %}{% endblock %}</div>
</body>
</html>
/* ./my.css */
.kakao_btn {
text-decoration: none;
font-size: 1.5rem;
color: rgb(49, 25, 11);
padding: 20px;
margin: 50px auto;
border-radius: 10px;
background-color: #fee500;
}
.box {
display: flex;
}
.middle {
display: inline-flex;
margin: 70px auto;
}
이런 베이스 페이지를 바탕으로 {% block content %}{% endblock %}위치의 데이터만 바꿔서 쓸 수 있습니다. 자바에서의 상속처럼 extends를 사용합니다.
로그인 페이지를 만들어봅시다.
<!-- ./template/login.html -->
{% set title = "로그인 페이지"%}
{% extends "base.html"%}
{% block content -%}
<a class="kakao_btn" href="/auth/kakao">카카오로 로그인</a>
{% endblock %}
성공 페이지를 만들어봅시다.
<!-- ./template/success.html -->
{% set title = "로그인 성공"%}
{% extends "base.html"%}
{% block content -%}
<h2 class="middle">카카오로 로그인 성공!</h2>
{% endblock %}
실패 페이지를 만들어봅시다.
<!-- ./template/fail.html -->
{% set title = "로그인 실패"%}
{% extends "base.html"%}
{% block content -%}
<h2 class="middle">카카오로 로그인 실패...</h2>
{% endblock %}
순식간에 페이지 세개를 만들었습니다!