캡스톤 디자인 프로젝트 -맛집 정보 사이트 만들기- (4) 백엔드 회원 기능

2024. 12. 3. 01:21프로젝트

728x90
반응형

우선 로그인 기능을 이용해서 접근 제어를 할 수있는 방법은 세션 기능과 JWT토큰을 이용하는 방법들이 있습니다.

 

1. 세션 기반 인증이란?

작동 방식

사용자가 로그인하면 서버는 세션을 생성하고, 세션 ID를 클라이언트의 쿠키에 저장합니다.
이후 요청마다 클라이언트는 이 세션 ID를 서버에 보냅니다.
서버는 세션 ID를 확인해 사용자를 인증합니다.

흐름

  1. 로그인 요청
    사용자가 ID와 비밀번호를 서버에 보냅니다.
  2. 서버에서 세션 생성
    서버는 사용자가 맞는지 확인한 후, 고유한 세션 ID를 생성하고 이를 저장합니다.
    (예: Session ID: abc123)
  3. 세션 ID를 클라이언트에 전달
    클라이언트는 서버로부터 받은 세션 ID를 쿠키에 저장합니다.
  4. 인증된 요청
    클라이언트는 요청을 보낼 때마다 쿠키에 저장된 세션 ID를 서버에 보냅니다.
  5. 서버에서 세션 확인
    서버는 세션 ID를 확인하고, 저장된 세션 정보(사용자 ID 등)로 인증을 처리합니다.
  6. 로그아웃
    서버에서 해당 세션을 삭제하면 사용자는 로그아웃됩니다.

2. JWT 기반 인증이란?

작동 방식

사용자가 로그인하면 서버는 JWT를 생성해 클라이언트에 전달합니다.
클라이언트는 JWT를 쿠키나 로컬스토리지에 저장합니다.
이후 요청마다 클라이언트는 이 JWT를 서버로 전송합니다.
서버는 JWT의 서명을 확인해 사용자를 인증합니다.

흐름

  1. 로그인 요청
    사용자가 ID와 비밀번호를 서버에 보냅니다.
  2. 서버에서 JWT 생성
    서버는 사용자가 맞는지 확인한 후, 사용자 정보를 포함한 JWT 토큰을 생성합니다.
  3. JWT를 클라이언트에 전달
    클라이언트는 서버로부터 받은 JWT를 로컬스토리지쿠키에 저장합니다.
  4. 인증된 요청
    클라이언트는 요청을 보낼 때마다 JWT를 HTTP 헤더에 포함시켜 서버에 보냅니다.
  5. 서버에서 JWT 확인
    서버는 JWT의 서명을 확인하고, 유효하면 사용자 정보를 인증합니다.
  6. 로그아웃
    클라이언트에서 JWT를 삭제하면 로그아웃됩니다.
    (서버는 JWT 자체를 저장하지 않기 때문에 강제 만료가 어려움.)

 

우선 회원가입부터 살펴보겠습니다.

https://solapi.com/

 

SOLAPI - 알림톡과 문자메시지 발송, CRM 자동화 솔루션

CRM을 통해 고객에게 더 나은 서비스를 제공하고 비즈니스의 성장을 이룰 수 있는 방법을 소개합니다. 대량문자, 카카오 알림톡 친구톡, 문자 API연동, 앱발송, 웹발송, CRM자동화, 보이스톡

solapi.com

우리는 회원가입에서 전화번호 인증을 위해서 솔라피api를 이용하여 전화번호인증을하면 랜덤숫자를 통해 인증을 해주는걸로 하였습니다.

npm install solapi

 

를 설치를 해줍니다.

 

solapi를 통한 인증번호

import pkg from "solapi"; // solapi 모듈 전체를 임포트
import dotenv from "dotenv";
dotenv.config();

const { SolapiMessageService } = pkg; // 필요한 서비스만 가져옴

