본문 바로가기
sns 프로젝트

회원가입, 로그인 기능 구현(JWT)

by 우주속공간 2024. 1. 21.

 

로그인 및 회원가입 기능 : JWT를 통해 회원가입, 로그인을 구현하고 쿠키에 토큰들을 담아서 API를 보낼때 쿠키가 유효한지 확인하는 미들웨어를 만들어서 보안성을 높였다. 

 

회원가입 구현


1️⃣ users 데이터 베이스 생성

2️⃣ 프론트 페이지에서 axios 통신으로 아이디, 비밀번호 데이터 값을 보내주면 users 데이터베이스에 저장

→ 이미 존재하는 아이디(이메일)인지 확인하는 작업 필요

→ 비밀번호 암호화 과정 추가(bcrypt)

-> 문자열 확인? 아이디, 비밀번호 양식 확인하는 코드 추가해야함

 

⬇️ register.ts

import express, { Request, Response, NextFunction } from "express";
import { Users } from "../models/user";
const bcrypt = require("bcrypt");

const router = express.Router();

router.post("/", async (req: Request, res: Response, next: NextFunction) => {
  const { email, password } = req.body;
  const emailExists = await Users.findOne({ where: { email: email } });
  if (emailExists) {
    return res.status(400).json({ message: "이미 사용중인 이메일입니다." });
  }
  const salt = await bcrypt.genSalt(10);
  const hashedPassword = await bcrypt.hash(password, salt);
  await Users.create({
    email: email,
    password: hashedPassword,
  }).then((result) => {
    res.status(201).json(result);
  });
});

module.exports = router;

bcrypt❓

데이터베이스에 유저 정보를 저장할때 비밀번호 데이터를 hasing해주는 라이브러리

 

 

로그인 구현


로그인 시도 → 유저와 일치하는 정보가 있으면 JWT 생성후 보냄 → 받은 access token,refresh token 브라우저 저장 → API 요청할때마다 access token도 보내져서 검사 후 정보 제공

 

로그인 기능을 구현하기 위해서 토큰을 어디에 저장할지 정하기 위해서 유저 인증 보안 취약점과 보통 어디에 저장하는지 찾아보았다. 

유저 인증 보안 취약점

  1. XSS 공격 : 해커가 클라이언트 브라우저에 Javascript 삽입해 실행하는 공격
  • <Input>을 통해 Javascript를 서버로 전송해 스크립트 실행
  • url에 Javascript를 적어서 클라이언트에 스크립트 실행

⇒ 사이트의 글로벌 변수값 또는 그 값을 이용해 API콜 실행

 

 

   2. CSRF 공격 : 다른 사이트에서 우리 사이트 API 콜을 요청해 실행하는 공격

 

링크를 클릭하면 사용자 모르게 사이트의 어떤 기능을 실행하는 것

API를 요청할 수 있는 클라이언트 도메인이 누구인지 서버에서 통제하고 있지 않으면 CSRF공격 가능.

⇒ 그래서 해커가 유저 정보를 서버에 보낼 수 있으면! 제대로 로그인한 것처럼 유저 정보 변경, 유저만 가능한 실행

ex) 은행사이트 : 로그인한 척 계좌 비밀번호를 바꾸거나 송금

 

토큰 저장 방식

보통은 local Storage, Cookie에 저장

 

1️⃣  localStorage 저장방식 : 브라우저 저장소에 저장하는 방식이다. Javascript 내 글로벌 변수로 읽기 / 쓰기 접근이 가능

⇒ XXS취약점이 있을때 local Storage 안에 담긴 값을 불러오거나, 불러온 값을 이용해 API콜을 위조할 수 있다.

 

2️⃣ 쿠키 저장 방식 : 브라우저에 쿠키로 저장되는데, 클라이언트가 HTTP 요청을 보낼 때마다 자동으로 쿠키가 서버에 전송됨. Javascript 내 글로벌 변수로 읽기 / 쓰기 접근이 가능

⇒ XXS취약점이 있을때 담긴 값을 불러오거나, 불러온 값을 이용해 API콜을 위조할 수 있다.

⇒ 인증 정보가 쿠키에 담겨 서버로 보내지기때문에 공격자는 유저 권한으로 정보를 가져오거나 액션을 수행할 수 있다.

  • 쿠키에 refreshToken만 저장하고 새로운 accessToken을 받아와 인증에 이용하는 구조에서는 CSRF 취약점 공격을 방어할 수 있다.

