- Published on
express에서 JWT사용하기
JWT 는 JSON Web Token의 약자로 클라이언트와 서버, 서비스와 서비스 사이 통신 시 권한 인가(Authorization)를 위해 사용하는 토큰이다.
URL-safe(URL로 이용할 수있는 문자 만 구성된)하기 때문에 HTTP 어디든(URL, Header, ...) 위치할 수 있다.
JWT 토큰 구성
JWT는 헤더 (Header), 페이로드 (Payload), 서명 (Sinature) 세 파트로 나누어지며, Json형태인 각 부분은 Base64로 인코딩된다.
각 파트는 점으로 구분하여 xxxxx.yyyyy.zzzzz 이런식으로 표현된다.
Header
Header는 토큰의 타입(typ)과 Signature를 해싱하기 위한 알고리즘(alg)으로 이루어진다.
- typ: 토큰의 타입을 지정 ex) JWT
- alg: 해시 알고리즘 방식을 지정한다. ex) SHA256, RSA
Payload
Payload에는 토큰에 담을 클레임(claim)정보를 포함한다. 클라이언트와 서버 간 주고 받기로 한 값들로 구성된 JSON형태로 이루어져 있다.
Signature
점(.)을 구분자로 해서 헤더와 페이로드를 합친 문자열을 서명한 값이다.
서명은 헤더의 alg에 정의된 알고리즘과 비밀키를 이용해 생성하고 Base64로 인코딩한다.
signature = base64UrlEncode(
Sign('ES256', '${PRIVATE_KEY}', base64UrlEncode(header) + '.' + base64UrlEncode(payload))
);
JWT의 장점과 단점
장점
-
JWT에는 필요한 모든 정보를 토큰에 포함하기 때문에 서버에서 인증시 DB 조회 비용을 줄일 수 있다.
-
세션정보를 서버에서 저장하고 있지 않아도 된다.
-
분산 마이크로 서비스 환경에서 중앙집중식 인증 서버에 의존하지 않아도 된다.
-
쿠키를 사용하지 않기 때문에 여러 도메인에서 API를 제공하더라도 문제가 발생하지 않는다. (Secret Key만 공유하면 된다.)
단점
-
토큰은 클라이언트에 저장되어 서버에서 제어가 불가능하다. (토큰 만료시간을 꼭 넣어 주어야 한다.)
-
필드가 많이 추가되면 토큰이 커질 수 있으며, 데이터 트래픽 크기에 영향을 미칠 수 있다.
-
Payload는 암호화가 아니라 Base64로 인코딩되었기 때문에, 중간에 탈취하여 데이터를 볼 수 있다. (Payload에 중요 데이터를 넣지 않아야 한다.)
JWT 구현
1. 서버 로그인 구현 (토큰 발급)
- db에서 user정보를 찾아 password가 일치하는지 확인한다. (bcrypt를 사용하여 password를 암호화하여 저장하고 해싱한다.)
- SecretKey를 사용하여 jwt 토큰을 생성한다. 토큰만료는 7일로 설정하였다.
- jwt 토큰을 클라이언트에 전달한다.
module.exports = {
authenticate: async (req, res, next) => {
if (!req.body || !req.body.id || !req.body.password) {
return res.send({ code: 'nok', error: 'wrong parameter' });
}
try {
const user = await userModel.findOne({ id: req.body.id }).exec();
if (user && bcrypt.compareSync(req.body.password, user.password)) {
const token = jwt.sign(
{
id: user.id,
admin: user.admin,
},
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '7d' }
);
return res.send({ code: 'ok', user: { id: user.id, admin: user.admin }, token: token });
}
return res.send({ code: 'nok', message: 'Invalid id or password' });
} catch (err) {
return next(err);
}
},
};
2. 토큰 인증 구현
- 클라이언트에서 API를 요청할 때 Authorization header에 다음과 같이 토큰을 보낸다.
{
"Authorization": "Bearer {생성된 토큰 값}"
}
- 서버는 인증이 필요한 경로에 접근할 때 JWT Signature를 체크하고 Payload로부터 사용자 정보를 확인한다.
app.use('/translates', validateUser, translates);
function validateUser(req, res, next) {
if (req.method === 'OPTIONS') {
return res.send({ code: 'ok' });
}
const authHeader = req.headers['authorization'];
const token = authHeader ? authHeader.split(' ')[1] : null;
if (!token) {
return res.status(401).json({ code: 'nok', message: 'Unauthorized' });
}
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, function (err, user) {
if (err) {
return res.status(403).json({ code: 'nok', message: 'Forbidden' });
}
req.user = user;
next();
});
}