// SOLAPI 초기화
const messageService = new SolapiMessageService(
  process.env.SOLAPI_API_KEY, // API Key
  process.env.SOLAPI_API_SECRET // Secret Key
);
// 전화번호 인증코드 발송
const sendVerificationCode = async (req, res) => {
  const { phone_number } = req.body;

  // 전화번호 확인
  if (!phone_number) {
    return res.status(400).json({
      resultCode: "F-1",
      msg: "전화번호가 누락되었습니다.",
    });
  }

  // 6자리 인증코드 생성
  const verificationCode = Math.floor(
    100000 + Math.random() * 900000
  ).toString();

  try {
    const response = await messageService.send({
      to: phone_number,
      from: "010-2848-5397",
      text: `인증코드: ${verificationCode}`,
    });

    console.log("CoolSMS API Response:", response); // CoolSMS 응답 출력

    if (response.error_list && response.error_list.length > 0) {
      console.error("CoolSMS API Error:", response.error_list); // 에러 리스트 출력
      return res.status(500).json({
        resultCode: "F-2",
        msg: "SMS 전송 중 에러가 발생했습니다.",
        error: response.error_list,
      });
    }

    return res.status(200).json({
      resultCode: "S-1",
      msg: "인증코드가 성공적으로 전송되었습니다.",
      verificationCode,
    });
  } catch (error) {
    console.error("Unexpected Error:", error); // 예외 처리
    return res.status(500).json({
      resultCode: "F-1",
      msg: "서버 에러 발생",
      error: error.message,
    });
  }
};

 

클라이언트에서 전화번호를 입력하면

const { phone_number } = req.body;

 

통해서 전화번호가 받아와지고

const messageService = new SolapiMessageService(
  process.env.SOLAPI_API_KEY, // API Key
  process.env.SOLAPI_API_SECRET // Secret Key
);

 

를 통해서 api 를 통해서

