이 코드들은 Express와 MongoDB를 사용하여 API를 개발하는 설정을 보여줍니다.
주요한 인증, 토큰 처리, MongoDB 연동 및 passport를 통한 인증 전략 설정 등을 포함하고 있습니다. 각각의 파일과 역할에 대해 살펴보겠습니다.
설치 라이브러리
1. 기본 서버 및 유틸리티
npm install express dotenv
- express: Node.js 웹 서버 프레임워크.
- dotenv: .env 파일을 통해 환경변수를 관리.
2. 인증 및 토큰 처리
npm install bcrypt jsonwebtoken passport passport-local express-session
- bcrypt: 비밀번호 해싱 및 검증.
- jsonwebtoken: JWT 토큰 생성 및 검증.
- passport: 인증 미들웨어.
- passport-local: 로컬 인증 전략 지원.
- express-session: 세션 관리.
3.MongoDB 연동
npm install mongoose
mongoose: MongoDB와의 객체-문서 매핑(ODM)을 지원.
4. 에러 처리 및 기타 유틸리티
npm install http-errors
http-errors: HTTP 상태 코드를 쉽게 관리할 수 있는 에러 생성 도구.
5. 개발 시 편의 도구 (선택 사항)
npm install --save-dev nodemon
6. 설치 명령어 종합
위 라이브러리를 한 번에 설치하려면 다음 명령어를 실행하세요.
npm install express dotenv bcrypt jsonwebtoken passport passport-local express-session mongoose http-errors npm install --save-dev nodemon
1. util/auth.js: 인증과 토큰 관련 유틸리티
auth.js는 **JWT(Json Web Token)**를 생성, 검증하고, 토큰을 MongoDB에 저장하거나 삭제하는 작업을 처리합니다.
주요 기능:
1.비밀번호 비교:
const { compare } = require('bcrypt'); function isValidPassword(password, storedPassword) { return compare(password, storedPassword); }
- bcrypt를 사용해 사용자가 입력한 비밀번호와 데이터베이스에 저장된 해시 비밀번호를 비교합니다.
- 2. 토큰 생성:
- 접근 토큰
function createAccessToken(userId) { return sign({ userId }, ACCESS_TOKEN_SECRET, { expiresIn: '1h' }); }
- 사용자 ID를 기반으로 1시간 동안 유효한 JWT를 생성합니다.
- 갱신 토큰:
function createRefreshToken(userId) { return sign({ userId }, REFRESH_TOKEN_SECRET, { expiresIn: '14d' }); }
3. 토큰 저장 및 검증:
- 갱신 토큰을 MongoDB에 저장
async function storeRefreshToken(refreshToken, userId) { const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRATION * 1000); const existingToken = await RefreshToken.findOne({ userId }); if (existingToken) { existingToken.refreshToken = refreshToken; existingToken.expiresAt = expiresAt; await existingToken.save(); } else { await new RefreshToken({ userId, refreshToken, expiresAt }).save(); } }
저장된 갱신 토큰 가져오기
async function getStoredRefreshToken(userId) { return (await RefreshToken.findOne({ userId }))?.refreshToken || null; }
4.미들웨어로 인증 처리:
async function checkAuthMiddleware(req, res, next) { if (!req.headers.authorization) { return next(new NotAuthError('접근 권한이 없습니다.')); } const token = req.headers.authorization.split(' ')[1]; try { const validatedToken = validateAccessToken(token); req.userId = validatedToken.userId; } catch (error) { return res.status(403).json({ message: 'ACCESS_TOKEN_EXPIRED' }); } next(); }
2. passport/index.js: 인증 전략 설정
passport는 세션 기반 인증을 지원하며, 여기서 LocalStrategy와 KakaoStrategy가 설정됩니다.
1.직렬화 및 역직렬화:
사용자 객체를 세션에 저장
passport.serializeUser((user, done) => { done(null, user.id); });
세션에서 사용자 정보를 복원
passport.deserializeUser((id, done) => { User.findOne({ where: { id } }).then(user => done(null, user)).catch(done); });
2.로컬 인증 전략:
사용자 ID와 비밀번호를 확인
passport.use(new LocalStrategy({ usernameField: 'userId', passwordField: 'password', }, async (userId, password, done) => { const exUser = await User.findOne({ userId }); if (exUser) { const result = await bcrypt.compare(password, exUser.password); if (result) done(null, exUser); else done(null, false, { message: '비밀번호가 일치하지 않습니다.' }); } else { done(null, false, { message: '가입되지 않은 회원입니다.' }); } }));
3. app.js: Express 서버 설정
주요 설정:
1.환경변수 로드 및 기본 설정:
dotenv.config(); app.set('port', process.env.PORT || 8001);
.env 파일에서 환경 변수를 불러옵니다.
2.MongoDB 연결:
mongoose.connect(process.env.MONGO_URI) .then(() => console.log('MongoDB 연결 성공')) .catch(err => console.error('MongoDB 연결 실패:', err));
3.미들웨어 사용:
app.use(express.json()); app.use(session({ secret: process.env.COOKIE_SECRET, resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: process.env.NODE_ENV === 'production' }, })); app.use(passport.initialize()); app.use(passport.session());
4.라우터 연결:
const authRouter = require('./routes/auth'); app.use('/api/auth', authRouter);
4. MongoDB 모델
예시: refreshTokens 모델
const mongoose = require('mongoose'); const RefreshTokenSchema = new mongoose.Schema({ userId: { type: String, required: true, unique: true }, refreshToken: { type: String, required: true }, expiresAt: { type: Date, required: true }, }); module.exports = mongoose.model('RefreshToken', RefreshTokenSchema);
갱신 토큰과 만료 날짜를 저장하기 위한 스키마입니다.
요약
- auth.js: 토큰 생성 및 검증, MongoDB에 토큰 저장 및 관리.
- passport/index.js: 인증 전략 설정, 로컬 및 외부 인증 지원.
- app.js: Express 서버 설정, MongoDB 연결 및 라우팅.
- MongoDB: 갱신 토큰과 사용자 정보를 저장.
위 코드는 Express와 MongoDB, 그리고 passport를 활용한 API 서버에서 JWT 기반 인증을 구현하는 예제입니다.
전체:
util/auth.js
const { sign, verify, decode } = require('jsonwebtoken'); const { compare } = require('bcrypt'); const RefreshToken = require('../models/refreshTokens'); // 모델 import const { NotAuthError } = require('./errors'); // 암호화 키를 설정합니다. const KEY = process.env.JWT_SECRET_KEY; const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET; const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; const ACCESS_TOKEN_EXPIRATION = '1h'; // 1시간 const REFRESH_TOKEN_EXPIRATION = 14 * 24 * 60 * 60; // 14일 (초) // 1. 입력받은 비밀번호와 저장된 비밀번호를 비교하는 함수 function isValidPassword(password, storedPassword) { return compare(password, storedPassword); } // 2. 접근 토큰 생성 function createAccessToken(userId) { console.log("2. 접근 토큰 생성 :", userId, ACCESS_TOKEN_SECRET, ACCESS_TOKEN_EXPIRATION); const accessToken = sign({ userId }, ACCESS_TOKEN_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRATION }); const decodedToken = decode(accessToken); return { accessToken, accessTokenExpires: decodedToken.exp, }; } // 3. 갱신 토큰 생성 function createRefreshToken(userId) { const refreshToken = sign({ userId }, REFRESH_TOKEN_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRATION }); const decodedToken = decode(refreshToken); return { refreshToken, refreshTokenExpires: decodedToken.exp, }; } // 4. 접근 토큰 유효성 검사 function validateAccessToken(token) { return verify(token, ACCESS_TOKEN_SECRET); } // 5. 갱신 토큰 유효성 검사 function validateRefreshToken(token) { return verify(token, REFRESH_TOKEN_SECRET); } // 6. 갱신 토큰을 MongoDB에 저장하는 함수 async function storeRefreshToken(refreshToken, userId) { try { const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRATION * 1000); // 만료 시간 계산 const existingToken = await RefreshToken.findOne({ userId }); if (existingToken) { // 기존 토큰 업데이트 existingToken.refreshToken = refreshToken; existingToken.expiresAt = expiresAt; await existingToken.save(); } else { // 새 토큰 생성 const newToken = new RefreshToken({ userId, refreshToken, expiresAt, }); await newToken.save(); } console.log("6. 갱신 토큰을 MongoDB에 저장하는 함수 :", userId, refreshToken, expiresAt); } catch (error) { console.error('Failed to store refresh token:', error); throw new Error('Failed to store refresh token'); } } // 7. 저장된 MongoDB에 값을 가져오는 함수 async function getStoredRefreshToken(userId) { try { const tokenData = await RefreshToken.findOne({ userId }); return tokenData ? tokenData.refreshToken : null; } catch (error) { console.error('Failed to retrieve refresh token:', error); throw new Error('Failed to retrieve refresh token'); } } // 8. 인증 미들웨어 함수 async function checkAuthMiddleware(req, res, next) { console.log("미들웨어 인증 처리 :"); if (req.method === 'OPTIONS') { return next(); } if (!req.headers.authorization) { console.log("접근 권한이 없습니다.-1 :"); return next(new NotAuthError('접근 권한이 없습니다.')); } const authFragments = req.headers.authorization.split(' '); if (authFragments.length !== 2) { console.log("not-authenticated-2 :"); return next(new NotAuthError('접근 권한이 없습니다.')); } const authToken = authFragments[1]; try { const validatedToken = validateAccessToken(authToken); req.token = validatedToken; req.userId = validatedToken.userId; } catch (error) { console.log("not-authenticated-3:", error.message); if (error.message === 'jwt expired') { console.log("접근 토큰 인증 만료"); return res.status(403).json({ message: 'ACCESS_TOKEN_EXPIRED' }); } return next(new NotAuthError('접근 권한이 없습니다.')); } next(); } // 9. 로그아웃 정보를 저장하는 함수 async function storeLogoutInfo(userId, refreshToken) { const key = `logoutInfo:${userId}`; const logoutTimeMilliseconds = Date.now(); const logoutTime = new Date(logoutTimeMilliseconds).toLocaleString(); const logoutInfo = { userId, refreshToken, logoutTimeMilliseconds, logoutTime, }; await redisClient.set(key, JSON.stringify(logoutInfo)); } // 10. 로그아웃 시 갱신 토큰 삭제 async function deleteRefreshToken(userId) { try { await RefreshToken.deleteOne({ userId }); console.log(`갱신 토큰 삭제 완료: ${userId}`); } catch (error) { console.error('Failed to delete refresh token:', error); throw new Error('Failed to delete refresh token'); } } // 11. 접근 토큰 파싱 함수 async function parseAccessToken(req, res, next) { if (req.method === 'OPTIONS') { return next(); } if (!req.headers.authorization) { return next(new NotAuthError('not-authenticated')); } const authFragments = req.headers.authorization.split(' '); if (authFragments.length !== 2) { return next(new NotAuthError('not-authenticated')); } const authToken = authFragments[1]; try { const validatedToken = validateAccessToken(authToken); req.user = validatedToken; req.memberId = validatedToken.memberId; next(); } catch (error) { return next(new NotAuthError('not-authenticated')); } } // userId 받아 JSON Web Token을 생성하는 함수 function createJSONToken(userId) { return sign({ userId }, KEY, { expiresIn: '1h' }); // 유효기간 1시간 } // 토큰을 받아 유효성을 검증하는 함수 function validateJSONToken(token) { return verify(token, KEY); } // 인증 미들웨어 함수 function checkAuthMiddlewareAlone(req, res, next) { if (req.method === 'OPTIONS') { return next(); } if (!req.headers.authorization) { return next(new NotAuthError('인증되지 않았습니다.')); } const authFragments = req.headers.authorization.split(' '); if (authFragments.length !== 2) { return next(new NotAuthError('인증되지 않았습니다.')); } const authToken = authFragments[1]; try { const validatedToken = validateJSONToken(authToken); req.token = validatedToken; } catch (error) { return next(new NotAuthError('인증되지 않았습니다.')); } next(); } module.exports = { createAccessToken, createRefreshToken, validateAccessToken, validateRefreshToken, isValidPassword, storeRefreshToken, getStoredRefreshToken, checkAuthMiddleware, deleteRefreshToken, storeLogoutInfo, parseAccessToken, };
util/errors.js
class NotAuthError { constructor(message) { this.message = message; this.status = 401; // 인증 실패를 나타내는 HTTP 상태 코드 } } exports.NotAuthError = NotAuthError;
passport/index.js
const passport = require('passport'); const local = require('./localStrategy'); const kakao = require('./kakaoStrategy'); const User = require('../models/user'); module.exports = () => { passport.serializeUser((user, done) => { console.log('serialize'); done(null, user.id); }); passport.deserializeUser((id, done) => { console.log('deserialize'); User.findOne({ where: { id }, include: [{ model: User, attributes: ['id', 'nick'], as: 'Followers', }, { model: User, attributes: ['id', 'nick'], as: 'Followings', }], }) .then(user => { console.log('user', user); done(null, user); }) .catch(err => done(err)); }); local(); kakao(); };
passport/kakaoStrategy.js
const passport = require('passport'); const KakaoStrategy = require('passport-kakao').Strategy; const User = require('../models/user'); module.exports = () => { passport.use(new KakaoStrategy({ clientID: process.env.KAKAO_ID, callbackURL: '/auth/kakao/callback', }, async (accessToken, refreshToken, profile, done) => { console.log('kakao profile', profile); try { const exUser = await User.findOne({ where: { snsId: profile.id, provider: 'kakao' }, }); if (exUser) { done(null, exUser); } else { const newUser = await User.create({ email: profile._json?.kakao_account?.email, nick: profile.displayName, snsId: profile.id, provider: 'kakao', }); done(null, newUser); } } catch (error) { console.error(error); done(error); } })); };
passport/localStrategy.js
const passport = require('passport'); const LocalStrategy = require('passport-local').Strategy; const bcrypt = require('bcrypt'); const User = require('../models/user'); module.exports = () => { // 로그인 passport.use(new LocalStrategy({ usernameField: 'userId', // req.body.userId passwordField: 'password', // req.body.password passReqToCallback: false, }, async (userId, password, done) => { console.log("패스포트 전략 실행:", { userId, password }); try { const exUser = await User.findOne({ where: { userId } }); if (exUser) { const result = await bcrypt.compare(password, exUser.password); if (result) { done(null, exUser); } else { done(null, false, { message: '비밀번호가 일치하지 않습니다.' }); } } else { done(null, false, { message: '가입되지 않은 회원입니다.' }); } } catch (error) { console.error(error); done(error); } })); };
댓글 ( 0)
댓글 남기기