Node JS에서 액세스 토큰 및 새로 고침 토큰을 사용한 JWT 인증

56132 단어 jwtjavascriptnodereact
무슨 일이야 얘들 아. 우리 모두는 모든 애플리케이션에서 가장 중요한 기능이 인증이라는 것을 알고 있습니다. 인증을 훨씬 더 안전하게 만들고 더 나은 사용자 경험을 제공하려면 앱에서 새로 고침 및 액세스 토큰 기반 인증을 사용해야 합니다. 새로 고침 토큰이 무엇인지, 왜 사용해야 하는지, 어떻게 사용해야 하는지 생각할 수 있습니다. 걱정하지 마세요. 처음부터 모든 것을 다룰 것입니다.

그럼 코딩을 시작해 볼까요?

더 나은 이해를 위해 데모 비디오를 시청하는 것이 좋습니다. 내 작업이 마음에 든다면 내 채널을 지원해 주세요.

데모 비디오


Project Github Link

다음 표는 내보낸 Rest API의 개요를 보여줍니다.


행동 양식
URL
행위


게시하다
/가입하기
사용자 가입

게시하다
/로그인
로그인 사용자

게시하다
/새로 고침 토큰
새 액세스 토큰 받기

삭제
/새로 고침 토큰
로그아웃 사용자


새로 고침 토큰이란 무엇입니까?

Refresh Token은 Access Token에 불과하지만 1~2개월 정도의 수명을 가지고 있습니다. 액세스 토큰의 만료 시간은 약 10~15분입니다. 이 액세스 토큰이 만료될 때마다. 우리는 새 액세스 토큰을 얻기 위해 사용자에게 다시 로그인하도록 요청하지 않고 대신 서버에 새로 고침 토큰을 보냅니다. 여기에서 해당 토큰을 확인하고 새 액세스 토큰을 클라이언트에 보냅니다. 이 방법을 사용하면 사용자는 반복해서 로그인할 필요가 없습니다. 이것은 사용자 경험을 사용자에게 훨씬 더 쉽게 만듭니다.

Node.js 앱 만들기

$ mkdir refreshTokenAuth
$ cd refreshTokenAuth
$ npm init --yes
$ npm install express mongoose jsonwebtoken dotenv bcrypt joi joi-password-complexity 
$ npm install --save-dev nodemon


프로젝트 구조



패키지.json

{
  "name": "refreshTokenAuth",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "type": "module",
  "scripts": {
    "start": "nodemon server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcrypt": "^5.0.1",
    "dotenv": "^16.0.0",
    "express": "^4.17.3",
    "joi": "^17.6.0",
    "joi-password-complexity": "^5.1.0",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^6.2.8"
  },
  "devDependencies": {
    "nodemon": "^2.0.15"
  }
}


사용자 모델
/모델/User.js

import mongoose from "mongoose";

const Schema = mongoose.Schema;

const userSchema = new Schema({
    userName: {
        type: String,
        required: true,
    },
    email: {
        type: String,
        required: true,
        unique: true,
    },
    password: {
        type: String,
        required: true,
    },
    roles: {
        type: [String],
        enum: ["user", "admin", "super_admin"],
        default: ["user"],
    },
});

const User = mongoose.model("User", userSchema);

export default User;


사용자 토큰 모델
/models/UserToken.js

import mongoose from "mongoose";

const Schema = mongoose.Schema;

const userTokenSchema = new Schema({
    userId: { type: Schema.Types.ObjectId, required: true },
    token: { type: String, required: true },
    createdAt: { type: Date, default: Date.now, expires: 30 * 86400 }, // 30 days
});

const UserToken = mongoose.model("UserToken", userTokenSchema);

export default UserToken;


토큰 생성 기능
/utils/generateTokens.js

import jwt from "jsonwebtoken";
import UserToken from "../models/UserToken.js";

