프로젝트 재구성, JWT Auth Logic

SangYeon Min·2024년 2월 19일
0

PROJECT-HEARUS

목록 보기
1/12
post-thumbnail

프로젝트 재구성

React-Native FE, NLP, DA, 기획 파트의 팀원들을 새롭게 맞이하고
2024년도 상반기 제품 상용화, 하반기 수익화를 위해 프로젝트를 재구성하였다.

추후 위 이미지 구조에 프로젝트를 진행하며 내용을 추가할 예정이다.
Github, Notion, Jira, Slack 등을 통해 협업을 진행할 예정이다.

API Chart

현재 기획된 MVP 모델에서의 예상 API Chart이다.
OAuth, 일반 Auth를 구분하여 API를 구성하고 강의 데이터 구조가 확정되면 /lecture 라우트 하위에서 해당 데이터에 대한 처리를 진행할 예정이다.

또한 실시간 처리를 위한 Socket.io의 서버 기준 on, emit 처리는 위와 같다.

Auth DB 요구사항 명세서

User 모델의 요구사항 명세서를 위와 같이 작성하였다.
강의 데이터를 저장할 Lecture 모델은 추후 작성할 예정이다.


Auth API Implementation

Model-Router-Controller Structure

/HEARUS-BACKEND
    /controllers
    	로직을 구현하고 response를 보냄
    /middlewares
    	HTTP req와 res 사이에서 단계별 동작을 수행
    /models
    	Table Column들의 표현
    /routers
    	특정 주소를 구현된 controller로 라우팅
    app.js

API BE 서버를 구현하기 위해 프로젝트를 위와 같이 구성한다.

MongoDB

위 이미지와 같이 Hearus-Cluster를 구성하였다.

npm install mongodb bcrypt express-session mongoose

이후 MongoDB를 사용하기 위해 mongodb, bcrypt, mongoose 패키지를 설치한다.

const mongoose = require('mongoose');

// Connect MongoDB with mongoose
const uri = 'mongodb+srv://' +
  process.env.MONGO_USERNAME + ':' +
  process.env.MONGO_PASSWORD + '@' +
  process.env.MONGO_HOST + '/' +
  'test?retryWrites=true&w=majority';

mongoose.connect(uri, {
  serverSelectionTimeoutMS: 5000
}).catch(err => console.log(err.reason))
  .then(console.log("MongoDB Connected"));

위와 같이 환경변수로 USERNAME, PASSWORD, HOST를 connect시에 입력한다.

const mongoose = require('mongoose');

const userSchema = mongoose.Schema({
    name: {
        type: String,
        maxlength: 50,
        required: true,
    },
    email: {
        // 중복 허용x, 사용자 로그인시 사용
        type: String,
        trim: true,
        unique: 1,
        required: true,
    },
    password: {
        type: String,
        minlength: 5,
        required: true,
    },
    isOAuth: {
        type: Boolean,
        required: true,
    },
    OAuthType: {
        type: String,
        enum: ['kakao', 'google', 'naver'],
    },
    school: {
        type: String,
        required: true,
    },
    major: {
        type: String,
        required: true,
    },
    grade: {
        // 1학년, 2학년, 3학년, 4학년, 휴학생, 졸업생, 대학원생
        type: String,
        enum: ['freshman', 'sophomore', 'junior', 'senior', 'leaveAbsense', 'graduate', 'postgraduate'],
        required: true,
    },
    savedLectures: {
        Array: {
            type: String,
        },
    },
    usePurpose: {
        type: String,
        enum: ['offline', 'online', 'conversation', 'else'],
        required: true,
    },
});

const User = mongoose.model("User", userSchema);
module.export = { User };

또한 이전 DB 요구사항 명세서를 위와 같이 mongoose를 활용하여 구현한다.

Signup API

// controller/auth.js
const bycrpt = require('bcrypt');
const User = require('../models/user');

exports.signup = async (req, res, next) => {
    // destructure req.body
    const {
        name, email, password, isOAuth, OAuthType,
        school, major, grade, savedLectures, usePurpose,
    } = req.body;

    try {
        const exUser = await User.findOne({ email: email });
        if (exUser)
            return res.status(409).json({ status: "fail", message: "User Already Exists" });

        // hash password
        const salt = await bycrpt.genSalt(10);
        const hashedPW = await bycrpt.hash(password, salt);

        // save newUser
        const newUser = new User({
            name, email, password: hashedPW, isOAuth, OAuthType,
            school, major, grade, savedLectures, usePurpose,
        });
        await newUser.save();

        return res.status(201).json({ status: "success", message: "Signup success" });
    } catch (error) {
        console.error(error);
        return next(error);
    }

}

authcontroller에서 signup 함수를 위와 같이 구현하여 export한다.

// routes/auth.js
const express = require('express');
const { signup } = require('../controllers/auth');

const router = express.Router();

router.post('/signup', signup);

module.exports = router;
// app.js
const authRouter = require('./routes/auth');
...
app.use('/auth', authRouter);

이후 위와 같이 Router를 추가하여 준다.
위 이미지와 같이 JSON 형태로 Signup을 요청하면
MongoDB에 데이터가 정상적으로 들어가며 signup 기능이 구현된 것을 볼 수 있다.

Token Strategy

보안을 강화하기 위해 위와 같이 AccessTokenRefreshToken을 로그인시 발급한다.
이때 AccessToken이 1h이후 만료되면 만료시간이 더 긴 RefreshToken으로 재발급할 수 있도록 한다.

