풀스택 맛보기8-Join & Login

  1. User
    1. Model 생성
    2. form 생성
  2. Password
    1. Hashing
      1. bcrypt
        1. bcrypt.compare
  3. Login
    1. 로그인 오류 메시지 출력
    2. 회원가입 성공여부 알려주기
      1. status code
    3. 쿠키
      1. 세션
        1. express-session
        2. MongoDB에 세션 저장
        3. locals 전역변수
      2. Mongo Store
      3. Session authentication(세션 인증) 문제
        1. Token authentication
      4. Url 비공개
        1. dotenv
    4. 깃허브로 로그인하기
      1. scope
        1. URL 정리하기
    5. 로그아웃

User

Model 생성

src/models/User.js

import mongoose from "mongoose";

const userSchema = new mongoose.Schema({
    username: { type: String, required: true, unique: true },
    password: { type: String, required: true },
});
const User = mongoose.model("User", userSchema);

export default User;

init.js에 User 모델 import

import "./models/User.js";

=> DB에게 알려주는 과정

form 생성

회원가입 form 작성 src/views/join.pug

    form(method="POST")
        input(name="username" type="text" placeholder="아이디" required)
        input(name="password" type="password" placeholder="비밀번호" required)
        input(name="nickname" type="text" placeholder="닉네임" required)
        input(name="email" type="email" placeholder="이메일" required)
        input(type="submit" value="가입하기")

form에서 받은 정보 post하기 src/controllers/userController.js

export const postJoin = async (req, res) => {
  const { username, password, nickname, email } = req.body;
  await User.create({
    username,
    password,
    nickname,
    email,
  });
  return res.redirect("login");
};

Password

Hashing

  • password를 그대로 저장하면 쉽게 노출되므로 hashing 해준다.
  • Hashing은 일방향 함수이다.
  • 입력값을 입력하면 출력값이 나오지만 출력값으로 입력값을 알아낼 수 없다.

bcrypt

  • rainbow table 공격을 막아준다. 🔧 npm install bcrypt

해싱하기

userSchema.pre("save", async function () {
  this.password = await bcrypt.hash(this.password, 5);
});

💡숫자 5: salt 횟수이다.

bcrypt.compare
  • 로그인 할 때 유저가 form에 입력한 숫자를 해싱한 값과
    회원가입시 해싱된 값을 비교하여 로그인 성공여부를 결정한다.
    const ok = await bcrypt.compare(password, user.password);
    

Login

로그인 오류 메시지 출력

const usernameExists = await User.exists({ username: username });
  if (usernameExists) {
    return res.render("join", { pageTitle: "Join", errorMessage: "이미 존재하는 아이디입니다." });
  }

회원가입 성공여부 알려주기

조건에 의해 회원가입이 실패하면 에러 메시지는 뜨지만
브라우저에서 성공한줄 알고 아이디와 암호를 저장하려고 한다.
그래서 상태 코드를 이용하여 성공 여부를 알려줌으로 해결한다.

status code

  • HTTP 응답 상태를 알려주는 코드이다.

HTTP 응답 상태 코드 목록

2xx : success 4xx : client errors

=> 200번대 상태코드를 받는다면 URL을 History에 저장하지만
400번대 상태코드를 받으면 URL을 저장하지 않는다.

if (password !== password2) {
    return res.status(400).render("join", { pageTitle, errorMessage: "패스워드가 일치하지 않습니다." });
  }

쿠키

  • 정보를 주고받는 방식이며 session ID를 저장하고 전송한다.

세션

  • backend와 browser간에 어떤 활동을 했는지 기억한다.
express-session
  • express에서 세션을 처리할 수 있게 해주는 미들웨어.

🔧 npm install express-session

server.js //Router 위에 추가

app.use(
  session({
    secret: "Hello!",
    resave: true,
    saveUninitialized: true,
  })
);

=> 위의 미들웨어가 사이트로 들어오는 것을 기억하게 된다.

💡 resave : 변경 사항이 없어도 저장 💡 saveUninitialized : 세션 초기화 전에도 저장

MongoDB에 세션 저장
export const postLogin = async (req, res) => {
  const { pageTitle } = "로그인";
  const { username, password } = req.body;
  const user = await User.findOne({ username });
  if (!user) {
    return res.status(400).render("login", { pageTitle, errorMessage: "아이디가 존재하지 않습니다." });
  }
  const ok = await bcrypt.compare(password, user.password);
  if (!ok) {
    return res.status(400).render("login", { pageTitle, errorMessage: "비밀번호가 일치하지 않습니다." });
  }
  req.session.loggedIn = true;
  req.session.user = user;
  res.redirect("/");
};
locals 전역변수
  • res.locals object를 이용하면 template에 전역적으로 변수를 보낼 수 있다.

middlewares.js 생성