try {
    const response = await messageService.send({
      to: phone_number,
      from: "발송인",
      text: `인증코드: ${verificationCode}`,
    });
    
    // 6자리 인증코드 생성
  const verificationCode = Math.floor(
    100000 + Math.random() * 900000
  ).toString();

 

인증 코드가 랜덤으로 전송되고 

 

// 인증코드 검증
const verifyCode = (req, res) => {
  const { verificationCode, inputCode } = req.body;

  console.log("Stored code:", verificationCode); // 서버에서 저장된 인증코드
  console.log("User input code:", inputCode); // 사용자가 입력한 인증코드

  if (verificationCode !== inputCode) {
    return res.status(400).json({
      resultCode: "F-1",
      msg: "인증코드가 일치하지 않습니다.",
    });
  }

  return res.status(200).json({
    resultCode: "S-1",
    msg: "인증이 완료되었습니다.",
  });
};

 

위에 코드를 통해서 

인증코드 검증을 해줄수 있습니다.

 

회원가입 기능

/* 사용자 회원가입 */
const register = async (req, res) => {
  try {
    const { username, password, email, full_name, phone_number } = req.body;

    // 입력 값 검증
    if (!username || !password || !email) {
      return res.status(400).json({
        resultCode: "F-1",
        msg: "필수 입력 값이 누락되었습니다.",
      });
    }

    // 사용자 이름 중복 체크
    const { rows: existingUsers } = await pool.query(
      `SELECT id FROM users WHERE username = $1 OR email = $2`,
      [username, email]
    );

    if (existingUsers.length > 0) {
      return res.status(400).json({
        resultCode: "F-2",
        msg: "이미 사용 중인 사용자 이름 또는 이메일입니다.",
      });
    }

    console.log("Received Data:", req.body); // 입력 값 확인을 위한 로그

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

    // 사용자 생성
    const { rows } = await pool.query(
      `
        INSERT INTO users (username, password, email, full_name, phone_number) 
        VALUES ($1, $2, $3, $4, $5)
        RETURNING id, username, email, full_name, phone_number, created_at
      `,
      [username, hashedPassword, email, full_name, phone_number]
    );

    const newUser = rows[0];

    res.status(201).json({
      resultCode: "S-1",
      msg: "사용자 등록 성공",
      data: newUser,
    });
  } catch (error) {
    console.error("Error registering user:", error);
    res.status(500).json({
      resultCode: "F-1",
      msg: "서버 에러 발생",
      error: error.message,
    });
  }
};

 

위에 코드는 클라이언트쪽에서

const { username, password, email, full_name, phone_number } = req.body;

를 받아오고

// 입력 값 검증
    if (!username || !password || !email) {
      return res.status(400).json({
        resultCode: "F-1",
        msg: "필수 입력 값이 누락되었습니다.",
      });
    }

    // 사용자 이름 중복 체크
    const { rows: existingUsers } = await pool.query(
      `SELECT id FROM users WHERE username = $1 OR email = $2`,
      [username, email]
    );

필수로 입력되어야 하는 값들과 이름과 이메일을 중복체크를 해줍니다.

 

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

비밀번호는 안정적이게 bcrypt를 통해 해싱암호화를 해줍니다.

 

const { rows } = await pool.query(
  `
    INSERT INTO users (username, password, email, full_name, phone_number) 
    VALUES ($1, $2, $3, $4, $5)
    RETURNING id, username, email, full_name, phone_number, created_at
  `,
  [username, hashedPassword, email, full_name, phone_number]
);

 

쿼리문을 통해 테이블에 넣어주도록 합니다.

 

로그인 기능

const login = async (req, res) => {
  try {
    const { username, password } = req.body;

    const { rows } = await pool.query(
      `
        SELECT id, username, password, email, full_name, phone_number, created_at
        FROM users
        WHERE username = $1
      `,
      [username]
    );

    if (rows.length === 0) {
      return res.status(401).json({
        resultCode: "F-2",
        msg: "존재하지 않는 아이디입니다.",
      });
    }

    const user = rows[0];
    const match = await bcrypt.compare(password, user.password);

    if (!match) {
      return res.status(401).json({
        resultCode: "F-2",
        msg: "아이디 또는 비밀번호가 일치하지 않습니다.",
      });
    }

    req.session.userId = user.id;
    req.session.username = user.username;

    req.session.save((err) => {
      if (err) {
        console.error("세션 저장 중 에러 발생:", err);
        return res.status(500).json({
          resultCode: "F-1",
          msg: "세션 저장 중 에러 발생",
        });
      }

      // 세션 저장 성공 후 응답
      res.json({
        resultCode: "S-1",
        msg: "로그인 성공",
        data: {
          id: user.id,
          username: user.username,
          email: user.email,
          fullName: user.full_name,
          phone_number: user.phone_number,
          created_at: user.created_at,
        },
        sessionId: req.sessionID,
      });
    });
  } catch (error) {
    res.status(500).json({
      resultCode: "F-1",
      msg: "서버 에러 발생",
      error: error.message,
    });
  }
};

 

로그인 기능은 

const { username, password } = req.body;

을 통해 사용자가 입력한 유저 아이디와 비밀번호를 받아와서

 

const { rows } = await pool.query(
  `
    SELECT id, username, password, email, full_name, phone_number, created_at
    FROM users
    WHERE username = $1
  `,
  [username]
);

if (rows.length === 0) {
  return res.status(401).json({
    resultCode: "F-2",
    msg: "존재하지 않는 아이디입니다.",
  });
}

 

쿼리를 통해서 유저아이디를 찾도록 탐색합니다.

 

const user = rows[0];
const match = await bcrypt.compare(password, user.password);

if (!match) {
  return res.status(401).json({
    resultCode: "F-2",
    msg: "아이디 또는 비밀번호가 일치하지 않습니다.",
  });
}

 

데이터베이스에서 가져온 사용자 정보(user)에서 password 필드를 확인하고
bcrypt.compare: 사용자가 입력한 비밀번호와 데이터베이스에 저장된 해싱된 비밀번호를 비교.
결과: 비밀번호가 일치하지 않을 경우, HTTP 상태 코드 401과 에러 메시지 반환.

 

req.session.userId = user.id;
    req.session.username = user.username;

    req.session.save((err) => {
      if (err) {
        console.error("세션 저장 중 에러 발생:", err);
        return res.status(500).json({
          resultCode: "F-1",
          msg: "세션 저장 중 에러 발생",
        });
      }

      // 세션 저장 성공 후 응답
      res.json({
        resultCode: "S-1",
        msg: "로그인 성공",
        data: {
          id: user.id,
          username: user.username,
          email: user.email,
          fullName: user.full_name,
          phone_number: user.phone_number,
          created_at: user.created_at,
        },
        sessionId: req.sessionID,
      });
    });
  } catch (error) {
    res.status(500).json({
      resultCode: "F-1",
      msg: "서버 에러 발생",
      error: error.message,
    });
  }
};

 

 

req.session:Express.js에서 제공하는 세션 객체로, 현재 사용자에 대한 정보를 저장

 

저장 내용:userId: 데이터베이스에서 가져온 사용자 고유

 

 

 

 

req.session.save():세션 정보를 저장하고, 저장이 성공했는지 확인합니다.세션 저장은 비동기로 이루어지며,

콜백 함수로 성공/실패를 처리.

 

오류 처리:저장 중 에러가 발생하면 서버는 HTTP 상태 코드 500을 반환.

오류 메시지는 resultCode와 함께 클라이언트로 전송.

 

 

 

로그아웃 기능

