역할 기반 액세스 제어(RBAC)는 지정된 리소스에 대한 액세스를 정의된 역할을 가진 개인으로만 제한할 수 있는 고급 보안 조치입니다. 이 인증 방법은 무단 액세스에 대한 보호 기능을 강화합니다.

이 인증 방식을 사용하면 시스템 관리자가 각 사용자의 지정된 역할에 따라 권한을 규제할 수 있습니다. 이러한 수준의 구체적인 제어는 추가적인 보호 계층을 제공하여 애플리케이션이 승인되지 않은 모든 진입을 차단할 수 있도록 합니다.

Passport.js 및 JWT를 사용하여 역할 기반 액세스 제어 메커니즘 구현

사용자 역할 및 권한에 따라 애플리케이션에 대한 액세스를 규제하기 위해 일반적으로 사용되는 접근 방식인 역할 기반 액세스 제어(RBAC)는 여러 가지 기술을 통해 구현할 수 있습니다.

두 가지 널리 사용되는 방법은 AccessControl과 같은 특수한 RBAC 라이브러리를 활용하거나 기존 인증 라이브러리를 통합하여 메커니즘을 설정하는 것입니다.

JSON 웹 토큰(JWT)을 활용하면 인증 자격 증명을 안전하게 전송할 수 있으며, Passport.js는 다목적 인증 미들웨어를 제공하여 인증 프로세스를 용이하게 합니다.

이 방법을 활용하면 사용자에게 책임을 지정하고 인증 중에 이러한 지정 사항을 JWT 내에 포함할 수 있습니다. 이후 후속 요청에서 사용자의 신원 및 역할을 확인하는 데 JWT를 사용하여 역할 기반 권한 부여 및 액세스 제어를 용이하게 할 수 있습니다.

이 두 가지 구현 방법은 각각의 이점을 가지고 있지만, 주어진 프로젝트의 특정 요구에 따라 RBAC를 실행할 수 있습니다.

이 프로젝트의 소스 코드는 GitHub 리포지토리에서 사용할 수 있으며 관심 있는 당사자가 다운로드할 수 있습니다.

Express.js 프로젝트 설정

필요한 인프라를 설정하여 로컬 Express.js 개발 환경을 구축한 다음 npm 또는 yarn을 사용하여 필요한 종속성 설치를 진행합니다.

 npm install cors dotenv mongoose cookie-parser jsonwebtoken mongodb \
  passport passport-local

네트워크 내에서 인스턴스화하거나 클라우드 기반 MongoDB Atlas 서비스를 활용하여 MongoDB 데이터베이스를 구축합니다. 데이터베이스 연결 문자열을 가져와 프로젝트의 루트 디렉터리에 있는 환경 변수 파일에 포함하세요.

 CONNECTION_URI="connection URI" 

데이터베이스 연결 구성

 const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.CONNECTION_URI);
    console.log("Connected to MongoDB!");
  } catch (error) {
    console.error("Error connecting to MongoDB:", error);
  }
};

module.exports = connectDB;

데이터 모델 정의

루트 디렉터리에 `user.model.js`라는 새 파일을 생성하고 다음 코드를 포함시켜 몽구스를 활용한 사용자 정보 데이터 스키마를 설정하세요.

 const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  username: String,
  password: String,
  role: String
});

module.exports = mongoose.model('User', userSchema);

API 엔드포인트용 컨트롤러 만들기

루트 디렉터리에 “controllers/user.controller.js”라는 제목의 새 파일을 만든 다음 다음 코드를 추가합니다.

이 글도 확인해 보세요:  JavaScript를 사용하여 이미지에 X 및 Y 좌표를 오버레이하는 방법

나중에 사용하려면 일부 라이브러리를 가져와야 하는 것 같습니다.

 const User = require('../models/user.model');
const passport = require('passport');
const { generateToken } = require('../middleware/auth');
require('../middleware/passport')(passport);

시스템에 적절한 알고리즘과 프로토콜을 구현하여 사용자 등록 및 로그인 프로세스를 관리할 수 있는 논리적 프레임워크가 마련되어 있는지 확인합니다.

 exports.registerUser = async (req, res) => {
  const { username, password, role } = req.body;

  try {
    await User.create({ username, password, role });
    res.status(201).json({ message: 'User registered successfully' });
  } catch (error) {
    console.log(error);
    res.status(500).json({ message: 'An error occurred!' });
  }
};