⇒ refreshToken으로 accessToken을 받아도 accessToken을 스크립트에 삽입할 수 없다면 accessToken을 사용해 유저 정보를 가져올 수 없기 때문이다.

  • httpOnly 쿠키 방식으로 저장된 정보는 XSS 취약점 공격으로 담긴 값을 불러올 수 없다. → 자바스크립트에서 접근 불가능

✔️secure(https), SameSite 적용해보기

 ✅ Access token : JSON payload로 보내줘서 웹 어플리케이션 로컬 변수로 저장

 

 ✅ Refresh token : httpOnly 쿠키 저장

 

⬇️ login.ts

import express, { Request, Response, NextFunction } from "express";
import { Users } from "../models/user";
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");

const router = express.Router();

router.post("/", async (req: Request, res: Response, next: NextFunction) => {
  const { email, password } = req.body;
  const generateAccessToken = (email: any) => {
    return jwt.sign({ email }, process.env.ACCESS_TOKEN_SECRET, {
      expiresIn: "15m",
    });
  };

  // refersh token을 secret key  기반으로 생성
  const generateRefreshToken = (email: any) => {
    return jwt.sign({ email }, process.env.REFRESH_TOKEN_SECRET, {
      expiresIn: "180 days",
    });
  };

  try {
    const user = await Users.findOne({
      where: { email: email },
    });

    if (!user) {
      return res.status(400).send("존재하지 않는 계정입니다.");
    }

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

    let accessToken = generateAccessToken(email);
    let refreshToken = generateRefreshToken(email);

    res
      .cookie("refreshToken", refreshToken, {
        secure: true,
        httpOnly: true,
      })
      .status(200)
      .json({ accessToken, message: "ok" });
  } catch (err: any) {
    return res.status(400).send({ err: err.message });
  }
});

module.exports = router;

⬇️ 프론트 페이지 로그인 코드

const onLogin = () => {
    axios
      .post("<http://localhost:1234/login>", {
        email: email,
        password: password,
      })
      .then((response) => {
        const { accessToken } = response.data;

        // API 요청하는 콜마다 헤더에 accessToken 담아 보내도록 설정
        axios.defaults.headers.common[
          "Authorization"
        ] = `Bearer ${accessToken}`;
        alert("로그인 성공");

        // accessToken을 localStorage, cookie 등에 저장하지 않는다!
      })
      .catch((error) => {
        // ... 에러 처리
      });

refreshToken으로 새로운 accessToken 발급 받기

import { Users } from "../models/user";
import express, { Request, Response, NextFunction } from "express";
const jwt = require("jsonwebtoken");
const router = express.Router();

router.get("/", async (req: Request, res: Response, next: NextFunction) => {
  const token = req.cookies.refreshToken;

  try {
    const data = jwt.verify(token, process.env.REFRESH_TOKEN_SECRET); // 쿠키에 담긴 정보를 해독한 값
    const userInfo = await Users.findOne({ where: { email: data.email } });

    if (!data) {
      // 유효하지 않거나, 해독이 불가한 토큰인 경우
      return res.json({
        data: null,
        message: "invalid refresh token, please log in again",
      });
    }
    if (userInfo) {
      const payload = {
        email: data.email,
      };
      const accessToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, {
        expiresIn: "15m",
      });
      return res.json({
        accessToken,

        message: "ok",
      });
    }
    return res.json({
      data: null,
      message: "refresh token has been tempered",
    });
  } catch (err) {
    // 유효하지 않거나, 해독이 불가한 토큰인 경우
    return res.json({
      data: null,
      message: "invalid refresh token, please log in again",
    });
  }

  
});

module.exports = router;

 

<수정해야될 것>

토큰 검증 미들웨어,로그아웃(clearCookie), 아이디별 트윗 보여주기(filter)

✔️트위터 박스에 적었을때 → refreshToken 존재하는지 확인 ⇒ onChange로 확인. 한글자 적을때마다 확인해야함. 수정

tweet input 박스 onChange

✔️API 요청할때 accessToken 유효성 검사 받고 싶은데 아직 못함.

✔️ api 요청 - accessToken 유효성 검사 필요

✔️ 리로드 되었을때.refresh token으로 accessToken새로 발급 - refreshTokenRequest

✔️ refreshToken 만료되었을때 로그인 페이지로 이동