const logout = (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({
        resultCode: "F-1",
        msg: "로그아웃 중 에러 발생",
      });
    }

    res.clearCookie("connect.sid", {
      path: "/",
      secure: process.env.NODE_ENV === "production", // HTTPS 환경에서만 secure 쿠키 허용
      sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", // 개발 환경에서는 lax, 배포 환경에서는 none
      maxAge: 0, // 쿠키 즉시 만료
    });

    console.log("세션이 성공적으로 파괴되었습니다.");
    res.json({
      resultCode: "S-1",
      msg: "로그아웃 성공",
    });
  });
};

 

 

req.session.destroy((err) => {
  if (err) {
    return res.status(500).json({
      resultCode: "F-1",
      msg: "로그아웃 중 에러 발생",
    });
  }

 

 

req.session.destroy():

  • 현재 사용자의 세션 데이터를 삭제.
  • 세션 스토리지에 저장된 해당 세션 데이터를 제거.
res.clearCookie("connect.sid", {
  path: "/",
  secure: process.env.NODE_ENV === "production", // HTTPS 환경에서만 secure 쿠키 허용
  sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", // 개발 환경에서는 none, 개발 중에는 lax
  maxAge: 0, // 즉시 만료
});

 

 

res.clearCookie():

  • 클라이언트에 저장된 특정 쿠키를 제거.
  • 여기서는 connect.sid라는 이름의 쿠키를 삭제.

옵션 설명:

  • path:
    • 쿠키를 삭제할 경로. 기본적으로 세션 쿠키가 설정된 경로(/)를 지정.
  • secure:
    • HTTPS 환경에서만 쿠키를 전송하도록 설정.
    • NODE_ENV 환경 변수를 이용해 배포 환경과 개발 환경을 구분.
  • sameSite:
    • 쿠키가 동일한 사이트 내 요청에서만 전송되도록 제어.
    • none은 배포 환경, lax는 개발 환경에서 설정.
  • maxAge:
    • 쿠키의 유효 시간을 0으로 설정하여 즉시 만료.

 

 

세션 상태 확인

const checkSession = async (req, res) => {
  console.log("세션 데이터 확인:", req.session); // 세션 데이터 확인

  if (req.session && req.session.userId) {
    try {
      // 데이터베이스에서 사용자 정보 조회
      const { rows } = await pool.query(
        `
          SELECT id, username, email, full_name, phone_number, created_at
          FROM users
          WHERE id = $1
        `,
        [req.session.userId]
      );

      if (rows.length === 0) {
        return res.status(401).json({
          resultCode: "F-2",
          msg: "세션이 만료되었거나 유효하지 않습니다.",
          isAuthenticated: false,
        });
      }

      const user = rows[0];

      console.log("세션 유효함. 사용자 ID:", req.session.userId);
      return res.status(200).json({
        resultCode: "S-1",
        msg: "세션이 유효합니다.",
        isAuthenticated: true,
        user: {
          id: user.id,
          username: user.username,
          email: user.email,
          full_name: user.full_name,
          phone_number: user.phone_number,
          created_at: user.created_at,
        },
      });
    } catch (error) {
      console.error("세션 확인 중 에러 발생:", error);
      return res.status(500).json({
        resultCode: "F-1",
        msg: "서버 에러 발생",
        isAuthenticated: false,
      });
    }
  } else {
    return res.status(401).json({
      resultCode: "F-2",
      msg: "세션이 만료되었거나 유효하지 않습니다.",
      isAuthenticated: false,
    });
  }
};

 

 

console.log("세션 데이터 확인:", req.session); // 세션 데이터 확인

if (req.session && req.session.userId) {

 

req.session:

  • Express.js 세션 객체로, 현재 사용자와 관련된 세션 데이터를 포함.

eq.session.userId:

  • 로그인 시 저장된 사용자 ID. 이 값이 존재하면 세션이 유효한 것으로 간주.

처리:

  • req.session 또는 req.session.userId가 없으면 클라이언트에 401 상태 코드와 오류 메시지를 반환

 

const { rows } = await pool.query(
  `
    SELECT id, username, email, full_name, phone_number, created_at
    FROM users
    WHERE id = $1
  `,
  [req.session.userId]
);

 

 

목적:

  • 세션에 저장된 userId를 이용해 데이터베이스에서 사용자의 상세 정보를 조회.
  • 쿼리 결과:
    • rows 배열에 조회된 데이터가 저장되며, rows[0]은 사용자 객체를 나타냄.
  • 조건:
    • rows.length === 0일 경우: 세션의 userId가 더 이상 유효하지 않은 경우로 간주하고, 401 응답 반환.