const generateTokens = async (user) => {
    try {
        const payload = { _id: user._id, roles: user.roles };
        const accessToken = jwt.sign(
            payload,
            process.env.ACCESS_TOKEN_PRIVATE_KEY,
            { expiresIn: "14m" }
        );
        const refreshToken = jwt.sign(
            payload,
            process.env.REFRESH_TOKEN_PRIVATE_KEY,
            { expiresIn: "30d" }
        );

        const userToken = await UserToken.findOne({ userId: user._id });
        if (userToken) await userToken.remove();

        await new UserToken({ userId: user._id, token: refreshToken }).save();
        return Promise.resolve({ accessToken, refreshToken });
    } catch (err) {
        return Promise.reject(err);
    }
};

export default generateTokens;


새로 고침 토큰 기능 확인
/utils/verifyRefreshToken.js

import UserToken from "../models/UserToken.js";
import jwt from "jsonwebtoken";

const verifyRefreshToken = (refreshToken) => {
    const privateKey = process.env.REFRESH_TOKEN_PRIVATE_KEY;

    return new Promise((resolve, reject) => {
        UserToken.findOne({ token: refreshToken }, (err, doc) => {
            if (!doc)
                return reject({ error: true, message: "Invalid refresh token" });

            jwt.verify(refreshToken, privateKey, (err, tokenDetails) => {
                if (err)
                    return reject({ error: true, message: "Invalid refresh token" });
                resolve({
                    tokenDetails,
                    error: false,
                    message: "Valid refresh token",
                });
            });
        });
    });
};

export default verifyRefreshToken;


검증 스키마 기능
/utils/validationSchema.js

import Joi from "joi";
import passwordComplexity from "joi-password-complexity";

const signUpBodyValidation = (body) => {
    const schema = Joi.object({
        userName: Joi.string().required().label("User Name"),
        email: Joi.string().email().required().label("Email"),
        password: passwordComplexity().required().label("Password"),
    });
    return schema.validate(body);
};

const logInBodyValidation = (body) => {
    const schema = Joi.object({
        email: Joi.string().email().required().label("Email"),
        password: Joi.string().required().label("Password"),
    });
    return schema.validate(body);
};

const refreshTokenBodyValidation = (body) => {
    const schema = Joi.object({
        refreshToken: Joi.string().required().label("Refresh Token"),
    });
    return schema.validate(body);
};

export {
    signUpBodyValidation,
    logInBodyValidation,
    refreshTokenBodyValidation,
};


인증 경로
/routes/auth.js

import { Router } from "express";
import User from "../models/User.js";
import bcrypt from "bcrypt";
import generateTokens from "../utils/generateTokens.js";
import {
    signUpBodyValidation,
    logInBodyValidation,
} from "../utils/validationSchema.js";

const router = Router();

// signup
router.post("/signUp", async (req, res) => {
    try {
        const { error } = signUpBodyValidation(req.body);
        if (error)
            return res
                .status(400)
                .json({ error: true, message: error.details[0].message });

        const user = await User.findOne({ email: req.body.email });
        if (user)
            return res
                .status(400)
                .json({ error: true, message: "User with given email already exist" });

        const salt = await bcrypt.genSalt(Number(process.env.SALT));
        const hashPassword = await bcrypt.hash(req.body.password, salt);

        await new User({ ...req.body, password: hashPassword }).save();

        res
            .status(201)
            .json({ error: false, message: "Account created sucessfully" });
    } catch (err) {
        console.log(err);
        res.status(500).json({ error: true, message: "Internal Server Error" });
    }
});

