JWT 토큰 인증
API 서비스를 제공하는 입장에서 다른 클라이언트가 NodeBird의 데이터를 가져갈 수 있게 해야 하는 만큼 별도의 인증과정이 필요하다.
JWT는 JSON Web Token의 약어로, JSON 형식의 데이터를 저장하는 토큰이다. 구성요소는 다음과 같다.
- 헤더(HEADER): 토큰 종류와 해시 알고리즘 정보가 들어 있다.
- 페이로드(PAYLOAD): 토큰의 내용물이 인코딩 된 부분이다.
- 시그니처(SIGNATURE): 일련의 문자열이며, 시그니처를 통해 토큰 변조 여부를 확인한다.
페이로드 부분은 노출된다. 이는 토큰의 내용을 모두 감춘다면 매 요청마다 권한을 체크해야 한다.
따라서 비밀키를 숨긴다. 비밀키는 시그니처와 다른 의미이다.
JWT 토큰의 단점은 용량이 크다는 것이다. 내용물이 들어 있으므로 랜덤한 토큰을 사용할 때와 비교해서 용량이 클 수밖에 없다. 매 요청 시 토큰이 오고 가서 데이터양이 증가한다.
그럼 이제 JWT 토큰 인증을 구현해보자.
npm i jsonwebtoken
COOKIE_SECRET=nodebirdsecret
KAKAO_ID=fac87a99de7a475d7724b32f10bdc6c7
JWT_SECRET=jwtSecret
.env
.env파일을 위와 같이 수정하자.
그리고 미들웨어를 만들어 보자.
const jwt = require('jsonwebtoken')
exports.isLoggedIn = (req, res, next) => {
if(req.isAuthenticated()){
next();
}else{
res.status(403).send('로그인 필요');
}
};
exports.isNotLoggedIn = (req, res, next) => {
if(!req.isAuthenticated()){
next();
}else{
const message = encodeURIComponent('로그인한 상태입니다.');
res.redirect(`/?error=${message}`);
}
};
exports.verifyToken = (req, res, next) => {
try{
req.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET);
return next();
}catch(err){
if(err.name === 'TokenExpiredError'){
return res.status(419).json({
code: 419,
message: '토큰이 만료되었습니다.',
});
}
return res.status(401).json({
code: 401,
message: '유효하지 않은 토큰이다.'
});
}
};
routes/middlewares.js
요청 헤더에 저장된 토큰을 사용한다. 사용자가 쿠키처럼 헤더에 토큰을 넣어 보낼 것이다. jwt.verify 메서드로 토큰을 검증할 수 있다. 메서드의 첫 번째 인수로는 토큰을, 두 번째 인수로는 토큰의 비밀 키를 넣는다.
토큰의 비밀 키가 일치하지 않는다면 인증을 받을 수 없고 에러가 발생한다.
또한, 올바른 토큰이더라도 유효 기간이 지난 경우라면 역시 catch문으로 이동한다.
이제 토큰의 내용물을 사용하는 미들웨어를 만들어 보자.
const express = require('express');
const jwt = require('jsonwebtoken');
const { verifyToken } = require('./middlewares');
const { Domian, User } = require('../models');
const rotuer = express.Router();
rotuer.post('/token', async(req, res) => {
const { clientSecret } = req.body;
try{
const domain = await Domian.findOne({
where: {clientSecret},
include: {
model: User,
attribute: ['nick', 'id'],
},
});
if(!domain){
return res.status(401).json({
code: 401,
message: '등록되지 않은 도메인입니다. 먼저 도메인을 등록하세요',
});
}
const token = jwt.sign({
id: domain.User.id,
nick: domain.User.nick,
}, process.env.JWR_SECRET, {
expiresIn: '1m',
issuer: 'nodebird',
});
return res.json({
code: 200,
message: '토큰이 발급되었습니다.',
token,
});
}catch(err){
return res.status(500).json({
code: 500,
message: '서버 에러',
});
}
});
rotuer.get('/test', verifyToken, (req, res) => {
res.json(req.decoded);
});
module.exports = rotuer;
routes/v1.js
토큰을 발급하는 라우터(POST /v1/token)와 사용자가 토큰을 테스트해볼 수 있는 라우터(GET /v1/test)를 만들었다.
다른 서비스에서 기존 API를 쓰고 있음을 염두에 두고 수정을 하기 위하여 v1(version1)이라고 네이밍했다.
jwt.sign의 첫 번째 인수는 토큰의 내용, 두 번째 인수는 토큰의 비밀 키이다. 세 번째 인수는 토큰의 설정이다.
이제 라우터를 서버에 연결해보자.
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const passport = require('passport');
const morgan = require('morgan');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
const v1 = require('./routes/v1');
dotenv.config();
const authRouter = require('./routes/auth');
const indexRouter = require('./routes');
const {sequelize} = require('./models');
const passportConfig = require('./passport');
const app = express();
passportConfig();
app.set('port', process.env.PORT || 8002);
app.set('view engine', 'html');
nunjucks.configure('views',{
express: app,
watch: true,
});
sequelize.sync({ force: false})
.then(() => {
console.log('데이터베이스 연결 성공');
})
.catch((err) => {
console.error(err);
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie:{
httpOnly: true,
secure: false,
},
}));
app.use(passport.initialize());
app.use(passport.session());
app.use('/v1', v1);
app.use('/auth', authRouter);
app.use('/', indexRouter);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다. `);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중...');
});
app.js