엘리스 34일차 목요일 온라인강의 Session, JWT, Passport

치즈말랑이·2022년 5월 19일
0

엘리스

목록 보기
35/47
post-thumbnail

01

hash

utils/hash-password.js

const crypto = require('crypto');

module.exports = (password) => {
	const hash = crypto.createHash('sha1');
  	hash.update(password);
	return hash.digest("hex");
}

암호화 라이브러리 crypto를 사용했다.
sh1 방식으로.

routes/index.js

const { Router } = require('express');
const asyncHandler = require('../utils/async-handler');
const { User } = require('../models');
const getHash = require('../utils/hash-password');
const router = Router();

router.post('/join', asyncHandler(async (req, res) => {
  const { email, name, password } = req.body;
  // 비밀번호 해쉬값 만들기
  const hashedPassword = getHash(password);
  const user = await User.create({
    email, name, password:hashedPassword,
  });
  
  console.log('신규 회원', user);
  
  res.redirect('/');
}));

hash-password.js 파일에서 만든 함수로 암호화를 해주고 있는 모습이다.

Passport.js 로그인

passport는 strategy를 설정해줘야 한다.
passport/strategies/local.js

const LocalStrategy = require('passport-local').Strategy;
const { User } = require('../../models');
const hashPassword = require('../../utils/hash-password');

const config = {
  usernameField:'email', // 'email' 필드 사용하도록 설정
  passwordField:'password', // 'password' 필드 사용하도록 설정
};

const local = new LocalStrategy(config, async (email, password, done) => {
  try {
    const user = await User.findOne({ email });
    if (!user) {
      throw new Error('회원을 찾을 수 없습니다.');
    }
    // 검색 한 유저의 비밀번호와 요청된 비밀번호의 해쉬값이 일치하는지 확인
    if (user.password !== hashPassword(password)) {
      throw new Error('비밀번호가 일치하지 않습니다.');
    }

    done (null, {
      shortId: user.shortId,
      email: user.email,
      name: user.name,
    });
  } catch (err) {
    done(err, null);
  }
});

module.exports = local;

passport에서 done은 라우터의 next와 같은 역할을 한다.

passport/index.js

const passport = require('passport');
const local = require('./strategies/local');
module.exports = () => {
  // local strategy 사용
    passport.use(local);
  passport.serializeUser((user, callback) => {
    callback(null, user);
  });

  passport.deserializeUser((obj, callback) => {
    callback(null, obj);
  });
};

local strategy를 사용하겠다고 선언을 해주고, serializerUser와 deserializeUser를 선언해준다.

routes/auth.js

const { Router } = require('express');
const passport = require('passport');

const router = Router();

// passport local 로 authenticate 하기
router.post('/', passport.authenticate('local'), (req, res, next) => {
//req.user
  res.redirect('/');
});

module.exports = router;

라우터에서 미들웨어로 passport를 추가해주는데, local strategy를 사용하게 authenticate도 같이써준다.
이렇게하면 req.user에 유저정보가 저장된다.

Session

유저정보를 세션으로 저장하기 위해서는 mongoDB를 사용하는게 좋다.
그냥 쓰면 서버를 종료했을때 세션정보가 다 삭제되는데, mongoDB에 저장해두면 서버가 종료되어도 정보가 남아있기 때문이다.
app.js

const MongoStore = require('connect-mongo');

app.use(session({ 
  secret: 'elice', 
  resave: false, 
  saveUninitialized: true,
  // 세션 스토어 사용하기
  store: MongoStore.create({
  mongoUrl: 'mongoDB접속주소',
}),
}));

session설정에다가 추가로 적어줘야한다.

populate

이게 좀 어렵다

User와 Post 모델을 연결하는 상황이다.
정확하게는, Post의 author에 User 정보를 join하는 것이다.

  1. Post 스키마에 User 연결
const { Schema } = require('mongoose');
const shortId = require('./types/short-id');

const PostSchema = new Schema({
  shortId,
  title: {
    type: String,
    required: true,
  },
  content: {
    type: String,
    required: true,
  },
  author: {
    type: Schema.Types.ObjectId,
    ref: 'User',
    required: true,
    // index 추가하기
    index: true,
  },
}, {
  timestamps: true,
});

module.exports = PostSchema;
  1. 로그인된 사용자의 shortId로 db에서 사용자정보를 찾아 게시글 생성시 author에 추가