exports.loginUser = (req, res, next) => {
  passport.authenticate('local', { session: false }, (err, user, info) => {
    if (err) {
      console.log(err);

      return res.status(500).json({
        message: 'An error occurred while logging in'
      });
    }

    if (!user) {
      return res.status(401).json({
        message: 'Invalid login credentials'
      });
    }

    req.login(user, { session: false }, (err) => {
      if (err) {
        console.log(err);

        return res.status(500).json({
          message: 'An error occurred while logging in'
        });
      }

      const { _id, username, role } = user;
      const payload = { userId: _id, username, role };
      const token = generateToken(payload);
      res.cookie('token', token, { httpOnly: true });
      return res.status(200).json({ message: 'Login successful' });
    });
  })(req, res, next);
};

registerUser 함수는 시스템 내에서 새 사용자 계정을 생성하기 위해 들어오는 요청을 처리하는 역할을 담당합니다. 이는 선택한 사용자 이름, 비밀번호, 할당된 역할 등 요청 본문에서 관련 데이터를 수집하여 이루어집니다. 이 기능은 이후 이 정보를 데이터베이스에 추가하고 등록이 성공했음을 알리는 확인 메시지 또는 해당 프로세스 중에 문제가 발생할 경우 오류 알림을 다시 보냅니다.

loginUser 함수는 Passport.js의 로컬 인증 전략 구현을 통해 사용자에게 로그인 수단을 제공하는 역할을 합니다. 이 함수는 사용자의 자격 증명을 검증하고 로그인 성공 시 토큰을 발급하며, 이 토큰은 향후 인증 요청에 사용할 수 있도록 쿠키로 저장됩니다. 로그인 과정에서 오류가 발생하면 오류 메시지가 대신 반환됩니다.

이 글도 확인해 보세요:  슬랙에서 나만의 사용자 지정 슬래시 명령 만들기

궁극적으로 데이터베이스에서 모든 사용자 데이터를 검색하는 기능을 실행하는 코드를 통합합니다. 이 엔드포인트는 관리 권한이 있는 인증된 사용자만 액세스할 수 있는 제한된 액세스 포인트 역할을 합니다.

 exports.getUsers = async (req, res) => {
  try {
    const users = await User.find({});
    res.json(users);
  } catch (error) {
    console.log(error);
    res.status(500).json({ message: 'An error occurred!' });
  }
};

Passport.js 로컬 인증 전략 설정

로그인 정보를 제공한 개인의 신원을 확인하려면 로컬 사용자 인증 메커니즘을 구현해야 합니다.

보다 정교한 방법으로 프로젝트의 루트 레벨에 “middleware/passport.js”라는 이름의 파일을 새로 생성하고 그 안에 다음 코드를 포함하세요.

 const LocalStrategy = require('passport-local').Strategy;
const User = require('../models/user.model');

module.exports = (passport) => {
  passport.use(
    new LocalStrategy(async (username, password, done) => {
      try {
        const user = await User.findOne({ username });

        if (!user) {
          return done(null, false);
        }

        if (user.password !== password) {
          return done(null, false);
        }

        return done(null, user);
      } catch (error) {
        return done(error);
      }
    })
  );
};

본 코드에는 로컬에서 사용하도록 설계된 이 전략을 통해 사용자의 로그인 자격 증명을 확인하는 Passport.js 라이브러리의 인증 메커니즘 구현이 포함되어 있습니다.

인증 요청을 받으면 시스템은 제공된 사용자 이름과 자격 증명이 일치하는 개인과 관련된 정보를 저장소에서 검색합니다. 획득한 데이터의 정확성을 확인한 후 시스템은 사용자 신원의 증거로 사용되는 인증된 사용자 객체를 생성합니다.

JWT 확인 미들웨어 만들기

미들웨어의 디렉터리 내에 “auth.js”라는 이름의 파일을 새로 만들어야 합니다. 이 파일에는 JSON 웹 토큰(JWT)을 개발하고 유효성을 검사하는 함수가 포함되어야 합니다.

 const jwt = require('jsonwebtoken');
