매일 업데이트
2022-08-20 12:09 16 min

NodeJS에서 JWT를 사용하여 사용자를 인증하고 권한을 부여하는 방법

보안의 핵심: 인증과 권한 부여

컴퓨터 보안에서 인증과 권한 부여는 매우 중요한 개념입니다. 사용자는 자신의 신원을 확인하고 등록된 사용자임을 증명하기 위해 사용자 이름, 비밀번호와 같은 자격 증명을 사용합니다. 이후 추가적인 권한을 획득할 수 있게 됩니다.

온라인 서비스에 로그인할 때 Facebook이나 Google 계정을 사용하는 것 또한 이러한 인증 방식의 한 예입니다.

이 글에서는 JWT(JSON 웹 토큰) 인증을 사용하여 Node.js API를 구축하는 방법을 알아봅니다. 이 튜토리얼에서 사용될 주요 도구는 다음과 같습니다.

  • Express.js
  • MongoDB 데이터베이스
  • Mongoose
  • Dotenv
  • Bcrypt.js
  • Jsonwebtoken

인증과 권한 부여의 차이점

인증이란 무엇인가?

인증은 이메일, 비밀번호, 토큰 등의 자격 증명을 통해 사용자의 신원을 확인하는 프로세스입니다. 입력된 자격 증명은 로컬 시스템 또는 데이터베이스에 저장된 등록된 사용자의 정보와 비교됩니다. 자격 증명이 데이터베이스의 정보와 일치하면 인증이 완료되고 사용자는 리소스에 접근할 수 있게 됩니다.

권한 부여란 무엇인가?

권한 부여는 인증이 완료된 후에 이루어집니다. 권한 부여 프로세스는 항상 인증 프로세스를 선행합니다. 이는 사용자가 시스템 또는 웹사이트의 특정 리소스에 접근할 수 있도록 하는 과정입니다. 이 튜토리얼에서는 로그인한 사용자에게 사용자 데이터 접근 권한을 부여합니다. 반대로, 로그인하지 않은 사용자는 데이터에 접근할 수 없습니다.

소셜 미디어 플랫폼, 예를 들어 Facebook이나 Twitter에서 계정이 없는 경우 콘텐츠를 볼 수 없는 것이 권한 부여의 좋은 예시입니다. 또한, 구독 기반 콘텐츠는 웹사이트에 로그인(인증)을 하더라도 구독하지 않으면 콘텐츠를 이용(권한 부여)할 수 없습니다.

사전 요구 사항

이 가이드를 진행하기 위해서는 JavaScript와 MongoDB에 대한 기본 지식, 그리고 Node.js에 대한 충분한 이해가 필요합니다.

로컬 컴퓨터에 Node.js와 npm이 설치되어 있어야 합니다. 이를 확인하려면 명령 프롬프트 또는 터미널을 열고 node -vnpm -v 명령어를 입력하십시오. 결과로 각각 Node.js와 npm의 버전 정보가 출력되어야 합니다.

버전은 위에 보이는 것과 다를 수 있습니다. npm은 Node.js와 함께 자동으로 설치됩니다. 만약 설치되지 않았다면, Node.js 공식 웹사이트에서 다운로드할 수 있습니다.

코드 작성을 위해 IDE(통합 개발 환경)가 필요합니다. 이 튜토리얼에서는 VS Code 편집기를 사용합니다. 다른 IDE를 사용하셔도 무방합니다. VS Code가 설치되어 있지 않다면 VS Code 공식 웹사이트에서 다운로드할 수 있습니다. 사용하시는 운영체제에 맞는 버전을 선택하여 다운로드하십시오.

프로젝트 설정

로컬 컴퓨터 내 원하는 위치에 `nodeapi`라는 이름의 폴더를 생성하고, VS Code로 해당 폴더를 엽니다. VS Code 터미널에서 다음 명령어를 입력하여 Node 패키지 관리자를 초기화합니다.

npm init -y

반드시 `nodeapi` 디렉토리 안에 있는지 확인하십시오.

위 명령은 프로젝트에서 사용될 모든 종속성 정보를 포함하는 `package.json` 파일을 생성합니다.

이제 위에서 언급한 패키지들을 다운로드하기 위해 터미널에 다음 명령어를 입력합니다.

npm install express dotenv jsonwebtoken mongoose bcryptjs

이제 아래와 같은 파일과 폴더 구조를 확인할 수 있습니다.

서버 생성 및 데이터베이스 연결

이제 `index.js` 파일과 `config`라는 폴더를 만듭니다. `config` 폴더 안에 데이터베이스 연결을 위한 `conn.js` 파일과 환경 변수 설정을 위한 `config.env` 파일을 만듭니다. 아래 코드를 각 파일에 작성합니다.

