티스토리 뷰

Node.js 에서 JWT를 사용하여 토큰 기반 인증 시스템을 구축할 수 있다. 

 

세션 기반이나 쿠키와 같은 개념은 생략하고, 어떻게 JWT를 사용하여 토큰을 생성하고 이를 검증하는 지만 확인해보려고 한다.

 

예제는 직전에 DB연동을 하면서 진행했던 코드를 그대로 가지고 진행한다.

 

바로 한번 확인해보자.


우선, User라는 모델을 하나 만들고 DB에도 users 테이블을 하나 생성하자.

 

◎models/User.js

module.exports = (sequelize, DataTypes) => (
    sequelize.define(
        "User",
        {
            id: {
                type: DataTypes.INTEGER,
                autoIncrement: true,
                primaryKey: true
            },
            name: {
                type: DataTypes.STRING(100),
                allowNull: false
            },
            password: {
                type: DataTypes.STRING
            }
        },
        {
            timestamps: false
        }
    )
);

이 User 모델을 가지고 간단한 회원가입, 로그인 로직을 구현하려고 하기 때문에 bcrypt 라이브러리를 받아 비밀번호를 해시키로 암호화하는 로직을 만들어야 한다.

npm install bcrypt;

User모델을 제어할 컨트롤러도 만들어주고, 각 요청과 라우터를 등록해주자.

 

◎api/users/users.ctrl.js

const db = require("../../../models");
const bcrypt = require("bcrypt");

const User = db.user;

const env = process.env.NODE_ENV || "development";
const config = require("../../../config/config.json")[env];

const setPassword = async password => {
    return await bcrypt.hash(password, 10);
};

const checkPassword = async (name, password) => {
    const userPassword = await User.findOne({where: {name: name}}).catch(e => console.log(e));
    return await bcrypt.compare(password, userPassword.password);
}

exports.register = async ctx => {
    
};

exports.login = async ctx => {
    
};

exports.check = async ctx => {
    
};

exports.logout = async ctx => {
  
};

 

◎api/users/index.js

const Router = require('koa-router');
const usersCtrl = require('./users.ctrl');
const users = new Router();

users.post('/register', usersCtrl.register);
users.post('/login', usersCtrl.login);
users.post('/check', usersCtrl.check);
users.post('/logout', usersCtrl.logout);

module.exports = users;

 

◎api/index.js

const Router = require('koa-router');
const api = new Router();

const posts = require('./posts');
const users = require('./users');

api.use('/posts', posts.routes());
api.use('/users', users.routes());

api.get('/test', ctx => {
    ctx.body = 'test 성공';
});

// 라우터를 내보낸다.
module.exports = api;

이제, 회원가입과 로그인 시 JSON 토큰을 생성해줄 jwtGenerator를 아래와 같이 작성한다.

 

◎lib/jwtGenerator.js

const jwt = require("jsonwebtoken");

module.exports = (user, secret) => {
    return  jwt.sign(
        {
            id: user.id,
            name: user.name
        },
        secret,
        {
            expiresIn: '3d'
        },
    );
}

jwtGenerator는 현재 회원가입/로그인한 유저 데이터와 비밀키를 받아서 3일 짜리 토큰을 생성하는 역할을 한다.

 

이제 이 토큰을 생성하는 로직을 가지고 회원가입, 로그인 함수를 아래와 같이 구현할 수 있다.

 

◎api/users/users.ctrl.js

const db = require("../../../models");
const bcrypt = require("bcrypt");
const jwtGenerator = require('../../lib/jwtGenerator');
const User = db.user;

const env = process.env.NODE_ENV || "development";
const config = require("../../../config/config.json")[env];

const setPassword = async password => {
    return await bcrypt.hash(password, 10);
};

const checkPassword = async (name, password) => {
    const userPassword = await User.findOne({where: {name: name}}).catch(e => console.log(e));
    return await bcrypt.compare(password, userPassword.password);
}

exports.register = async ctx => {
    const {name, password} = ctx.request.body;

    const exists = await User.findOne({where: {name: name}}).catch(e => console.log(e));

    if(exists) {
        ctx.status = 409;
        return;
    }

    const userPassword = await setPassword(password);

    const user = {
        name: name,
        password: userPassword
    }

    await User.create(user).catch(e=>console.log(e));

    delete user.password;

    ctx.body = user;

    const token = jwtGenerator(user, config.jwt_secret);

    ctx.cookies.set('access_token', token, {
        maxAge: 1000 * 60 * 60 * 24 * 7,
        httpOnly: true,
    });
};

exports.login = async ctx => {
    const {name, password} = ctx.request.body;

    if(!name || !password) {
        ctx.status = 401;
        return;
    }

    const user = await User.findOne({where: {name: name}}).catch(e => console.log(e));

    if(!user) {
        ctx.status = 401;
        return;
    }

    const valid = await checkPassword(user.name, password);

    if(!valid) {
        ctx.status = 401;
        return;
    }
    ctx.body = user;

    const token =  jwtGenerator(user, config.jwt_secret);

    ctx.cookies.set('access_token', token, {
        maxAge: 1000 * 60 * 60 * 24 * 3,
        httpOnly: true,
    });
};

exports.check = async ctx => {
    
};

exports.logout = async ctx => {
    
};

모두 구현이 됐으면, 요청을 받을 때 가지고 있는 토큰을 복호화하여 현재 접속하고 있는 유저가 누구인지 알 수 있고, 토큰의 유효 기간이 만료됐을 때 재발급하는 기능을 할 미들웨어를 하나 만들어준다.

 

