강의

멘토링

로드맵

Inflearn brand logo image

인프런 커뮤니티 질문&답변

jasan88님의 프로필 이미지
jasan88

작성한 질문수

React, Node.js, MongoDB로 만드는 나만의 회사 웹사이트: 완벽 가이드

Ch4-6. 관리자 계정 로그아웃, 삭제

ch4-6 관리자 계정 로그아웃 , 삭제 관련

해결된 질문

작성

·

37

0

7:27 시점에서, 터미널에서 선생님은,,

eyJhbGcioiJOUzI1N,......주소명이 뜹니다만,,,

저의 경우, 아래와 같이 몽고DB에 연결이 되었습니다만 뜹니다....

 

이 경우 어떻게 해야할가요...

 

[nodemon] starting node index.js

[dotenv@17.2.1] injecting env (2) from .env -- tip: 🔐 prevent committing .env to code: https://dotenvx.com/precommit

Server is running

MongoDB와 연결이 되었습니다.

[nodemon] restarting due to changes...

[nodemon] starting node index.js

[nodemon] restarting due to changes...

[nodemon] starting node index.js

[dotenv@17.2.1] injecting env (2) from .env -- tip: suppress all logs with { quiet: true }

Server is running

MongoDB와 연결이 되었습니다.

 

image.png

 

아래는 routes폴더에 있는 user.js

const express = require("express");
const router = express.Router();
const bcrypt = require("bcrypt");
const User = require("../models/User");
const axios = require("axios");
const jwt = require("jsonwebtoken");

router.post("/signup", async (req, res) => {
  try {
    const { username, password } = req.body;

    const existingUser = await User.findOne({ username });
    if (existingUser) {
      return res.status(400).json({ message: "이미 존재하는 사용자입니다." });
    }

    const hashedPassword = await bcrypt.hash(password, 10);

    const user = new User({
      username,
      password: hashedPassword,
    });

    await user.save();
    res.status(201).json({ message: "회원가입이 완료되었습니다." });
  } catch (error) {
    res.status(500).json({ message: "서버 오류가 발생했습니다." });
    console.log(error);
  }
});

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

    const user = await User.findOne({ username }).select("+password");

    if(!user) {
      return res.status("401").json({message: "사용자를 찾을 수 없습니다."});
    }

    if(!user.isActive){
      return res
      .status(401)
      .json({ message: "비활성화된 계정입니다. 관리자에게 문의 주세요."});
    }

    if(user.isLoggedIn){
      return res
      .status(401)
      .json({message: "이미 다른 기기에서 로그인되어 있습니다."});
    }

    const isValidPassword = await bcrypt.compare(password, user.password);
    if(!isValidPassword){
      user.failedLoginAttempts += 1;
      user.lastLoginAttempt = new Date();

      if(user.failedLoginAttempts >= 5){
        user.isActive = false;
        await user.save();
        return res.status(401).json({
          message: "비밀번호를 5회이상 틀려 계정이 비활성화되었습니다.",
        });
      }
    await user.save();
      return res.status(401).json({
        message: "비밀번호가 일치하지 않습니다.",
        remainingAttempts: 5 - user.failedLoginAttempts,
      });
    }

    user.failedLoginAttempts = 0;
    user.lastLoginAttempt = new Date();
    user.isLoggedIn = true;

     try {
      const response = await axios.get("https://api.ipify.org?format=json");
      const ipAddress = response.data.ip;
      user.ipAddress = ipAddress;
    } catch (ipError) {
      console.error("IP 주소를 가져오는 중 오류 발생:", ipError.message);
    }
    await user.save();

     const token = jwt.sign(
      { userId: user._id, username: user.username },
      process.env.JWT_SECRET,
      { expiresIn: "24h" }
    );

    res.cookie("token", token, {
      httpOnly: true,
      secure: "production",
      sameSite: "strict",
      maxAge: 24 * 60 * 60 * 1000,
    });
    
   const userWithoutPassword = user.toObject();
    delete userWithoutPassword.password;

    res.json({ user: userWithoutPassword });
  } catch (error) {
    console.error("서버 오류:", error.message);
    res.status(500).json({ message: "서버 오류가 발생했습니다." });
  }
});


router.post("/logout", async (req, res) => {
  try {
    const token = req.cookies.token;

    if (!token) {
      return res.status(400).json({ message: "이미 로그아웃된 상태입니다." });
    }

    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      const user = await User.findById(decoded.userId);

      if (user) {
        user.isLoggedIn = false;
        await user.save();
      }
    } catch (error) {
      console.log("토큰 검증 오류: ", error.message);
    }

    res.clearCookie("token", {
      httpOnly: true,
      secure: "production",
      sameSite: "strict",
    });

    res.json({ message: "로그아웃되었습니다." });
  } catch (error) {
    console.log("로그아웃 오류: ", error.message);
    res.status(500).json({ message: "서버 오류가 발생했습니다." });
  }
});

router.delete("/delete/:userId", async (req, res) => {
  try {
    const user = await User.findByIdAndDelete(req.params.userId);
    if (!user) {
      return res.status(404).json({ message: "사용자를 찾을 수 없습니다." });
    }
    res.json({ message: "사용자가 성공적으로 삭제되었습니다." });
  } catch (error) {
    res.status(500).json({ message: "서버 오류가 발생했습니다." });
  }
});