index.js

const express = require('express');
const dotenv = require('dotenv');

// dotenv 설정, 다른 라이브러리나 파일보다 먼저 설정해야 함
dotenv.config({path:'./config/config.env'}); 

// Express 앱 생성
const app = express();

// JSON 데이터 요청 처리를 위한 미들웨어
app.use(express.json());

// 서버 실행
app.listen(process.env.PORT,()=>{
    console.log(`서버가 ${process.env.PORT} 포트에서 시작되었습니다.`);
});

dotenv를 사용하는 경우, 환경 변수를 사용하는 다른 파일보다 먼저 `index.js` 파일에서 구성해야 합니다.

conn.js

const mongoose = require('mongoose');

mongoose.connect(process.env.URI, { useNewUrlParser: true, useUnifiedTopology: true })
    .then((data) => {
        console.log(`데이터베이스가 ${data.connection.host} 호스트에 연결되었습니다.`);
    });

config.env

URI = 'mongodb+srv://ghulamrabbani883:[email protected]/?retryWrites=true&w=majority'
PORT = 5000

위에서는 MongoDB Atlas URI를 사용하고 있지만, localhost를 사용할 수도 있습니다.

모델 및 라우트 생성

모델은 MongoDB 데이터베이스의 데이터 구조를 정의하며, JSON 문서 형태로 저장됩니다. 모델 생성에는 Mongoose 스키마가 사용됩니다.

라우팅은 클라이언트 요청에 대한 애플리케이션의 응답 방식을 정의합니다. Express 라우터 기능을 사용하여 라우트를 생성합니다.

라우팅 메소드는 일반적으로 두 개의 인자를 사용합니다. 첫 번째는 경로이고, 두 번째는 해당 경로에 대한 클라이언트 요청에 따라 실행될 콜백 함수입니다. 또한, 인증 과정처럼 필요에 따라 미들웨어 함수를 세 번째 인자로 사용할 수 있습니다. 인증 API를 구축하는 과정에서 사용자 인증과 인가를 위해 미들웨어 함수를 사용할 것입니다.

이제 `routes`와 `models` 두 개의 폴더를 생성합니다. `routes` 폴더 안에 `userRoute.js` 파일을, `models` 폴더 안에 `userModel.js` 파일을 생성합니다. 파일을 생성한 후에는 각 파일에 다음 코드를 작성합니다.

userModel.js

const mongoose = require('mongoose');

// Mongoose 스키마 생성
const userSchema = new mongoose.Schema({
    name: {
        type:String,
        required:true,
        minLength:[4,'이름은 최소 4자 이상이어야 합니다.']
    },
    email:{
        type:String,
        required:true,
        unique:true,
    },
    password:{
        type:String,
        required:true,
        minLength:[8,'비밀번호는 최소 8자 이상이어야 합니다.']
    },
    token:{
        type:String
    }
});

// 모델 생성
const userModel = mongoose.model('user',userSchema);
module.exports = userModel;

userRoute.js

const express = require('express');
// Express 라우터 생성
const route = express.Router();
// userModel 가져오기
const userModel = require('../models/userModel');

// 회원가입 라우트 생성
route.post('/register',(req,res)=>{

});

// 로그인 라우트 생성
route.post('/login',(req,res)=>{

});

// 사용자 데이터 가져오기 라우트 생성
route.get('/user',(req,res)=>{

});

라우트 기능 구현 및 JWT 토큰 생성

JWT 란 무엇인가?

JSON 웹 토큰(JWT)은 토큰을 생성하고 검증하는 JavaScript 라이브러리입니다. 이는 클라이언트와 서버 간에 정보를 안전하게 공유하기 위한 개방형 표준입니다. JWT의 두 가지 주요 기능을 사용합니다. 첫 번째는 새 토큰을 생성하는 기능이고, 두 번째는 토큰의 유효성을 확인하는 기능입니다.

bcryptjs 란 무엇인가?

Bcrypt.js는 Niels Provos와 David Mazières가 만든 해싱 함수입니다. 이 함수는 비밀번호를 안전하게 해시하는 데 사용됩니다. 이 프로젝트에서는 두 가지 주요 기능을 사용할 것입니다. 첫 번째는 해시된 값을 생성하는 기능이고, 두 번째는 입력된 비밀번호와 저장된 해시값을 비교하는 기능입니다.

라우트 기능 구현

라우트의 콜백 함수는 요청(request), 응답(response), 그리고 `next` 함수의 세 가지 인수를 받습니다. `next` 인수는 선택 사항입니다. 필요할 때만 전달합니다. 인수는 반드시 요청, 응답, `next` 순서로 전달되어야 합니다. 이제 다음 코드를 사용하여 `userRoute.js`, `config.env`, `index.js` 파일을 수정합니다.