◎lib/jwtMiddleware.js

const jwt = require('jsonwebtoken');
const env = process.env.NODE_ENV || "development";
const config = require("../../config/config.json")[env];

const db = require("../../models");
const jwtGenerator = require("./jwtGenerator");

const User = db.user;

const jwtMiddleware = async (ctx, next) => {
    const token = ctx.cookies.get('access_token');

    if(!token) return next();

    try{
        const decoded = jwt.verify(token, config.jwt_secret);

        ctx.state.user = {
            id: decoded.id,
            name: decoded.name
        };

        const now = Math.floor(Date.now() / 1000);
        if(decoded.exp - now < 60*60*24*3.5) {
            const user = await User.findByPk(decoded.id);
            const token =  jwtGenerator(user, config.jwt_secret);

            ctx.cookies.set('access_token', token, {
                maxAge: 1000 * 60 * 60 * 24 * 7,
                httpOnly: true,
            })
        }

        return next();
    } catch (e) {
        return next();
    }
};

module.exports = jwtMiddleware;

이제 이 미들웨어를 시작 파일에 적용해주면, 요청을 처리할 때마다 ctx.state.user에 토큰에 있는 정보를 입력하고 토큰의유효성을 검사한다.

 

◎src/index.js

const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');
const api = require('./api');
const jwtMiddleware = require('./lib/jwtMiddleware');

const app = new Koa();
const router = new Router();

router.use('/api', api.routes());

require("../models");

// 라우터 적용 적엔 bodyParser 적용
app.use(bodyParser());
app.use(jwtMiddleware);

// app 인스턴스에 라우터 적용
app.use(router.routes()).use(router.allowedMethods());

app.listen(4000, () => {
    console.log('Listening to port 4000');
});

이제 로그인 상태인지 아닌지 체크하는 로직과 로그아웃을 아래와 같이 구현한다.

 

◎api/users/users.ctrl.js

const db = require("../../../models");
const bcrypt = require("bcrypt");
const jwtGenerator = require('../../lib/jwtGenerator');
const User = db.user;

const env = process.env.NODE_ENV || "development";
const config = require("../../../config/config.json")[env];

const setPassword = async password => {
    return await bcrypt.hash(password, 10);
};

const checkPassword = async (name, password) => {
    const userPassword = await User.findOne({where: {name: name}}).catch(e => console.log(e));
    return await bcrypt.compare(password, userPassword.password);
}

exports.register = async ctx => {
    const {name, password} = ctx.request.body;

    const exists = await User.findOne({where: {name: name}}).catch(e => console.log(e));

    if(exists) {
        ctx.status = 409;
        return;
    }

    const userPassword = await setPassword(password);

    const user = {
        name: name,
        password: userPassword
    }

    await User.create(user).catch(e=>console.log(e));

    delete user.password;

    ctx.body = user;

    const token = jwtGenerator(user, config.jwt_secret);

    ctx.cookies.set('access_token', token, {
        maxAge: 1000 * 60 * 60 * 24 * 7,
        httpOnly: true,
    });
};

exports.login = async ctx => {
    const {name, password} = ctx.request.body;

    if(!name || !password) {
        ctx.status = 401;
        return;
    }

    const user = await User.findOne({where: {name: name}}).catch(e => console.log(e));

    if(!user) {
        ctx.status = 401;
        return;
    }

    const valid = await checkPassword(user.name, password);

    if(!valid) {
        ctx.status = 401;
        return;
    }
    ctx.body = user;

    const token =  jwtGenerator(user, config.jwt_secret);

    ctx.cookies.set('access_token', token, {
        maxAge: 1000 * 60 * 60 * 24 * 3,
        httpOnly: true,
    });
};

exports.check = async ctx => {
    const {user} = ctx.state;
    if(!user) {
        ctx.status = 401;
        return;
    }

    ctx.body = user;
};

exports.logout = async ctx => {
    ctx.cookies.set('access_token');
    ctx.status = 204;
};

마지막으로 직전 포스팅에서 구현했던 간단한 게시판을 로그인 상태(토큰이 존재하는 상태)일 때만 작성, 수정, 삭제가 가능하도록 로그인을 체크하는 함수를 하나 생성하고, 아래와 같이 요청을 받을 때 해당 함수를 인자로 넣어 검사하게끔 구현할 수 있다.

 

◎lib/checkLoggedIn.js

const checkLoggedIn = (ctx, next) => {
    if(!ctx.state.user) {
        ctx.status = 401;
        return;
    }
    return next();
};

module.exports = checkLoggedIn;

 

◎api/posts/index.js

const Router = require('koa-router');
const postsCtrl = require('./posts.ctrl');
const checkLoggedIn = require("../../lib/checkLoggedIn");
const posts = new Router();

posts.get('/', postsCtrl.list);
posts.post('/', checkLoggedIn, postsCtrl.write);
posts.get('/:id', postsCtrl.read);
posts.delete('/:id', checkLoggedIn, postsCtrl.remove);
posts.patch('/:id', checkLoggedIn, postsCtrl.update);

module.exports = posts

이렇게만 해주면, 간단하게 로그인 기능을 구현하고 토큰을 활용한 인증 시스템까지 구현이 된다!

 

 

 

끝!

'WEB > Node' 카테고리의 다른 글

[Node] DB 연동: Sequelize  (0) 2023.04.11
[Node] Backend server: Koa  (0) 2023.04.10
Comments