module.exports = router;

 

env.에 표기한 부분

MONGO_URI=mongodb+srv://sungwon5623:cho121101!@sungwon.oirqw5d.mongodb.net/?retryWrites=true&w=majority&appName=Sungwon

JWT_SECRET=c21b6ba5372fa2b8

 

models폴더에 있는 User.js

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema(
  {
    username: {
      type: String,
      required: true,
      trim: true,
      minlength: 2,
      maxlength: 30,
    },
    password: {
      type: String,
      required: true,
      select: false,
    },
    isLoggedIn: {
      type: Boolean,
      default: false,
    },
    isActive: {
      type: Boolean,
      default: true,
    },
    failedLoginAttempts: {
      type: Number,
      default: 0,
    },
    lastLoginAttempt: {
      type: Date,
    },
    ipAddress: {
      type: String,
      trim: true,
    },
    createdAt: {
      type: Date,
      default: Date.now,
    },
  },
  {
    timestamps: true,
  }
);

const User = mongoose.model("User", userSchema);

module.exports = User;

답변 2

0

닭강정님의 프로필 이미지
닭강정
지식공유자

안녕하세요. 질문 주셔서 감사합니다!

먼저, 코드에 MongoDB 연결 주소가 노출되어 있습니다. 보안을 위해 해당 정보는 즉시 삭제해주세요!

말씀하신 ey~~~로 시작하는 문자열은 jsonwebtoken에서 발급된 인코딩 값입니다.
답변에 앞서, 먼저 jsonwebtoken에 대해 간단히 설명드리면, 크게 두 가지 기능으로 나눌 수 있습니다.

  • 인코딩(발급): 내가 원하는 정보(예: 사용자 ID, 이름 등)를 토큰에 담아 하나의 긴 문자열로 변환

  • 디코딩(검증): 그 긴 문자열을 다시 풀어서 원래 담았던 정보로 복원

사용자 ID, 이름뿐만 아니라 모든 문자열 데이터는 인코딩할 수 있습니다. 다만 중요한 점은, 토큰 발급 시 저희가 설정한 JWT_SECRET 값을 사용해 서명한다는 것입니다. 그렇기에 실제 운영 중에 JWT_SECRET 값이 유출이 되면 공격자가 언제든지 암호화된 문자열을 디코딩이 가능하기 떄문에 각별한 주의가 필요합니다.


로그인 엔드포인트를 작성하셨다면, userIdusername을 인코딩하여 토큰 값을 발급받았던 과정을 기억하실 겁니다. 강의 영상 6분 34초에 jsonwebtoken 및 JWT_SECRET 값을 통해 인코딩 된 토큰 값을 출력하는 코드가 있습니다.

const token = jwt.sign(
  { userId: user._id, username: user.username },
  process.env.JWT_SECRET,
  { expiresIn: "24h" }
);

console.log(token)

해당 코드를 참고하시면서 개발을 진행하시면 됩니다. 감사합니다!

0

안녕하세요, 인프런 AI 인턴입니다. jasan88님께서 강의 중 로그아웃 처리 및 토큰과 관련된 문제를 겪고 계신 것 같습니다. 제공해주신 코드를 참조하여 몇 가지 해결 방법을 안내드리겠습니다.

로그아웃 과정 및 토큰 문제 해결

  1. JWT 토큰 생성 및 활용

    • 사용자가 로그인을 시도하면, 정상적인 경우 토큰이 발급되어 로그인 상태를 유지합니다. 그러나 토큰이 정상적으로 출력되지 않는다면, 로그인 과정에서 JWT 토큰의 생성 및 반환이 제대로 수행되는지 확인해야 합니다.
    • 터미널 출력에서 토큰 정보가 나타나지 않는다면, 로그인이 완료되기 전에 문제 발생 가능성이 있습니다. 예를 들면 jwt.sign() 호출 시 필요한 환경변수(JWT_SECRET)가 설정되어 있는지 점검해 보세요.
  2. 토큰과 로그아웃

    • 로그아웃 요청 시 쿠키에 저장된 토큰이 클라이언트로부터 제대로 전달되고 있는지 확인해 보셔야 합니다. 쿠키가 HTTP 전송 과정에서 손상되거나 JavaScript 설정에서 접근 불가능하게 되어 있을 수도 있습니다.
    • res.clearCookie("token") 코드로 해결될 수 있고, 쿠키 설정의 secure, sameSite 옵션이 환경에 맞게 잘 설정되어 있는지도 확인해 보세요.
  3. 서버 로그 확인

    • 서버 로그에 표시된 오류 메시지는 문제 해결의 단서가 될 수 있습니다. console.log를 사용하여 문제 발생 시 로그를 남기고, 어떤 단계에서 작동이 중지되는지 추적해보세요. 특히, 서버 쪽에서 환경 변수나 토큰 생성에서 오류가 발생했을 경우 명확한 오류 메시지가 표시되도록 합니다.

참고 자료

비슷한 문제가 발생했을 때 어떤 방식으로 접근하면 좋을지 기존에 해결된 사례를 공유드립니다.

위의 URL에서 상세한 방법을 확인하시고 문제 해결에 도움이 되길 바랍니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

jasan88님의 프로필 이미지
jasan88

작성한 질문수

질문하기