JWT 인증 로그인 시스템 구현하기. B

캡틴 노드랭크·2021년 6월 29일
2

NodeJS

목록 보기
2/12

아직은 미완성이지만 학습과 여러차례 시도 끝에 어느정도 기반은 마무리되었다. 우선 어떻게 해결하면서 작성했는지 기록하려고한다.

사용한 프레임워크 및 라이브러리

Passport라이브러리는 Social 로그인을 구현 및 jwt 인증을 위해 미리 설치해놨다.

서버 디렉토리 구조

한 파일 내에서 다양한 코드를 작성하기에는 줄이 너무길어지고, 특정 버그 발생시 문제되는 코드의 위치를 찾기 적절하게 모듈화 해두었다.

복잡해 보이지만, 각자의 역할을 주어진 카테고리에 맞게 폴더를 나누었다.

기본 서버 구성

gserver.js

const express = require("express");
// const passportConfig = require("./passport/index");
// const passport = require("passport");
const https = require("https");

const path = require("path");
const fs = require("fs");
const cors = require("cors");
const cookieParser = require("cookie-parser");
const logger = require("morgan");

require("dotenv").config();
const authRoute = require("./routes/users");

const port = process.env.PORT || 8000;
const app = express();

const cookieOption = {
  httpOnly: true,
  secure: true,
  sameSite: true,
};

const corsOption = {
  origin: true,
  credentials: true,
  methods: ["GET", "POST", "PUT", "DELETE", "OPTION"],
};

const sslCert = {
  cert: fs.readFileSync(path.join(__dirname, "cert", "cert.pem")),
  key: fs.readFileSync(path.join(__dirname, "cert", "key.pem")),
};

app.use(logger("dev"));
app.use(cookieParser(cookieOption));
app.use(cors(corsOption));

app.use(express.urlencoded({ extended: false }));
app.use(express.json());
// app.use(passport.initialize());
// passportConfig();

app.use("/auth", authRoute);
const gServer = https.createServer(sslCert, app);

gServer.listen(port, () => {
  console.log(`localhost:${port}`);
});

저번에 작성했던 https 프로토콜을 사용한 express웹서버를 가져다가 좀더 보강했다.

cookie-parser가 보이는데 이건 왜사용함?
처음에 cookie-parser라이브러리를 사용하기 위해 왜 써야하는지 찾아봤다. 우선 토큰은 클라이언트에서 로그인을 하면 서버에서 확인 후 토큰을 발급해준다.

하지만 이 증명서를 보관하기 위해서 저장공간(스토리지)이 필요한데 쿠키와 웹스토리지(세션 + 로컬)로 나뉘게된다.

우선 웹스토리지에 보관할 경우 반영구적으로 넓은 공간에 보관할 수 있는 이점이 있지만. 애플리케이션이 XSS 공격에 노출될 우려가 있기 때문에 보안이 중요한 이시대에서는 좋은 방법은 아닌것 같았다.

반면 쿠키에 저장할 경우 httpOnly 속성을 통해 변조된 코드(Javascript)를 통한 접근을 막을 수 있다. cookie에 저장된 증명서를 쉽게 불러오기위해 cookie-parser라이브러리를 사용해주었다.

전문내용 출처

/routes/user.js

const router = require("express").Router();

const userController = require("../controller/userController/userController");

router.post("/login", userController.login);
router.post("/register", userController.register);

module.exports = router;

보이는데로 URL routing 해주는 공간이다.

URL 라우팅이란?

URL 라우팅이라는 것은 사용자가 접근 한 URL에 따라서 Controller의 메소드를 호출해주는 기능이다.

/Controller/userController/userController.js

module, register

const bcrypt = require("bcrypt");
const { User } = require("../../models");
const jwt = require("jsonwebtoken");
const {
  registerValidator,
  loginValidator,
} = require("../../validation/authValidation");

require("dotenv").config();