router.post('/', asyncHandler(async (req, res) => {
  const { title, content } = req.body;
  
  if (!title || !content) {
    throw new Error('제목과 내용을 입력 해 주세요');
  }
  // 로그인 된 사용자의 shortId 로 사용자를 찾아 게시글 생성시 작성자로 추가
  const author = await User.findOne({shortId: req.user.shortId});
  if (!author) {
    throw new Error('No Author');
  }
  const post = await Post.create({ title, content, author });
  res.redirect(`/posts/${post.shortId}`);
}));
  1. 게시글 읽기
router.get('/:shortId', asyncHandler(async (req, res) => {
  const { shortId } = req.params;
  const post = await Post.findOne({ shortId }).populate('author');
  
  if (req.query.edit) {
    res.render('post/edit', { post });
    return;
  }
  
  res.render('post/view', { post });
}));

게시글의 shortId를 이용해 Post 모델에서 게시글을 찾고, author 속성을 populate하여 post에 할당한다.

  1. 게시글 수정
router.post('/:shortId', asyncHandler(async (req, res) => {
  const { shortId } = req.params;
  const { title, content } = req.body;
  
  if (!title || !content) {
    throw new Error('제목과 내용을 입력 해 주세요');
  }
  
  const post = await Post.findOne({ shortId }).populate('author'); // 작성자 populate
  // 작성자와 로그인된 사용자의 shortId 가 다를경우 오류 발생
    if (post.author.shortId !== req.user.shortId) {
        throw new Error('작성자가 아닙니다.');
    }
    
  await Post.updateOne({ shortId }, { title, content });
  res.redirect(`/posts/${shortId}`);
}));

이렇게 하면 post.author가 post의 author에 연결된 user모델이 되어 post.author.shortId !== req.user.shortId 처럼 req.user와 비교가 가능하다.

  1. Pagination을 할때는 페이징 처리 다 해준다음에 populate 하면 된다.
const posts = Post
      .find({author})
      .sort({ createdAt : -1 })
      .skip(perPage * (page - 1))
      .limit(perPage)
      .populate('author')

댓글

  1. 댓글 스키마 작성
const { Schema } = require('mongoose');

const CommentSchema = new Schema({
  // content, String, required,
  content: {
    type: String,
    required:true,
  }, 
  // author, User, required
  author: {
    type: Schema.Types.ObjectId,
    ref: 'User',
    required:true,
  }
}, {
  timestamps: true,
});

module.exports = CommentSchema;

댓글스키마의 author를 User 모델과 연결시킨다.

  1. Post 스키마 작성
const { Schema } = require('mongoose');
const shortId = require('./types/short-id');
const CommentSchema = require('./comment');

const PostSchema = new Schema({
  shortId,
  title: {
    type: String,
    required: true,
  },
  content: {
    type: String,
    required: true,
  },
  author: {
    type: Schema.Types.ObjectId,
    ref: 'User',
    required: true,
    index: true,
  },
  // comments 필드 선언
  comments: [CommentSchema],
}, {
  timestamps: true,
});

module.exports = PostSchema;

게시글의 댓글과 댓글스키마를 연결하는데, 댓글모델이아니고 댓글스키마라는걸 주의하자.
왜냐면 위의 Post, User 연결시키는것과는 다르게 이건 댓글을 서브스키마를 사용하여 배열로 선언하는것이기 때문이다.

  1. 게시글에 댓글 나타나게 하기
const { Router } = require('express');
const asyncHandler = require('../utils/async-handler');
const { Post, User } = require('../models');

const router = Router();

router.get('/posts/:shortId/comments', asyncHandler(async (req, res) => {
  const { shortId } = req.params;
  const post = await Post.findOne({ shortId });
  
  // post.comments 의 작성자 populate 하기
  await User.populate(post.comments, {path: 'author'})
  // json 으로 응답 보내기
  res.json(post.comments);
}));

module.exports = router;

게시글의 shortId로 Post모델에 검색해서 게시글을 post에 할당한다.
post의 댓글을 서브쿼리로 등록했으므로 populate하는방법이 좀 다르다.

  1. 게시글에 댓글 쓰기
const { Router } = require('express');
const asyncHandler = require('../utils/async-handler');
const { Post, User } = require('../models');

const router = Router();

router.post('/posts/:shortId/comments', asyncHandler(async (req, res) => {
  const { shortId } = req.params;
  const { content } = req.body;
  const author = await User.findOne({ shortId: req.user.shortId });
  
  // $push operator 사용하여 댓글 추가하기
  await Post.updateOne({
    shortId,
  }, {
    $push: {comments: {content,author}},
  })
  res.json({ result: 'success' });
}));