userRoute.js

// 필요한 파일 및 라이브러리 가져오기
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

// Express 라우터 생성
const route = express.Router();
// userModel 가져오기
const userModel = require('../models/userModel');

// 회원가입 라우트
route.post("/register", async (req, res) => {
    try {
        const { name, email, password } = req.body;

        // 입력 데이터 유효성 검사
        if (!name || !email || !password) {
            return res.json({ message: '모든 정보를 입력해주세요.' });
        }

        // 기존 사용자 확인
        const userExist = await userModel.findOne({ email: req.body.email });
        if (userExist) {
            return res.json({ message: '이미 해당 이메일로 가입된 사용자가 있습니다.' });
        }
        // 비밀번호 해싱
        const salt = await bcrypt.genSalt(10);
        const hashPassword = await bcrypt.hash(req.body.password, salt);
        req.body.password = hashPassword;

        const user = new userModel(req.body);
        await user.save();
        const token = await jwt.sign({ id: user._id }, process.env.SECRET_KEY, {
            expiresIn: process.env.JWT_EXPIRE,
        });
        return res.cookie({ 'token': token }).json({ success: true, message: '회원가입이 성공적으로 완료되었습니다.', data: user });

    } catch (error) {
        return res.json({ error: error });
    }
});

// 로그인 라우트
route.post('/login', async (req, res) => {
    try {
        const { email, password } = req.body;

        // 입력 데이터 유효성 검사
        if (!email || !password) {
            return res.json({ message: '모든 정보를 입력해주세요.' });
        }
        // 사용자 존재 확인
        const userExist = await userModel.findOne({ email: req.body.email });
        if (!userExist) {
            return res.json({ message: '잘못된 정보입니다.' });
        }
        // 비밀번호 일치 여부 확인
        const isPasswordMatched = await bcrypt.compare(password, userExist.password);
        if (!isPasswordMatched) {
            return res.json({ message: '잘못된 비밀번호입니다.' });
        }
        const token = await jwt.sign({ id: userExist._id }, process.env.SECRET_KEY, {
            expiresIn: process.env.JWT_EXPIRE,
        });
        return res.cookie({ "token": token }).json({ success: true, message: '로그인에 성공했습니다.' });
    } catch (error) {
        return res.json({ error: error });
    }
});

// 사용자 데이터 가져오기 라우트
route.get('/user', async (req, res) => {
    try {
        const user = await userModel.find();
        if (!user) {
            return res.json({ message: '사용자를 찾을 수 없습니다.' });
        }
        return res.json({ user: user });
    } catch (error) {
        return res.json({ error: error });
    }
});

module.exports = route;

async 함수를 사용할 때는 try-catch 블록을 사용해야 합니다. 그렇지 않으면 처리되지 않은 약속 거부(unhandled promise rejection) 오류가 발생할 수 있습니다.

config.env

URI = 'mongodb+srv://ghulamrabbani883:[email protected]/?retryWrites=true&w=majority'
PORT = 5000
SECRET_KEY = KGGK>HKHVHJVKBKJKJBKBKHKBMKHB
JWT_EXPIRE = 2d

index.js

const express = require('express');
const dotenv = require('dotenv');

// dotenv 설정, 다른 라이브러리나 파일보다 먼저 설정해야 함
dotenv.config({ path: './config/config.env' });
require('./config/conn');

// Express 앱 생성
const app = express();
const route = require('./routes/userRoute');

// JSON 데이터 요청 처리를 위한 미들웨어
app.use(express.json());

// 라우트 사용
app.use('/api', route);

// 서버 실행
app.listen(process.env.PORT, () => {
    console.log(`서버가 ${process.env.PORT} 포트에서 시작되었습니다.`);
});

사용자 인증을 위한 미들웨어 생성

미들웨어란 무엇인가?

미들웨어는 요청-응답 주기에서 요청(request), 응답(response) 객체, 그리고 `next` 함수에 접근할 수 있는 함수입니다. 함수 실행이 완료되면 `next` 함수가 호출됩니다. 앞서 언급했듯이, 다른 콜백 함수나 미들웨어 함수를 실행해야 할 때 `next()`를 사용합니다.

이제 `middleware` 폴더를 만들고 그 안에 `auth.js` 파일을 생성한 다음, 다음 코드를 작성합니다.

auth.js

const userModel = require('../models/userModel');
const jwt = require('jsonwebtoken');

const isAuthenticated = async (req, res, next) => {
    try {
        const { token } = req.cookies;
        if (!token) {
            return next('로그인 후 이용해주세요.');
        }
        const verify = await jwt.verify(token, process.env.SECRET_KEY);
        req.user = await userModel.findById(verify.id);
        next();
    } catch (error) {
        return next(error);
    }
};