exports.register = async (req, res) => {
  const { errors, isValid } = registerValidator(req.body);

  let { email, username, password } = req.body;

  if (!isValid) {
    return res.status(400).json(errors);
  }

  const emailExists = await User.findOne({ Where: { email: email } });
  console.log(emailExists);
  if (emailExists) {
    return res.status(400).json({ message: "이미 사용중인 이메일입니다." });
  }
  const salt = await bcrypt.genSalt(10);
  const hashedPassword = await bcrypt.hash(password, salt);

  const newUser = {
    username: username,
    email: email,
    password: hashedPassword,
  };

  User.create(newUser)
    .then((save) => {
      res.status(200).json({ status: "Success", new_user_id: save.id });
    })
    .catch((err) => res.status(500).json({ message: err + "잘안됩니다." }));

email, password, username을 json으로 요청하게되면 req.body에 담겨 전송하는데

DB에 User.findAll() =SELECT * FROM User을 활용해 email의 중복을 검사하고

중복된 메일이 없으면(존재하지 않으면), 패스워드에 소금을 친다.(hashing)

그렇게 해싱된 패스워드를 가진 신규 유저를 User.create로 DB에 새로 추가한다.

module, login

exports.login = async (req, res) => {
  const { errors, isValid } = loginValidator(req.body);

  if (!isValid) {
    return res.status(400).json({message: errors});
  }

  const { email, password } = req.body;

  const user = await User.findOne({ where: { email: email } });
  if (!user) return res.status(400).send("존재하지 않는 계정입니다.");

  const ValidPassword = await bcrypt.compare(password, user.password);
  if (!ValidPassword) {
    return res.status(400).send("패스워드를 제대로 입력하세요");
  }

  const token = jwt.sign(
    {
      id: user.id,
      exp: Math.floor(Date.now() / 1000) + 60 * 60,
    },
    process.env.JWT_SECRET
  );

  res.cookie("auth_token", token).json({ tokne: token });
}

로그인 함수는 역시 req.body에 담긴 email, password를 findOne() 으로 DB에 저장된 email을 검사한다.

소금에 절여진 패스워드와 현재 패스워드를 bcrypt.comapre로 비교하고, token을 발급받는다.

jwt.sign의 첫번째 인자는 payload고 해당 유저의 고유번호인 id에 유효시간 1시간뒤에 만료되는 토큰을 지급하고(수정할것)

jwt.sign의 두번째 인자는 비밀 키 값을 넣는다.

그리고 cookie에 저장한다.

Q. bcrypt와 jwt를 사용한이유.

A. bcrypt
bcrypt는 간단하게 말해 데이터베이스에 유저 정보를 저장할 때
비밀번호 데이터를 hasing해주는 정말 편리한 라이브러리이다.

현재 비밀번호와 salt값을 통해 해싱 후 로그인이나 패스워드 변경시 현재 비밀번호와 비밀번호 확인시 동일한 값인지 검증한다.

A. jwt
Jsonwebtoken으로 토큰을 생성, 저장 하기 쉽게 도와주는 라이브러리이다. 기존에는 생성, 재발급 등 직접 함수로 구현해줬었기 때문에 항암제 역할을 해준다.

A. Validator? 이게뭐야?

아래에서 설명 예정

/validation/authValidation.js

const Validator = require("validator");

const isEmpty = (value) =>
  value === undefined ||
  value === null ||
  (typeof value === "object" && Object.keys(value).length === 0) ||
  (typeof value === "string" && value.trim().length === 0);

const registerValidator = (data) => {
  let errors = {};

  data.username = !isEmpty(data.username) ? data.username : "";
  data.email = !isEmpty(data.email) ? data.email : "";
  data.password = !isEmpty(data.password) ? data.password : "";

  if (Validator.isEmpty(data.username)) {
    errors.username = "유저네임을 입력해주세요.";
  }

  if (Validator.isEmpty(data.email)) {
    errors.email = "이메일을 입력해주세요.";
  }

  if (Validator.isEmpty(data.password)) {
    errors.password = "비밀번호를 입력해주세요.";
  }

  if (!Validator.isLength(data.password, { min: 6, max: 30 })) {
    errors.password = "비밀번호는 6자 이상 30자 미만으로 작성해야합니다.";
  }

  return {
    errors,
    isValid: isEmpty(errors),
  };
};

const loginValidator = (data) => {
  let errors = {};

  data.email = !isEmpty(data.email) ? data.email : "";
  data.password = !isEmpty(data.password) ? data.password : "";

  if (!Validator.isEmail(data.email)) {
    errors.email = "유효한 이메일이 아닙니다.";
  }

  if (Validator.isEmpty(data.email)) {
    errors.email = "이메일을 입력해주세요";
  }

  if (Validator.isEmpty(data.password)) {
    errors.password = "패스워드를 입력해주세요";
  }

  return {
    errors,
    isValid: isEmpty(errors),
  };
};

module.exports.registerValidator = registerValidator;
module.exports.loginValidator = loginValidator;

validator은 문자열 유효성 검사기 라이브러리이다. 처음에 몰랐었는데 이게 얼마나 편하냐면, 회원가입이나 로그인 구현할 떄

if, else문으로 중복검사나 빈문자열을 작성 해줘서 다양한 검증을 해야했는데, 이 라이브러리는 문자열을 기반으로 수많은 유효성을 검사할 수있다. isEmptyisEmail만을 검사하지만 1/10도 사용하지 않았다.

validator

문제점1. 회원가입 중복검사

처음엔 Postman으로 회원가입은 잘 됬었다.


로그인도..

문제는 새 계정을 생성 할 경우 계속 중복처리를 해버린다.

  const emailExists = await User.findOne({ Where: { email: email } });
  console.log(emailExists);
  if (emailExists) {
    return res.status(400).json({ message: "이미 사용중인 이메일입니다." });
  }

여기서 걸리는건지 아직은 잘 모르겠다..

어디가 문제인지 원인분석중이다..

문제점2. passport를 잘 이해하지 못하고 시도한점..

passport는 소셜(카카오, 페이스북, 구글 등)및 로컬 인증 방식을 위한 라이브러리이다. 이 라이브러리가 없다면 각각 따로 구현해줘야 하는 걸로 알고있었다.

하지만 나는 잠깐 공식API를 보고 무작정 도입하려고 했었다. 특히나 회원가입도 같이 적용하려는 무리수를 뒀는데, 신중하게 자세히 알아보고 도입해야한다.

문제점3 멍청하게 작성된 .env환경변수

.env환경 변수를 생성하고 dotenv.config()로 환경변수를 불러오는데 계속 안되고 sequelize도 DB이름과 사용자를 해석하지 못했다. 뭔가 이상해서 다시 .env를 봤는데 멍청하게 작성되어있었다.

DB_NAME:
DB_PW:
DB_USER:
PORT:
JWT_SECRET:

콜론이 들어가버린것.....

DB_NAME=
DB_PW=
DB_USER=
PORT=
JWT_SECRET=

이렇게 쓰자

profile
다시 처음부터 천천히... 급할필요가 없다.

0개의 댓글