- Published on
사용자 인증 방법 및 암호화 | node.js
Intro
해외spa 패션사이트를 들어가면 맞춤형 광고를 위한 사용자 쿠키저장을 허용할 것인가라는 팝업창이 뜨면서 필수적 쿠키와 광고를 위한 쿠키 저장 등이 뜬다. 몇년전에는 안떴던거 같은데..정책이 바뀐것인지 아니면 내가 몰랐던 것인지는 모르겠지만 아무리 체크를 해도 몇일이 지나면 다시 떠서 여간 번거로운게 아니였는데 이번에 왜그런지 알게되었다. 이 포스팅에서는 쿠키와 세션을 사용하는 이유와 각각의 개념, 그리고 jwt 인증방식을 소개하고 비밀번호 암호화하는 코드 순으로 이어진다.쿠키와 세션을 사용하는 이유
사실 깊게 생각해보지 않았으나 포스팅을 작성하기 위해 자료를 찾아보다보니 알게되었다. 워낙 정리가 잘된 블로그가 많아서 내용을 참고하여 작성한다. 쿠키와 세션을 사용하는 이유는 http 프로토콜의 약점을 보완하기 위해서 사용된다. http는 Connectionless(비연결성), Stateless(무상태) 라는 특징을 가진다.Connectionless는 클라이언트가 서버에 요청을 하고 응답을 받으면 바로 TCP/IP연결을 끊고 유지하지 않는것이다. 이를 통해 서버의 자원을 효율적으로 관리하고 수많은 클라이언트의 요청에도 대응할 수 있게 한다. 일반적으로 초 단위 이하의 빠른 속도로 응답하여 데이터 전송속도가 빠르다. 이 부분에 대해서 궁금해서 좀더 자료를 찾아보았다. 우선 네트워크에 대해 살펴볼 필요가 있었다. TCP는 Connection-Oriented(연결지향)프로토콜이며 1:1 연결상태를 유지하는 통신을 한다. 그리고 IP,UDP는 Connectionless 프로토콜이다. TCP/IP 계층에는 Application, TCP/UDP, IP, Interface가 존재하며 TCP와 UDP는 IP 프로토콜 위에 존재한다. IP는 성능중심의 Connecionless 서비스를 제공하며 한번 전송하면 그 패킷에 관여하지 않는다.
Stateless는 서버가 클라이언트의 이전 상태를 보존하지 않는다는 의미이다. 상태를 보관하지 않으므로 클라이언트의 요청에 어느 서버가 응답해도 상관없다. 따라서 클라이언트 요청이 대폭 증가해도 서버를 증설해서 해결할 수 있다. 단점은 클라이언트가 하고자 하는 최종 목적을 위해 지나는 과정마다 점점 전달해야하는 내용이 많아진다는 것이다.
이러한 특징들로 인해 서버는 클라이언트가 누구인지 계속 인증을 해줘야한다. 이는 웹페이지의 로딩을 느리게 만드는 요인이 되고 또한 번거롭기 때문에 쿠키와 세션을 이용하여 클라이이언트의 정보를 유지한다.
Cookie
사용자가 웹사이트를 방문한 경우 그 사이트가 사용하는 서버에서 사용자의 컴퓨터에 저장하는 기록 정보 파일이다. 쿠키는 http메세지의 헤더(header)영역을 통해 서버와 브라우저 사이에서 주고 받게 되며 클라이언트 요청에 응답할 때 쿠키는 전달된다.다음은 쿠키를 이용하여 만든 사용자 인증예제이다. 팝업창이 뜨고 오늘 그만보기를 클릭하면 쿠키를 저장하여 새로고침을 해도 브라우저를 껐다 다시 켜도 24시간 동안 같은 팝업창이 뜨지 않게끔 한다.
아래는 주요 코드이다. 상세설명을 주석으로 달아놓았다.
//app.js
const express = require("express");
const cookieParser = require("cookie-parser");
const app = express();
const PORT = 8000;
app.set("view engine", "ejs");
//cookie-parser
//암호화 쿠키
app.use(cookieParser("abcdef"));
//cookie option객체
const cookieConfig = {
//httpOnly: 웹 서버를 통해서만 쿠키에 접근가능(자바스크립트에서 document.cookie를 이용해서 쿠키에 접속하는 것을 차단)
//maxAge : 쿠키의 수명(밀리초 단위)
//expires : 만료날짜를 GMT 시간 설정
//path : 해당 디렉토리와 하위 디렉토리에서만 경로가 활성화되고 웹 브라우저는 해당하는 쿠키만 웹서버에 전송
//-> 쿠키가 전송될 URL 특정 가능(기본값: '/')
//domain: 쿠키가 전송될 도메인을 특정 가능(기본값: 현재 도메인)
//secure : 웹브라우저와 웹서버가 https로 통신하는 경우만 쿠키를 서버에 전송
//signed: 쿠키의 암호화결정(req.signedCookies 객체에 들어있음)
httpOnly: true,
// maxAge: 60 * 1000, //1분
maxAge: 24 * 60 * 60 * 1000, //24시간
signed: true, //암호화 쿠키 사용시
};
app.get("/", (req, res) => {
if (req.signedCookies.myCookie) {
res.render("c_index", { cookie: false });
} else {
res.cookie("myCookie", "myValue", cookieConfig);
res.render("c_index", { cookie: true });
}
});
//암호화 쿠키 사용시
app.post("/getCookie", (req, res) => {
res.send({ cookie: req.signedCookies });
});
app.get("/clearCookie", (req, res) => {
//setCookie한 값과 동일해야함
res.clearCookie("myCookie", "myValue", cookieConfig);
res.send("clear Cookie");
});
app.listen(PORT, () => {
console.log(`http://localhost:${PORT}`);
});
//views/c_index.ejs
...생략...
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
...생략...
<body>
<input
type="hidden"
id="cookieValue"
name="cookieValue"
value="<%= cookie %>"
/>
<!-- Modal -->
<div class="modal" id="myModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="headerTxt">안내</div>
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">×</span>
</button>
</div>
<div class="titleAndbody">
<div class="titleAndX">
<h3 class="modal-title" id="staticBackdropLabel">
인터넷 익스플로러(IE) 지원 종료 안내
</h3>
</div>
<div class="modal-body">
<p>
인터넷익스플로러(IE) 서비스 지원이
<strong>2022년 6월 15일</strong>
종료됨에<br />
따라 안전하고 원활한 LH청약센터 이용을 위해 크롬, 사파리, 엣지
<br />
등의 웹브라우저를 이용해주시기 바랍니다.
</p>
</div>
</div>
<div class="modal-footer">
<div class="chkbox">
<input
type="checkbox"
name="xBtn"
value="x"
id="chkbox"
onchange="closeToday()"
/>오늘그만보기
</div>
</div>
</div>
</div>
</div>
<!-- <a href="/clearCookie">쿠키삭제</a> -->
<script>
const modal = document.getElementById("myModal");
const cookieValue = document.getElementById("cookieValue").value;
console.log("cookieValue", cookieValue);
//페이지가 처음 열릴 때, 쿠키의 값을 확인
modal.style.display = cookieValue === "true" ? "block" : "none";
function closeToday() {
modal.style.display = "none";
const checkedClose = [];
const checked = document.querySelectorAll(
'input[type="checkbox"]:checked'
);
checked.forEach((check) => {
checkedClose.push(check.value);
});
console.log("checked", checked);
const data = {
checkedClose,
};
axios
.post("/getCookie", data, {
headers: {
"Content-Type": "application/json",
},
})
.then((res) => {
if (res.data.cookie) {
console.log("쿠키있음");
console.log(res);
} else {
console.log(res);
console.log("쿠키없음");
}
})
.catch((error) => {
console.error("쿠키 오류: ", error);
});
}
</script>
</body>
</html>
Session
쿠키를 기반으로 하나, 쿠키와는 달리 세션ID를 부여하여 클라이언트를 구분하고 웹브라우저가 서버에 접속하여 브라우저를 종료할때까지 인증상태를 유지한다. 사용자에 대한 정보를 서버에 두기 때문에 쿠키보다 보안에 좋지만 사용자가 많아질수록 서버 메모리를 차지하여 성능저하의 요인이 된다. 또한 매번 요청 시 세션 저장소를 조회해야하는 단점도 있다. 댓글을 작성과 삭제할때, 로그인 후 마이페이지를 볼 때, 구매한 상품내역을 볼 때 모두 세션을 이용한다.다음은 세션을 이용한 예제이다. 로그인 후 마이페이지로 넘어가서 사용자를 확인한다.
다음은 주요 코드이다.
const express = require("express");
const session = require("express-session");
const app = express();
const PORT = 8000;
//body-parser
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
//세션
//옵션객체
//httpOnly: 값을 true로 하면 사용자가 자바스크립트를 통해서 세션을 사용할수 없도록 강제
//secure: 값을 true로 하면 https에서만 세션을 주고받음
//secrete : 안전하게 쿠키를 전송하기 위해 쿠키 서명값(세션을 발급할 때 사용되는 키)
//resave: 세션에 수정사항이 생기지 않더라도 매 요청(req)마다 세션을 다시 저장할 것인지 물어봄, 덮어쓰기 가능하게 하기
//saveUninitialized: 세선에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정
app.set("view engine", "ejs");
app.use(
session({
secret: "mySessionKey",
resave: false,
saveUninitialized: true,
// store: new FileStore(),
})
);
let userid;
let password;
const userInfo = { id: 'ㅇㄹㅇㄹ', pw: '12121' };
app.get("/", (req, res) => {
console.log(req.session);
res.render("index");
});
app.get("/login", (req, res) => {
res.render("login");
});
app.post("/login", (req, res) => {
userid = req.session.userid;
password = req.session.password;
console.log("세션아이디 일치여부", userid, password);
if (userid === userInfo.id && password === userInfo.pw) {
res.send({ success: true });
} else {
res.send({ success: false });
}
});
app.get("/s_login", (req, res) => {
console.log("세션", { userid, password });
res.render("s_login", { userid: userid, password: password });
});
app.get("/destroy", (req, res) => {
req.session.destroy(() => {
//세션이 만료되었기때문에 해당페이지에서는 더이상 접근하면 안됨, 콜백함수로 메인페이지로 리다이렉트
res.redirect("/");
});
});
app.listen(PORT);
JWT
위 쿠키와 세션 인증방식에서 더 나아간 것이 JWT(Json Web Token)이다. 서버에 세션 정보를 저장하지 않고 인증에 필요한 정보들을 암호화시킨 json 토큰을 의미한다. 이 JWT 토큰을 http헤더에 실어 서버가 클라이언트를 식별하는 방식이 JWT 기반 인증방식이다. 대부분 로그인할 때 사용한다고 한다.JWT인코딩
👉 https://jwt.io/
위 링크로 접속하면 JWT를 만들거나 확인해볼수 있다.
JWT구조
JWT구조는 Base64로 인코딩되어 HEADER(헤더), PAYLOAD(내용), SIGNATURE(서명) 위와 같이 '.'을 구분자로 하여 토큰이 구성된다.헤더.페이로드.서명
완성된 토큰은 아래와 같다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
아래는 디코딩한 결과이다.
HEADER:ALGORITHM & TOKEN TYPE{
"alg": "HS256",
"typ": "JWT"
}
헤더에서 해시 알고리즘과 토큰의 타입을 정의할 수 있다.
PAYLOAD:DATA{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
PAYLOAD에서는 전달하는 데이터를 포함할 수 있다. claim 이라고 불리는 사용자의 인증정보가 담기게 된다. PAYLOAD는 서명된 파트가 아니라 단순 Base64 인코딩이 된 파트이며 누구나 디코딩하여 열람이 가능하다. 때문에 password 같은 보안이 필요한 요소들은 PAYLOAD에 담기면 안된다.
VERIFY SIGNATUREHMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret //유저가 지정하는 비밀코드
)
헤더와 페이로드가 비밀키로 서명되어 저장된다.
node.js에서는 jsonwebtoken을 설치하여 사용할 수 있다.
//views/index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<title>Document</title>
</head>
<body>
<h1>JWT 실습</h1>
<div id="info"></div>
<script>
(async function () {
const token = localStorage.getItem("login");
const info = document.querySelector("#info");
let data = "";
if (!token) {
data = '<a href="/login">로그인</a>';
} else {
const res = await axios({
method: "POST",
url: "/token",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (res.data.result) {
data = `
<p>${res.data.name}님 환영합니다</p>
<button type='button' onclick='logout()'>로그아웃</button>
`;
}
}
info.innerHTML = data;
})();
function logout() {
localStorage.clear();
document.location.reload();
}
</script>
</body>
</html>
//app.js
const jwt = require("jsonwebtoken");
const SECRET = "secretKey";
const user = { id: "ddd", pw: "1234", name: "ddd" };
console.log(user.id, user.pw);
app.post("/login", (req, res) => {
try {
const { id, pw } = req.body;
const { id: uId, pw: uPw } = user;
if (id === uId && pw === uPw) {
//sign메소드를 통해 access token 발급!
const token = jwt.sign({ id }, SECRET);
res.json({ result: true, token });
// send : json, string ..모든걸 다 보낼수 있음.
} else {
res.json({ result: false, message: "로그인 정보가 올바르지 않습니다." });
}
} catch (err) {
console.log(err);
}
});
app.post("/token", (req, res) => {
console.log(req.headers.authorization);
//뷰에서 보내준 헤더 받기
if (req.headers.authorization) {
const token = req.headers.authorization.split(" ");
try {
// verify를 통해 값 decode!
const result = jwt.verify(token[1], SECRET);
console.log(result);
if (result.id === user.id) {
res.json({ result: true, name: user.name });
}
} catch (err) {
console.log(err);
res.json({ result: false, message: "인증된 회원이 아닙니다." });
}
} else {
res.redirect("/login");
}
});
postman으로 jwt를 확인해볼 수 있다. 암호가 맞으면 success에 true가 뜨고 아니면 false가 뜬다.
비밀번호 암호화
사용자가 회원가입을 했을 때 비밀번호와 아이디를 DB에 저장한다. 이때 비밀번호를 평문으로 저장한다면 보안상 문제가 크다. 때문에 비밀번호는 암호화 되어 저장된다면 나중에 누군가 DB 정보를 탈취했다는 사고가 일어났을 때 암호화된 비밀번호이기 때문에 복호화가 불가능하고 의미 또한 파악할수 없어 평문보다 비교적 안전하다.암호화를 위해 가장 먼저 분류되는 방식은 양방향, 단방향 알고리즘이다. 양방향 알고리즘은 암호화된 암호문을 복호화 할 수 있는 알고리즘을 의미한다. 반대로 단방향 알고리즘은 암호화는 수행하지만 절대 복호화가 불가능한 알고리즘이다. 양방향 알고리즘에는 대칭키(비공개키)방식과 비대칭키(공개키)방식으로 나누어지며, 단방향은 해시방식이 대표적이다.
세부적으로 비밀번호 암호화 방법에는 4가지 유형이 있다.
대칭키 : 어떤 정보를 암호화, 복호화 할때 사용하는 키가 동일(대칭)한 경우이다. 따라서 암호화 된 정보를 전달하고 확인하기 위해서는 송,수신자 둘다 똑같은 키를 가지고 있어야한다. (비밀키가 하나이다.)
공개키 : 대칭키는 키의 분배, 디지털 서명이 불가능하다는것에 단점이 있다. 그 단점을 보완한 것이 공개키(비대칭키)암호화 방식이다. (속도는 대칭키가 약 1000배 빠르다고 한다.) 공개키와 비밀키 두개가 존재하며 암호화, 복호화에 사용하는 키가 서로 다르다. 공개키는 누구든 사용할 수 있지만, 비밀키는 선택된 소수만 사용할 수 있다. 대표적인 알고리즘으로 RSA, Elgamal 등이 있다.
해싱 : 해시함수에 문자열 입력값을 넣어서 특정한 값으로 추출하는 것을 의미한다.
솔트 : 해시함수를 돌리기 전에 원문에 임의의 문자열을 덧붙이는 것을 말한다. 같은 비밀번호를 사용하더라도 salting된 문자열은 서로 다르기 때문에 각 사용자의 비밀번호는 서로 다른값으로 저장된다.
node.js에서는 crypto, bcrypt 라는 내장모듈이 있다. crypto는 단방향, 양방향 암호화 처리가 가능하고 bcrypt는 단방향 암호화 모듈이지만 가장 많이 사용하고 보안이 더 뛰어나며 사용문법도 쉽다.
아래 node.js 코드를 첨부한다.
//===단방향 암호화
const crypto = require("crypto"); //내장모듈
//fuctions -> listen 밑에는 서버코드를 입력해도 실행안됨
const creatHashedPassword = (password) => {
//createHash(알고리즘).update(암호화할 값).digest(인코딩방식)
return crypto.createHash("sha512").update(password).digest("base64");
};
//암호생성
const salt = crypto.randomBytes(16).toString("base64"); //솔트생성
const iterations = 100; //반복횟수
const keylen = 64; //생성할 키의 길이
const digest = "sha512"; //해시 알고리즘
const createPbkdf = (password) => {
//pbkdf2Sync(비밀번호, 솔트, 반복횟수, 키의 길이, 알고리즘)으로 생성
//반환되는 값은 buffer값
const hash = crypto.pbkdf2Sync(password, salt, iterations, keylen, digest);
return hash.toString("base64");
};
//암호비교
const verifyPassword = (password, salt, dbPassword) => {
const compare = crypto
.pbkdf2Sync(password, salt, iterations, keylen, digest)
.toString("base64");
if (compare === dbPassword) {
return true;
} else {
return false;
}
};
//====양방향 암호화
const algorithm = "aes-256-cbc"; //256비트 키를 사용, 블록크기는 128비트
const key = crypto.randomBytes(32); //1바이트 = 8비트
const iv = crypto.randomBytes(16); //초기화 벡터, 데이터블록을 암호화할때 보안성을 높히기 위해 사용
//암호화
const cipherEncrypt = (word) => {
const cipher = crypto.createCipheriv(algorithm, key, iv); //암호화 객체 생성
let encryptedData = cipher.update(word, "utf-8", "base64"); //암호화할 데이터 처리
encryptedData += cipher.final("base64"); //c최종결과 생성
return encryptedData;
};
//복호화
const decipher = (encryptedData) => {
const decipher = crypto.createDecipheriv(algorithm, key, iv); //복호화 객체
let decrytedData = decipher.update(encryptedData, "base64", "utf-8");
decrytedData += decipher.final("utf-8");
return decrytedData;
};
//bcrypt (단방향)
//암호화
const bcrypt = require("bcrypt");
const saltNum = 10;
const bcryptPassword = (password) => {
return bcrypt.hashSync(password, saltNum);
};
//비밀번호가 맞는지 검증
const comparePassword = (password, dbPassword) => {
return bcrypt.compareSync(password, dbPassword);
};
JWT와 비밀번호 암호화를 이용한 실습예제
회원가입 시, DB에 암호화된 비밀번호를 저장하고,로컬스토리지에 jwt토큰을 저장한 뒤 로그인 한 사용자면 인증되어 사용자 정보 페이지를 조회 할 수 있다.
로그인 하지 않았을때는 사용자 정보 페이지가 뜨지 않는다.
DB(mysql)에 회원정보가 저장된 모습이다.
파일구조이다. MVC패턴을 이용하여 구현하였다.
C:.
│ .gitignore
│ app.js
│ index.js
│ package-lock.json
│ package.json
│
├─config
│ config.json
│
├─controller
│ Cuser.js
│
├─models
│ index.js
│ user.js
│
├─routes
│ user.js
│
└─views
index.ejs
login.ejs
profile.ejs
signup.ejs
주요 로직이 controller에 있어 이것만 첨부한다.
bcrypt을 사용하여 암호화된 비밀번호를 DB에 넣고 jwt 토큰을 뷰에 보내 로컬스토리지에 저장시킨다.
//controller/Cuser.js
const models = require("../models");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const SECRET = "secretKey";
let hash = "";
let token = "";
//회원가입
exports.getSignupList = async (req, res) => {
try {
const { id, pw, name } = req.body;
console.log(pw);
hash = bcryptPassword(pw);
const result = await models.User.create({
pw: hash,
name: name,
userid: id,
});
if (result) {
res.send({ result: true });
}
} catch (error) {
console.log(error);
}
};
//bcrypt (단방향)
//암호화
const saltNum = 10;
const bcryptPassword = (password) => {
return bcrypt.hashSync(password, saltNum);
};
//로그인
exports.getSignin = async (req, res) => {
try {
const { id, pw } = req.body;
console.log("로그인비번", pw);
const result = await models.User.findOne({
//where 안에는 객체이다.
where: { userid: id }, //아이디로 필터
});
if (!result) { //아이디가 없으면
res.send({ success: false });
} else { //아이디가 있으면
const compare = comparePassword(pw, result.pw);
if (compare === true) {
token = jwt.sign({ id }, SECRET);
loginid = id;
res.send({ success: true, id: id, token });
} else {
res.send({ success: false });
}
}
} catch (error) {
console.log(error);
}
};
//비밀번호가 맞는지 검증
const comparePassword = (password, dbPassword) => {
return bcrypt.compareSync(password, dbPassword);
};
exports.getProfile = (req, res) => {
console.log("profile실행", loginid);
models.User.findOne({
where: { userid: loginid },
}).then((result) => {
console.log("프로필", result);
res.send({ id: loginid });
});
};
exports.Tokenverify = (req, res) => {
if (req.headers.authorization) {
const token = req.headers.authorization.split(" ");
console.log("토큰 인증 실행");
try {
const result = jwt.verify(token[1], SECRET);
console.log(result);
if (result.id === loginid) {
res.send({ result: true, id: result.id });
}
} catch (err) {
console.log(err);
res.send({ result: false });
}
} else {
res.render("login");
}
};
As a result...
계속 자료를 찾다보니 네트워크의 동작까지 알아야 개념이 더 잘 이해가 가는것 같다. 정처기 시험에서 잠깐 봤던 암호화 부분을 이렇게 실제 프로그래밍으로 배우게 되어 좋았다. 또한 이전에는 프로젝트 할때 평문으로 비밀번호를 저장했었는데 지금은 DB에 저장할 때 한번 더 신경 쓸거 같다.출처
쿠키/세션
https://interconnection.tistory.com/74
https://hahahoho5915.tistory.com/32
https://velog.io/@duarufp06/HTTP-Stateless-Connectionless-HTTP-%EB%A9%94%EC%8B%9C%EC%A7%80-%EA%B0%9C%EB%85%90
https://smpark1020.tistory.com/369
https://simhyejin.github.io/2016/07/04/connectionoriented-connectionless/
https://roxy.iro.ooo/infra/protocol/http/http-stateful-stateless
https://www.daleseo.com/http-cookies/
https://btcd.tistory.com/40
https://brunch.co.kr/@jinyoungchoi95/1JWT
https://velog.io/@vamos_eon/JWT%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B0%80-1
https://www.daleseo.com/jwt/#google_vignette암호화
https://www.okta.com/kr/identity-101/password-encryption/
https://universitytomorrow.com/22
https://www.crocus.co.kr/1236
https://velog.io/@bbaekddo/cs-3
https://javaplant.tistory.com/26
https://velog.io/@neity16/NodeJS-JWT-Token-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0
