토큰 기반 시스템은 stateless
무상태이다. 즉, 상태유지를 하지 않는다. 유저의 인증 정보를 서버나 세션에 담아두지 않으므로써 어제 배웠던 세션
의 문제점이 해결된다.
토큰기반 인증 절차
1.클라이언트가 서버에 아이디/비밀번호를 담아 로그인 요청을 보낸다.
2.아이디/비밀번호가 일치하는지 확인하고, 클라이언트에게 보낼 암호화된 토큰을 생성한다.
3.토큰을 클라이언트에게 보내주면, 클라이언트는 토큰을 저장한다.
4.클라이언트가 HTTP 헤더(authorization)에 토큰을 담아 보낸다.
5.서버는 토큰을 해독하여 토큰이 맞다면 클라이언트의 요청을 처리한 후 응답을 보내준다.
토큰기반 인증의 장점
1.Statelessness & Scalability (무상태성 & 확장성)
2.안전하다.
3.어디서나 생성 가능하다.
4.권한 부여에 용이하다.
오늘은 토큰중에서도 가장 많이 사용하는 jsonwebtoken
을 활용해서 토큰 API를 구현했다.
JWT의 종류
Access Token은 보호된 정보들(유저의 이메일, 사진 등)에 접근할 수 있는 권한부여에 사용한다.
클라이언트가 처음 인증을 받게 될 때(로그인 시), access, refresh token 두가지를 다 받지만, 실제로 권한을 얻는데 사용하는 토큰은 Access Token 이다.
1.Header
Header
는 이것이 어떤 종류의 토큰인지, 어떤 알고리즘으로 할지 적혀있다.
밑에 JSON 객체를 base64로 인코딩하면 JWT의 첫 번째 블럭이 완성 된다.
{
"alg": "HS256",
"typ": "JWT"
}
2.Payload
payload
에는 정보가 담겨 있다. 어떤 정보에 접근 가능한지에 대한 권한을 담을 수도 있고, 사용자의 유저이름 등 필요한 데이터는 이곳에 담아 암호화 시킨다. 물론 암호화(헤더에서 정의한)가 될 정보지만, 민감한 정보는 담지 않는 것이 좋다.
밑에 JSON 객체를 base64로 인코딩하면 JWT의 두 번째 블럭이 완성 된다.
{
"sub": "someInformation",
"name": "useonglee",
"iat": 151623391
}
3.Signature
base64로 인코딩 된 첫번째, 그리고 두번째 부분이 완성 되었다면, 원하는 비밀 키(암호화에 추가할 salt)를 사용하여 암호화를 한다.
HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);
JWT 사용 예시
JWT는 권한 부여에 굉장히 유용하다.
예시
새로 다운 받은 A
라는 앱이 Gmail과 연동되어 이메일을 읽어와야 한다고 한다면,
A
앱은 JWT
를 사용해 해당 유저의 Gmail 이메일을 읽거나 사용할 수 있다.
오늘 스프린트 과제는 서버에서 Token 기반 API를 작성하고, 클라이언트에서 요청하고 응답을 받고 다시 헤더에 토큰을 넣어서 보내는 것 까지 하는 것이였다.
POST/login
로그인 요청에 응답하는 라우트이다.
const jwt = require('jsonwebtoken');
const data = await Users.findOne({
where: { userId: req.body.userId, password: req.body.password },
});
// 생략
if (data) {
// payload를 만든다. 공식문서를 참고했다.
const access_payload = {
id: data.id,
userId: data.userId,
email: data.email,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (60 * 60)
}
const refresh_payload = {
id: data.id,
userId: data.userId,
email: data.email,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (60 * 60)
}
// 토큰을 생성한다. jwt.sign 메소드 사용
const accessToken = jwt.sign(access_payload, process.env.ACCESS_SECRET);
const refreshToken = jwt.sign(refresh_payload, process.env.REFRESH_SECRET);
// 성공 응답메세지를 보낸다.
res
.status(200)
.cookie('refreshToken', refreshToken, {
secure: true,
httpsOnly: true,
sameSite: 'none',
})
.send({ data: { accessToken: accessToken }, message: "ok" })
}
GET/accesstokenrequest
Access Token을 가지고 있는 클라이언트에서 보내는 유저정보 요청에 응답하는 라우트이다.
const jwt = require('jsonwebtoken');
const authorization = req.headers['authorization'];
// 생략..
if(authorization) {
const token = authorization.split(' ')[1];
// 암호화된 것을 해독한다.
const data = jwt.verify(token, process.env.ACCESS_SECRET);
const { id, userId, email, createdAt, updatedAt } = data;
const userInfo = await Users.findOne({
where: { userId: data.userId },
});
if (!userInfo) {
return res.status(400).send({ data: null, message: "access token has been tempered" });
}
else {
return res.status(200).send({ data: { userInfo: { id, userId, email, createdAt, updatedAt } }, message: "ok" })
}
}
GET/refreshtokenrequest
Access Token이 만료되어 Refresh Token으로 새로운 Access Token을 발급받고, 유저가 요청한 정보를 반환하는 라우트이다.
// cookie에 refreshToken이 담겨있는지 확인한다.
const token = req.cookies.refreshToken;
// 생략..
if (token) {
jwt.verify(token, process.env.REFRESH_SECRET, async (err, decoded) => {
// refresh token이 유효한지,
// 서버가 가지고 있는 비밀 키로 생성한 것이 맞는지 확인한다.
if (err) {
return res.status(400).send({ "data": null, "message": "invalid refresh token, please log in again" });
}
const userInfo = await Users.findOne({
where: { userId: decoded.userId },
});
// JWT를 해독하여 얻은 payload안의 값으로 DB에 유저를 조회한다.
if (userInfo) {
const { id, userId, email, createdAt, updatedAt } = decoded;
const accessToken = jwt.sign({ id, userId, email, createdAt, updatedAt }, process.env.ACCESS_SECRET)
return res.status(200).send({ data: {
accessToken: accessToken,
userInfo: { id, userId, email, createdAt, updatedAt } }, message: "ok" })
}
});
}
오늘 이유는 모르겠지만 하루종일 잠이 몰려오는 하루였다. 게다가 오늘 처음 배워본 token
의 개념과 한 번에 많은 개념들을 익히려다 보니 쉽지 않았다. API문서를 구현하는 과정에서도 한 문제를 가지고 두시간동안 테스트 케이스를 통과하지 못하였는데, 그 이유 중의 하나가 오타였다….. 오타때문에 시간을 많이 허비해서 이머시브 과정 처음으로 제 시간에 못끝냈다.. 물론 수업이 끝나자마자 클라이언트 부분도 다 작성해서 바로 통과가 되었지만, 정말 집중이 안되는 하루였던 것 같다.
그리고 테스트 케이스 통과가 안 된 또 다른 하나는 GET/accesstokenrequest API문서를 작성할 때, authorization.split(‘ ‘)[1] 이렇게 헤더의 한 부분을 짤라오는 것을 if문 안에서 했어야 했는데, if문 밖에서 하는 바람에 계속 응답이 보내지질 않았다. 정말 기초적인 실수를 했다. 구현 자체는 금방했는데 이런 실수들로 인해서 시간을 많이 빼았긴 것 같다.
이런 실수를 최대한 줄이도록 천천히 로직을 생각해보면서 작성해보는 습관도 길러야겠다. 그래도 오늘 하루가 잘 마무리가 되었다. 다시 처음 부터 풀어보면서 복습해야겠다.
👊 내일의 TIW(today I Will)
OAuth