module.exports = isAuthenticated;

이제 쿠키에 저장된 토큰에 접근하기 위해 `cookie-parser` 라이브러리를 설치하고 앱에 구성합니다. Node.js 앱에 `cookie-parser`가 구성되지 않으면 요청 객체의 헤더에서 쿠키에 접근할 수 없습니다. 터미널에 다음 명령어를 입력하여 `cookie-parser`를 다운로드합니다.

npm i cookie-parser

이제 `cookie-parser`가 설치되었습니다. `index.js` 파일을 수정하여 앱을 구성하고, `/user/` 라우트에 미들웨어를 추가합니다.

index.js 파일

const cookieParser = require('cookie-parser');
const express = require('express');
const dotenv = require('dotenv');

// dotenv 설정, 다른 라이브러리나 파일보다 먼저 설정해야 함
dotenv.config({ path: './config/config.env' });
require('./config/conn');

// Express 앱 생성
const app = express();
const route = require('./routes/userRoute');

// JSON 데이터 요청 처리를 위한 미들웨어
app.use(express.json());

// cookie-parser 설정
app.use(cookieParser());

// 라우트 사용
app.use('/api', route);

// 서버 실행
app.listen(process.env.PORT, () => {
    console.log(`서버가 ${process.env.PORT} 포트에서 시작되었습니다.`);
});

userRoute.js

// 필요한 파일 및 라이브러리 가져오기
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const isAuthenticated = require('../middleware/auth');

// Express 라우터 생성
const route = express.Router();
// userModel 가져오기
const userModel = require('../models/userModel');

// 사용자 데이터 가져오기 라우트
route.get('/user', isAuthenticated, async (req, res) => {
    try {
        const user = await userModel.find();
        if (!user) {
            return res.json({ message: '사용자를 찾을 수 없습니다.' });
        }
        return res.json({ user: user });
    } catch (error) {
        return res.json({ error: error });
    }
});

module.exports = route;

`/user` 경로는 사용자가 로그인한 경우에만 접근할 수 있습니다.

POSTMAN에서 API 확인

API를 확인하기 전에 `package.json` 파일을 수정해야 합니다. 다음 코드 줄을 추가합니다.

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js",
    "dev": "nodemon index.js"
  },

npm start 명령어로 서버를 시작할 수 있지만, 이는 한 번만 실행됩니다. 파일을 수정하는 동안 서버를 계속 실행하려면 `nodemon`이 필요합니다. 터미널에 다음 명령어를 입력하여 다운로드합니다.

npm install -g nodemon

`-g` 플래그는 `nodemon`을 로컬 시스템에 전역적으로 다운로드합니다. 따라서 새 프로젝트를 만들 때마다 다시 다운로드할 필요가 없습니다.

서버를 실행하려면 터미널에 `npm run dev`를 입력합니다. 다음과 같은 결과를 얻을 수 있습니다.

이제 코드가 완성되었고 서버가 정상적으로 실행되고 있다면, POSTMAN으로 이동하여 API가 제대로 작동하는지 확인하십시오.

POSTMAN 이란 무엇인가?

POSTMAN은 API를 설계, 구축, 개발 및 테스트하는 데 사용되는 소프트웨어 도구입니다.

컴퓨터에 POSTMAN을 다운로드하지 않았다면 POSTMAN 공식 웹사이트에서 다운로드할 수 있습니다.

이제 POSTMAN을 열고 `nodeAPItest`라는 컬렉션을 생성한 다음, 그 안에 회원가입, 로그인, 사용자 정보 조회의 세 가지 요청을 생성합니다. 다음과 같은 파일 구조를 확인할 수 있어야 합니다.

`localhost:5000/api/register`로 JSON 데이터를 전송하면 다음과 같은 결과를 확인할 수 있습니다.

회원가입 과정에서 토큰이 생성되어 쿠키에 저장되므로, `localhost:5000/api/user` 경로로 요청을 보내면 사용자 정보를 얻을 수 있습니다. 나머지 요청은 POSTMAN에서 확인해 보십시오.

전체 코드는 다음 GitHub 저장소에서 확인할 수 있습니다: GitHub 계정.

결론

이 튜토리얼에서는 JWT 토큰을 사용하여 Node.js API에 인증을 구현하는 방법을 배웠습니다. 또한, 사용자가 사용자 데이터에 접근할 수 있도록 권한을 부여하는 방법도 알아보았습니다.

즐거운 코딩 하세요!

저자
Korea

기술 트렌드와 실용적인 팁을 전하는 लेखक입니다.