// login
router.post("/logIn", async (req, res) => {
    try {
        const { error } = logInBodyValidation(req.body);
        if (error)
            return res
                .status(400)
                .json({ error: true, message: error.details[0].message });

        const user = await User.findOne({ email: req.body.email });
        if (!user)
            return res
                .status(401)
                .json({ error: true, message: "Invalid email or password" });

        const verifiedPassword = await bcrypt.compare(
            req.body.password,
            user.password
        );
        if (!verifiedPassword)
            return res
                .status(401)
                .json({ error: true, message: "Invalid email or password" });

        const { accessToken, refreshToken } = await generateTokens(user);

        res.status(200).json({
            error: false,
            accessToken,
            refreshToken,
            message: "Logged in sucessfully",
        });
    } catch (err) {
        console.log(err);
        res.status(500).json({ error: true, message: "Internal Server Error" });
    }
});

export default router;


새로 고침 토큰 경로
/routes/refreshToken.js

import { Router } from "express";
import UserToken from "../models/UserToken.js";
import jwt from "jsonwebtoken";
import { refreshTokenBodyValidation } from "../utils/validationSchema.js";
import verifyRefreshToken from "../utils/verifyRefreshToken.js";

const router = Router();

// get new access token
router.post("/", async (req, res) => {
    const { error } = refreshTokenBodyValidation(req.body);
    if (error)
        return res
            .status(400)
            .json({ error: true, message: error.details[0].message });

    verifyRefreshToken(req.body.refreshToken)
        .then(({ tokenDetails }) => {
            const payload = { _id: tokenDetails._id, roles: tokenDetails.roles };
            const accessToken = jwt.sign(
                payload,
                process.env.ACCESS_TOKEN_PRIVATE_KEY,
                { expiresIn: "14m" }
            );
            res.status(200).json({
                error: false,
                accessToken,
                message: "Access token created successfully",
            });
        })
        .catch((err) => res.status(400).json(err));
});

// logout
router.delete("/", async (req, res) => {
    try {
        const { error } = refreshTokenBodyValidation(req.body);
        if (error)
            return res
                .status(400)
                .json({ error: true, message: error.details[0].message });

        const userToken = await UserToken.findOne({ token: req.body.refreshToken });
        if (!userToken)
            return res
                .status(200)
                .json({ error: false, message: "Logged Out Sucessfully" });

        await userToken.remove();
        res.status(200).json({ error: false, message: "Logged Out Sucessfully" });
    } catch (err) {
        console.log(err);
        res.status(500).json({ error: true, message: "Internal Server Error" });
    }
});

export default router;


.env 파일
/.env

DB = Your database URL
SALT = 10
ACCESS_TOKEN_PRIVATE_KEY = Add your private key
REFRESH_TOKEN_PRIVATE_KEY = Add your private key


데이터베이스 연결
/dbConnect.js

import mongoose from "mongoose";

const dbConnect = () => {
    const connectionParams = { useNewUrlParser: true };
    mongoose.connect(process.env.DB, connectionParams);

    mongoose.connection.on("connected", () => {
        console.log("Connected to database sucessfully");
    });

    mongoose.connection.on("error", (err) => {
        console.log("Error while connecting to database :" + err);
    });

    mongoose.connection.on("disconnected", () => {
        console.log("Mongodb connection disconnected");
    });
};

export default dbConnect;


Sever.js
/server.js

import express from "express";
import { config } from "dotenv";
import dbConnect from "./dbConnect.js";
import authRoutes from "./routes/auth.js";
import refreshTokenRoutes from "./routes/refreshToken.js";

const app = express();

config();
dbConnect();

app.use(express.json());

app.use("/api", authRoutes);
app.use("/api/refreshToken", refreshTokenRoutes);

const port = process.env.PORT || 8080;
app.listen(port, () => console.log(`Listening on port ${port}...`));


이것이 우리가 Node JS에서 새로 고침 및 액세스 토큰 기반 인증을 성공적으로 구현한 것입니다.

이 프로젝트의 보너스를 위해 인증된 사용자만 액세스할 수 있는 경로와 역할 기반 인증을 구현했습니다. Demo Video에서 찾을 수 있습니다.

감사합니다 :)

좋은 웹페이지 즐겨찾기