const secretKey = process.env.SECRET_KEY;

const generateToken = (payload) => {
  const token = jwt.sign(payload, secretKey, { expiresIn: '1h' });
  return token;
};

const verifyToken = (requiredRole) => (req, res, next) => {
  const token = req.cookies.token;

  if (!token) {
    return res.status(401).json({ message: 'No token provided' });
  }

  jwt.verify(token, secretKey, (err, decoded) => {
    if (err) {
      return res.status(401).json({ message: 'Invalid token' });
    }

    req.userId = decoded.userId;

    if (decoded.role !== requiredRole) {
      return res.status(403).json({
        message: 'You do not have the authorization and permissions to access this resource.'
      });
    }

    next();
  });
};

module.exports = { generateToken, verifyToken };

앞서 언급한 프로세스에는 두 가지 함수가 사용되며, 하나는 “generateToken” 함수로 지정된 만료 기간이 설정된 JSON 웹 토큰(JWT)을 생성하는 역할을 담당합니다. 다른 함수는 “verifyToken” 함수로, 제공된 토큰이 진짜인지, 수명을 초과하지 않았는지 여부를 확인하는 역할을 합니다. 또한 이 함수는 해독된 토큰에 필요한 클레임, 특히 사용자에게 할당된 역할 및 권한과 관련된 클레임이 있는지 확인하여 필요한 권한이 있는 개인만 액세스할 수 있도록 보장합니다.

JSON 웹 토큰(JWT)이 고유한지 확인하려면 아래에 설명된 방식으로 고유한 비밀 키를 생성하여 .env 파일에 추가해야 합니다.

 SECRET_KEY="This is a sample secret key." 

API 경로 정의

 const express = require('express');
const router = express.Router();
const userControllers = require('../controllers/userController');
const { verifyToken } = require('../middleware/auth');

router.post('/api/register', userControllers.registerUser);
router.post('/api/login', userControllers.loginUser);

router.get('/api/users', verifyToken('admin'), userControllers.getUsers);

module.exports = router;

본 코드는 REST(Representational State Transfer) 애플리케이션 프로그래밍 인터페이스에 대한 HTTP 경로를 간략하게 설명합니다. 이 중 사용자와 관련된 경로는 제한된 경로 역할을 합니다. 이 리소스에 대한 액세스를 관리 지정 권한이 부여된 사용자로만 제한하면 역할 기반 액세스 제어 메커니즘을 효율적으로 구현할 수 있습니다.

이 글도 확인해 보세요:  Vite로 React 앱을 설정하는 방법

메인 서버 파일 업데이트

다음과 같은 변경 사항을 구현하여 “server.js” 파일의 내용을 수정할 것을 제안합니다:

 const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const app = express();
const port = 5000;
require('dotenv').config();
const connectDB = require('./utils/db');
const passport = require('passport');
require('./middleware/passport')(passport);

connectDB();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors());
app.use(cookieParser());
app.use(passport.initialize());

const userRoutes = require('./routes/userRoutes');
app.use('/', userRoutes);

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

마지막으로 개발 서버의 실행을 시작하여 애플리케이션을 작동합니다.

 node server.js 

RBAC 메커니즘을 활용하여 인증 시스템 강화

역할 기반 액세스 제어를 시행하는 것은 소프트웨어의 보안을 강화할 수 있는 강력한 방법입니다.

RBAC 라이브러리를 활용하여 사용자 역할을 명시적으로 정의하고 권한을 할당하면 기존 인증 라이브러리를 단순히 통합하는 것보다 더 강력한 솔루션을 제공하여 효율적인 RBAC 시스템을 구축할 수 있으며, 궁극적으로 애플리케이션의 보안을 강화할 수 있습니다.

By 김민수

안드로이드, 서버 개발을 시작으로 여러 분야를 넘나들고 있는 풀스택(Full-stack) 개발자입니다. 오픈소스 기술과 혁신에 큰 관심을 가지고 있고, 보다 많은 사람이 기술을 통해 꿈꾸던 일을 실현하도록 돕기를 희망하고 있습니다.