Login API

npm install jsonwebtoken

Token 방식의 Auth를 구현하기 위해 jsonwebtoken 패키지를 설치한다.

// controllers/auth.js
exports.login = async (req, res, next) => {
    // Destructure req.body
    const {
        email, password, isOAuth, OAuthType,
    } = req.body;

    // OAuth
    if (isOAuth)
        return res.status(405).json({ status: "fail", message: "OAuth not Implemented" });

    try {
        // Find User
        const exUser = await User.findOne({ email: email });
        if (!exUser)
            return res.status(401).json({ status: "fail", message: "Unknown User" });

        // Match password
        const matchPW = await bycrpt.compare(password, exUser.password);
        if (!matchPW)
            return res.status(401).json({ status: "fail", message: "Wrong Password" });

        // Certify Tokens
        const accessToken = jwt.sign({ userID: exUser._id }, process.env.JWT_ACCESS_SECRET, {
            expiresIn: '1h',
        });
        const refreshToken = jwt.sign({ userID: exUser._id }, process.env.JWT_REFRESH_SECRET, {
            expiresIn: '24h',
        });

        return res.status(201).json({
            status: "success",
            message: "Login Success",
            accessToken: accessToken,
            refreshToken: refreshToken,
        });
    } catch (error) {
        console.error(error);
        return next(error);
    }
}

bycrpt를 통해 암호화된 비밀번호를 검증하고 이후 JWT를 발급하여 res로 전달한다.

// middlewares/jwtToken.js
exports.verifyAccessToken = (req, res, next) => {
    const token = req.header('Authorization');
    if (!token)
        return res.status(401).json({ error: 'Access Denied' });

    try {
        const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
        req.userID = decoded.userID;
        next();
    } catch (error) {
        res.status(401).json({ error: 'Invalid Token' });
    }
};

exports.verifyRefreshToken = (req, res, next) => {
    const token = req.header('Authorization');
    if (!token)
        return res.status(401).json({ error: 'Access Denied' })

    try {
        const decoded = jwt.verify(token, process.env.JWT_REFRESH_SECRET);
        req.userID = decoded.userID;
        next();
    } catch (error) {
        res.status(401).json({ error: 'Invalid Token' });
    }
}

AccessTokenRefreshToken를 서로 다른 SecretKey로 verify하는 미들웨어를 작성한다.

// controllers/auth.js
exports.renewAccessToken = async (req, res, next) => {
    try {
        // Renew Access Token
        const accessToken = jwt.sign({ userID: req.userID }, process.env.JWT_ACCESS_SECRET, {
            expiresIn: '1h',
        });

        return res.status(201).json({
            status: "success",
            message: "Renew Access Token",
            accessToken: accessToken,
        });
    } catch (error) {
        console.error(error);
        return next(error);
    }
}

또한 만료된 AccessToken를 재발급할 수 있는 로직을 작성한다.

// routes/auth.js
const express = require('express');
const { verifyToken } = require('../middlewares/jwtToken');
const { signup, login, renewAccessToken } = require('../controllers/auth');

const router = express.Router();

router.post('/signup', signup);

router.post('/login', login);

router.post('/renewToken', verifyToken, renewAccessToken);

module.exports = router;

최종적으로 controller를 router를 통해 붙여주면 JWT를 이용한 Token Auth 로직 구현이 완료된다.

Test Auth

Auth와 Token을 테스트하기 위해 추후 구현될 lecture 구조를 구현하였다.

// routes/lecture.js
var express = require('express');
const { verifyAccessToken } = require('../middlewares/jwtToken');
const { renderLecture } = require('../controllers/lecture');
var router = express.Router();

router.get('/', verifyAccessToken, renderLecture);

module.exports = router;
// controllers/lecture.js
exports.renderLecture = (req, res) => {
    res.locals.userID = req.userID;
    res.render('lecture');
};

lecture controller는 token에서 decoded된 userID를 받아와서 view로 넘겨준다.

<!-- views/lecture.html -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        body {
            background-color: #121212;
            margin: 0;
        }

        .background {
            height: 100vh;
        }

        .card-container {
            border-radius: 20px;
            width: 30%;
            height: 50%;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            background-color: #1e1e1e;
        }

        .card-item {
            margin: 15px;
        }

        .logo-img {
            border-radius: 100px;
            width: 150px;
        }

        .btn {
            font-size: 20px;
            border-radius: 10px;
            background-color: #337ea9;
            color: white;
        }

        .text {
            font-size: 15px;
            color: white;
        }
    </style>
</head>

<body>
    <div class="background d-flex justify-content-center align-items-center vh-100">
        <div class="card-container text-center">
            <img src="/images/logo-red.png" alt="Logo" class="card-item logo-img mb-3" />
            <p class="text">Protected Lecture API</p>
            <p class="text">{{userID}}</p>
        </div>
    </div>

</body>

</html>

/lecture

헤더에 토큰정보 없이 /lecture에 요청한 결과이다.
토큰과 함께 요청하면 userID를 뷰에서 받아오는 것을 볼 수 있다.
마지막으로 refreshToken을 통해 요청하면 요청이 거부되는 것을 볼 수 있다.

/auth/renewToken

accessToken으로 요청하면 요청이 거부되며
refreshToken으로 요청하면 새로운 accessToken이 발급되는 것을 볼 수 있다.

0개의 댓글