한국외국어대학교 컴퓨터전자시스템공학전공 손종인
한국외국어대학교 컴퓨터전자시스템공학전공 장혜진
현재 우리는 많은 스마트기기에 둘러싸여 살고 있습니다. 이로 인해, 어디서든 버튼 하나만 누르면 개인 정보를 다른 사이트에 빠르게 보낼 수 있게 되었습니다. 그만큼 개인 정보를 전송하기 쉬워졌고, 유출 또한 쉬워졌습니다. 그로 인해, 보안에 대한 중요성이 점점 증가하고 있는 추세입니다.
oAuth2.0은 이러한 추세에 맞추어 개인 정보 보안에 초점을 둔 기술입니다. oAuth2.0이 어떤 환경에서 쓰면 좋을지 간단하게 우리의 일상생활과 관련 지어 설명해보겠습니다.
택배기사가 고객에게 물건을 배달하는 상황을 생각해봅시다. 고객이 택배를 주문하였을 때, 택배기사가 출입증이 있을 경우와 없을 경우로 나누어 살펴보겠습니다.
위 상황에서 출입증이 없을 경우는 고객의 개인 정보를 계속 제출하면서 외부인에게 노출될 확률이 높아집니다. 반대로 출입증이 있는 경우는 출입할 때 고객의 개인 정보를 쓰지 않고, 고객이 택배기사의 출입증을 언제든지 삭제할 수 있습니다. 그러므로 외부인에게 개인 정보가 노출될 확률이 매우 낮아집니다.
이와 비슷하게 oAuth2.0은 데이터 교환에서 위 예시에서 나온 출입증과 같은 역할을 하는 access token을 발급해 개인 정보의 노출을 방지해줍니다. 이 방법을 사용하면 사용하지 않았을 때에 비해 정보 보안의 수준이 올라가게 됩니다.
그렇다면 이 예시를 oAuth2.0에 대입하면 어떻게 될까요? 표를 통해 살펴봅시다.
정보를 제공해주는 고객은 Resource Owner, 배달해주는 택배기사를 Client라고 합니다. 또한, 고객의 주소를 제공해주는 경비실은 Resource Server, 출입증은 Access Token, 출입증 인증 방식은 API라 볼 수 있습니다. 그럼 oAuth2.0의 구조는 어떻게 될까요?
다음 챕터에서 자세히 살펴보도록 합시다.
oAuth는 Resource Owner, Client, Resource Server 이렇게 3가지가 서로 상호작용을 하는 구조입니다. 이 챕터에서는 보기 쉽게 Resource Owner는 고객, Client는 클라이언트, Resource Server는 서버라 칭하겠습니다.
그럼 이제부터 oAuth2.0의 실행 과정을 살펴보도록 하겠습니다.
① 클라이언트는 서버에 등록을 하여 식별할 수 있는 정보를 얻은 후 고객은 개인정보 전송 동의를 서버에게 보냅니다. 동의를 받은 서버는 고객의 Id와 Password를 받아 로그인하고, 클라이언트에게 고객의 개인정보를 전송합니다.
② 서버는 개인정보를 전송한 클라이언트를 인증할 수 있도록 고객에게 정보를 보냅니다. 정보를 받은 고객은 클라이언트에게 전달하고 클라이언트는 다시 서버에 보내 인증을 완료합니다. 이 때, 인증 정보를 받는 URL이 바로 Redirect URL입니다.
③ 인증을 완료한 서버는 클라이언트에게 Access Token을 발급합니다. 여기서 Access Token은 후에 클라이언트가 서버에 접속해 고객의 개인정보를 가져올 때 다시 인증 과정을 거치지 않게 하는 역할을 합니다.
④ Access Token을 발급받은 후, 고객이 서비스 요청을 할 경우는 다음과 같습니다.
그럼 이제부터 본격적으로 oAuth2.0을 쓰는 방법을 알아볼까요?
oAuth2.0은 다양한 언어로 만들어질 수 있습니다. 이 문서에서는 Node.js를 이용해 oAuth2.0을 다뤄볼려고 합니다.
그럼, 먼저 개발환경을 설치해봅시다.
① Node.js 설치
https://nodejs.org/ko/ 에 들어가 다운로드 및 설치를 진행합니다.
② Visual Studio Code 설치
https://code.visualstudio.com/ 에 들어가 다운로드 및 설치를 진행합니다.
**③ Express 설치 **
https://nodejs.org/ko/ 에 들어가 다운로드 및 설치를 진행합니다.
④ Postman 설치
https://www.postman.com/downloads/ 에 들어가 다운로드 및 설치를 진행합니다.
mkdir oAuth
cd oAuth
npm init
[
{
"id" : "admin",
"name" : "관리자",
"password" : "1234"
}
]
"src/index.js"
"dev": "node src/index.js",
이렇게 추가시켜주면 index.js를 실행할 때, npm run dev를 입력해주면 코드가 실행되게됩니다.
npm install body-parser --save
npm install cookie-parser --save
npm install jsonwebtoken --save
npm install express --save
위의 모듈들이 잘 설치가 되었다면 package.json파일을 열었을 때, 다음과 같은 화면이 뜨게 됩니다.
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const jwt = require("jsonwebtoken");
const app = express(); //app을 이용해 express기능을 사용하겠다는 코드
app.use(bodyParser.json()); //express에서 bodyparser기능을 사용하겠다는 코드
app.use(cookieParser()); //express에서 cookieparser기능을 사용하겠다는 코드
다음은 우리가 실행시킬때 필요한 port를 정해줄 코드와 userData를 db폴더 안에 있는 user.json파일에서 가져와줄 코드를 작성합니다.
const port = 3000; //port 생성
const user = require("./db/user.json"); //user를 이용해 user.json에서 data를 사용하겠다는 코드
app.get("/", function (req, res) {
res.send("hello world!");
});
그리고 listen함수를 이용해 port를 설정해줍시다.
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
vscode 터미널 창에 코드를 실행 시킨 후, Postman에 들어가 hello world!가 뜨는지 실행시켜봅니다. Method는 get, URL은 http://localhost:3000/ 로 하여 Send해 봅시다.
다음은 login을 구현해봅시다. 밑의 코드를 index.js파일에 타이핑 또는 복사해줍니다. login은 사용자가 id와 password를 입력하면 입력한 id와 password의 존재 유무를 판단해 존재 한다면, refresh token을 발급해주는 기능을 합니다.
app.post("/login", (req, res) => {
const loginUser = user.find((u) => u.id === req.body.id);
//사용자가 입력한 id가 user.json파일에 있을 경우 loginUser가 존재합니다.
if (loginUser && loginUser.password === req.body.password) {
// loginuser가 존재하고 loginuser의 password가 동일한 경우
const refreshToken = jwt.sign(loginUser, superAwesomeCode, {
expiresIn: "1d",
}); //유효기간이 1일인 refresh token을 만들어 줍니다.
res.cookie("rt", refreshToken); //refresh token을 rt라는 이름으로 cookie에 저장합니다.
res.send({
token: refreshToken,
}); //json 형식으로 refresh token을 보내줍니다.
} else {
//id나 password가 다를 경우
res.status(401);
res.send("해당 User가 없거나, Password가 맞지 않습니다.");
}
또한, refresh token을 만들기 위해 필요한 secretkey인 superAwesomeCode를 위에서 정의해줍니다.
const superAwesomeCode = "차은우";
9-1) id와 password가 틀린 경우
9-2) id와 password가 맞는 경우
app.post("/token", (req, res) => {
let refreshToken;
if (req.body.rt) {
//rt가 존재할 경우, rt를 refreshToken이라고 선언합니다.
refreshToken = req.body.rt;
} else {
//rt가 존재하지 않을 경우, cookie안에 있는 rt를 refreshToken으로 선언합니다.
refreshToken = req.cookies["rt"];
}
try {
if (!refreshToken) {
//만약 refreshToken이 없다면, "Refresh token이 없습니다."를 출력하게합니다.
throw "Refresh token이 없습니다.";
}
const decoded = jwt.verify(refreshToken, superAwesomeCode); //verify함수를 사용해 refreshToken을 decoding합니다.
const user = {
id: decoded.id,
name: decoded.name,
};//decoding해 나온 id와 name값을 user로 선언합니다.
const accessToken = jwt.sign(user, superAwesomeCode, {
expiresIn: "1h",
});//위에서 선언한 user와 secretkey인 superAwesomeCode를 이용해 만료 시간이 1시간인 access token을 만듭니다.
res.send({ token: accessToken });//만들어진 access token을 json형태로 보내줍니다.
} catch (e) {
//acess token의 발급이 실패했을 경우
res.status(401);
res.send("Access token 발급에 실패하였습니다.");
}
});
위의 내용을 Postman을 통해 잘 실행되어지는지 알아봅시다. Method는 post, URL은 http://localhost:3000/token 로 하여 Send해봅시다.
이번에는 Access token의 정보를 확인하는 작업을 하는 코드를 만들어 봅시다. 아래에 있는 코드를 타이핑 또는 붙여넣어주세요.
이 코드는 위에서 만들어진 Access token을 보내주면 그 안의 정보를 출력해주는 역할을 합니다.
app.post("/vertify", (req, res) => {
const token = req.body.token; //body안에 있는 access token을 token이라고 선언
if (!token) {
//만약 token이 존재하지 않을 경우
res.status(402);
res.send("검증 대상인 token 이 없습니다.");
return;
}
try {
//token이 존재할 경우
const decoded = jwt.verify(token, superAwesomeCode);//access token을 decoding합니다.
const { id, name } = decoded;
res.send({ id, name });//decoding된 정보의 id와 name을 가져옵니다.
} catch (e) {
//token이 유효하지 않을 경우
res.status(401);
res.send("token이 유효하지 않습니다.");
}
});
{
"token" : "위에서 발급받은 Access token 삽입"
}