export const localsMiddleware = (req, res, next) => {
  res.locals.loggedIn = Boolean(req.session.loggedIn);
  res.locals.siteName = "Ἀγορά";
  res.locals.loggedInUser = req.session.user;
  next();
};

server.js에서 middlewares.js 사용하기

app.set("view engine", "pug");
app.set("views", process.cwd() + "/src/views");
app.use(logger);
app.use(express.urlencoded({ extended: true }));
app.use(
  session({
    secret: "Hello!",
    resave: true,
    saveUninitialized: true,
  })
);
app.use(localsMiddleware);
app.use("/", globalRouter);
app.use("/users", userRouter);
app.use("/posts", postRouter);

base.pug에서 locals에 있는 전역변수 loggedIn 사용예시

if loggedIn
    li 
        a(href="/users/logout") 로그아웃
    li 
        a(href="/posts/upload") 글쓰기 
else
    li 
        a(href="/login") 로그인
    li 
        a(href="/join") 회원가입

⭐ 정리

  1. 브라우저(클라이언트)가 서버에 접근
  2. 서버가 브라우저에게 쿠키를 줌
  3. 세션에 쿠키와 관련된 정보를 저장
  4. 브라우저가 서버에 다시 접근할 때 쿠키를 보여줌
  5. 서버는 쿠키를 통해 브라우저를 구분

Mongo Store

  • ssession data를 저장하기 위해 사용한다.

connect-mongo에 대해 알아보기

💡💡💡


  • 쿠키 안에는 session ID만 저장되고 session data는 저장되지 않는다.
  • session data는 서버쪽에 저장된다.

🔧 npm install connect-mongo

app.use(
  session({
    secret: "Hello!",
    resave: true,
    saveUninitialized: true,
    store: mongoStore.create({ mongoUrl: "mongodb://127.0.0.1:27017/agora" }),
  })
);

Mongo DB

> show collections
posts
sessions
users

=> Mongo DB에서 sessions가 추가된 것을 확인할 수 있다.

생성된 세션 확인

> db.sessions.find()
{ "_id" : "Qa6Yf2mv3WBnm-PSS92vhHMSJITcz02G", "expires" : ISODate("2022-09-13T04:25:12.466Z"), "session" : "{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"}}" }

Session authentication(세션 인증) 문제

  • 모든 방문자에 대해 쿠키를 저장하는 문제

resave & saveUninitialized false로 변경

  • server.js
    app.use(
    session({
      secret: "Hello!",
      resave: false,
      saveUninitialized: false,
      cookie: {
        maxAge: 20000,
      },
      store: MongoStore.create({ mongoUrl: "mongodb://127.0.0.1:27017/agora" }),
    })
    );
    

    💡 secret : 쿠키에 sign할 때 사용하는 string //session hijack을 방지하기 위함이다. 💡 resave : 변경 사항이 없어도 저장 💡 saveUninitialized : 세션 초기화 전에도 저장 💡 maxAge: 쿠키를 유지할 시간을 정함 // 20000 -> 20초

    Token authentication
  • IOS or 안드로이드는 쿠키를 갖지 않기 때문에 token을 사용함

Url 비공개

environment file(환경변수) .env 생성

  • 코드에 들어가면 안되는 값들을 지정

  • /.env
    COOKIE_SECRET = [YOUR Cookie Secret]
    DB_URL = [YOUR DB URL]
    

    사용 방법

  • server.js
    app.use(
    session({
      secret: process.env.COOKIE_SECRET,
      resave: false,
      saveUninitialized: false,
      store: MongoStore.create({ mongoUrl: process.env.DB_URL }),
    })
    );
    

    => 그러나 지금은 COOKIE_SECRET과 DB_URL을 출력하면 undefined를 반환받는다.

⚠️ gitignore에도 .env 추가하기!!

dotenv

🔧 npm install dotenv

가능한 제일 먼저 실행하게 해준다.

  • init.js
    import "dotenv/config";
    

깃허브로 로그인하기

  1. Github-Settings-Oauth Apps-new OAuth App 클릭 2.
  2. login.pug에 github 로그인 주소 추가
    form(method="POST")
         input(name="username" type="text" placeholder="아이디" required)
         input(name="password" type="password" placeholder="비밀번호" required)
         input(type="submit" value="로그인")
         br
         a(href="https:/github.com/login/oauth/authorize?[Client ID]") Github로 로그인하기 →
    

scope

  • 유저에게 어떤 정보를 가져올 것인지 설정

email 정보만 가져오기

a(href="https:/github.com/login/oauth/authorize?[Client ID]&scope=user:email")

결과 => oauth-scope-email.png

URL 정리하기

github에서 준 code를 Access Token으로 바꿔준다.

⚠️ client_secret은 무조건 backend에 존재해야한다.

code는 10분이면 만료한다.

npm install node-fetch@2.6.1

로그아웃

export const logout = (req, res) => {
  req.session.destroy();
  res.redirect("/");
};