module.exports = router;

4번과는 다르게 req.user.shortId로 해당되는 user를 User모델에서 찾아 author에 할당한다.
그러고 Post모델을 업데이트해주는데, 게시글의 shortId로 찾아서 $push: {comments:content, author} 처럼 $push를 이용하여 댓글내용과 작성자를 저장한다.
그러고 성공했다는 결과를 보낸다.

JWT

특정 URL에 들어가려면 로그인을 해야하고 그것을 request handler 전에 미들웨어로 끼워넣음
1. 토큰 검증

import jwt from "jsonwebtoken";

function login_required(req, res, next) {
  // request 헤더로부터 authorization bearer 토큰을 받음.
  const userToken = req.headers["authorization"]?.split(" ")[1] ?? "null";

  // 이 토큰은 jwt 토큰 문자열이거나, 혹은 "null" 문자열임.
  // 토큰이 "null" 일 경우, login_required 가 필요한 서비스 사용을 제한함.
  if (userToken === "null") {
    console.log("서비스 사용 요청이 있습니다.하지만, Authorization 토큰: 없음");
    res.status(400).send("로그인한 유저만 사용할 수 있는 서비스입니다.");
    return;
  }

  // 해당 token 이 정상적인 token인지 확인 -> 토큰에 담긴 user_id 정보 추출
  try {

    const secretKey = process.env.JWT_SECRET_KEY || "jwt-secret-key";
    
    // jwt.verify 함수를 이용하여 정상적인 jwt인지 확인 
    const jwtDecoded = jwt.verify(userToken, secretKey);
    // verify 함수로부터 반환된 결과에서 user_id 추출

    const user_id = jwtDecoded.user_id;



    // req 객체에 currentUserId 프로퍼티를 추가하고, 값으로는 user_id를 할당

    req.currentUserId = user_id;




    // next 함수를 호출하여 본래 요청이 갔었던 라우터로 진행
    next();
  } catch (error) {
    res.status(400).send("정상적인 토큰이 아닙니다. 다시 한 번 확인해 주세요.");
    return;
  }
}


export { login_required };
  1. 토큰생성
import { User } from "../db"; // from을 폴더(db) 로 설정 시, 디폴트로 index.js 로부터 import함.
import bcrypt from "bcrypt";
import { v4 as uuidv4 } from "uuid";
import jwt from "jsonwebtoken";

class userAuthService {

  .
  .
  .
  // 로그인 POST
  static async getUser({ email, password }) {
    // 이메일 db에 존재 여부 확인
    const user = await User.findByEmail({ email });
    if (!user) {
      const errorMessage =
        "해당 이메일은 가입 내역이 없습니다. 다시 한 번 확인해 주세요.";
      return { errorMessage };
    }

    // 비밀번호 일치 여부 확인
    const correctPasswordHash = user.password;
    const isPasswordCorrect = await bcrypt.compare(
      password,
      correctPasswordHash
    );
    if (!isPasswordCorrect) {
      const errorMessage =
        "비밀번호가 일치하지 않습니다. 다시 한 번 확인해 주세요.";
      return { errorMessage };
    }

    const secretKey = process.env.JWT_SECRET_KEY || "jwt-secret-key";
          // jwt 의 sign 함수를 이용하여 토큰 생성, 이 때 위의 secretKey 사용
    const token = jwt.sign({ user_id: user.id }, secretKey);

    // 반환할 loginuser 객체를 위한 변수 설정
    const id = user.id;
    const name = user.name;
    const description = user.description;

    const loginUser = {
      token,
      id,
      email,
      name,
      description,
      errorMessage: null,
    };

    return loginUser;
  }
  
	// 회원가입 POST
  static async addUser({ name, email, password }) {
    // 이메일 중복 확인
    const user = await User.findByEmail({ email });
    if (user) {
      const errorMessage =
        "이 이메일은 현재 사용중입니다. 다른 이메일을 입력해 주세요.";
      return { errorMessage };
    }

    // 비밀번호 해쉬화
    const hashedPassword = await bcrypt.hash(password, 10);

    // id 는 유니크 값 부여
    const id = uuidv4();
    const newUser = { id, name, email, password: hashedPassword };

    // db에 저장
    const createdNewUser = await User.create({ newUser });
    createdNewUser.errorMessage = null; // 문제 없이 db 저장 완료되었으므로 에러가 없음.

    return createdNewUser;
  }
  

}

export { userAuthService };
profile
공부일기

0개의 댓글