Nodejs

 

이 코드들은 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: 갱신 토큰과 사용자 정보를 저장.

위 코드는 ExpressMongoDB, 그리고 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);
    }
  }));
};

 

 

 

 

 

 

 

 

 

 

 

 

express

 

about author

PHRASE

Level 60  라이트

악한 사람은 자기 자신을 마치 사나운 적이라도 되는 것처럼 학대한다. 하물며 남과 어떻게 친구가 될 수 있을 것인가? -아리스토텔레스

댓글 ( 0)

댓글 남기기

작성

Nodejs 목록    more