안녕하세요

[Node.js] 회원 기능 구현하기 본문

데이터시각화-KMG/Node.js

[Node.js] 회원 기능 구현하기

sakuraop 2023. 7. 24. 21:49

회원가입, 로그인, 로그아웃, 회원탈퇴 이렇게 네가지를 구현합니다.

// 회원 가입
app.post("/api/users/create", async (req, res) => {
});

// 로그인
app.post("/api/users/login", async (req, res) => {
});

//로그아웃
app.post("/api/protected/users/signout", async (req, res) => {
});

//회원 탈퇴
app.delete("/api/protected/users/:userId/withdraw", async (req, res) => {
});

회원가입

구현 방법

  • DB에 저장된 user와 일치하는 id 또는 nickname이 존재하는지 확인합니다.
  • 중복되는 유저가 존재하지 않는다면 새로운 user를 DB에 저장합니다.

1) user collection 생성

// user Schema를 정의합니다.
const userSchema = new kmgDB.Schema({
  userId: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  nickname: { type: String, required: true, unique: true },
  refreshToken: { type: String },
  accessToken: { type: String },
  tokenExpiresAt: { type: Date, index: { expireAfterSeconds: 0 } },
});
  • schema의 unique 속성을 이용하여 중복된 userId또는 nickname이 존재한다면 생성을 할 수 없습니다.

 

2) 전달받은 데이터를 DB에 저장

// 회원 가입
app.post("/api/users/create", async (req, res) => {
  try {
    const { userId, password, nickname } = req.body;

    const hashedPassword = await hashPassword(password);
    const newUser = new User({ userId, password: hashedPassword, nickname });
    await newUser.save();

    // 회원 가입 성공
    res.status(201).json({ message: "회원 가입이 완료되었습니다." });
  • 유저의 패스워드를 해쉬화하여 보안을 강화합니다.
  • 새로운 유저를 DB에 저장합니다.
  • 중복된 userId 또는 nickname이 없다면 회원가입 완료 response를 합니다.

3) 중복된 userId 또는 nickname이 존재할 경우

  } catch (error) {
    if (error.code === 11000) {
      // 중복된 아이디 또는 닉네임 오류
      const key = Object.keys(error.keyValue)[0];
      const value = error.keyValue[key];
      res.status(409).json({ error: `${key} '${value}'는 이미 사용 중입니다.` });
    } else {
      // 이외의 예상치 못한 오류
      console.error(error);
      res.status(500).json({ error: "회원 가입 중 오류가 발생했습니다." });
    }
  }
});
  • unique여야 하는 필드에 중복된 값이 존재한다면 error를 통해서 확인할 수 있습니다.
  • 중복된 값으로 인해 회원가입에 실패하였다는 response를 합니다.

로그인 + 자동로그인

구현 방법

  • 전달받은 userId와 password가 DB에 존재하는 user 데이터와 일치하는지 확인합니다.
  • 존재하지 않는 userId 또는 일치하지 않는 password의 경우 에러를 response 합니다.
  • 문제가 없다면 refreshToken을 user 정보에 추가하고, accessToken을 발급합니다.

1) 전달받은 데이터가 DB의 user 데이터와 일치하는지 확인

// 로그인
app.post("/api/users/login", async (req, res) => {
  try {
    // user 데이터 확인
    const { userId, password, isRememberMe } = req.body;
    const userData = await User.findOne({ userId });

    // 존재하지 않는 userId
    if (!userData) {
      return res.status(401).json({ error: "존재하지 않는 ID입니다." });
    }

    // 일치하지 않는 password
    const isMatchedPassword = await bcrypt.compare(password, userData.password);
    if (!isMatchedPassword) {
      return res.status(401).json({ error: "비밀번호가 일치하지 않습니다." });
    }
  • userId가 DB에 존재하지 않는 경우의 response를 합니다.
  • userId가 일치하는 user가 존재하나, password가 일치하지 않는 경우의 response를 합니다.

2) 일치하는 데이터를 받은 경우에는 refeshToken을 DB에 저장하고 accessToken을 발급

    // 로그인 성공
    const refreshToken = jwt.sign({ userId }, accessTokenSecretKey, {
      expiresIn: "7d",
      issuer: "young",
    });
    await User.updateOne(
      { userId },
      { $set: { refreshToken, tokenExpiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) } }
    );

      // accessToken 발급
      const accessToken = createAccessToken(userId, accessTokenSecretKey);
      return res.status(200).json({
        message: "로그인되었습니다.",
        userId,
        accessToken,
        nickname: userData.nickname,
      });
    }
  }


// accessToken 발급 함수
const EXPIRATION_TIME = "10s";
exports.createAccessToken = (userId, accessTokenSecretKey) => {
  const expiresIn = EXPIRATION_TIME;
  return jwt.sign({ userId }, accessTokenSecretKey, {
    expiresIn,
    issuer: "issuerName",
  });
};
  • refeshToken은 DB에 저장하여 외부에서 접근할 수 없도록 합니다.
  • 로그인을 한 뒤 제한된 작업을 수행할 수 있도록 accessToken와 user 정보를 response합니다.

3) 자동로그인을 구현하기 위해서 accessToken을 cookie에 저장

    // 자동로그인 정보를 포함하여 전달 받기
    const { userId, password, isRememberMe } = req.body;

      // 자동 로그인에 체크한 경우 쿠키에 accessToken 저장
      if (isRememberMe) {
        console.log(`자동 로그인: accessToken 쿠키에 저장 userId - ${userId}`);
        res.cookie("accessToken", accessToken);
      }

      console.log(`로그인 성공: userId - [${userId}]`);
      return res.status(200).json({
        message: "로그인되었습니다.",
        userId,
        accessToken,
        nickname: userData.nickname,
      });
  • 자동로그인을 구현하기 위해서는 accessToken을 localStorage 또는 (여기서는)cookie에 저장합니다.

4) 클라이언트가 방문할 때 cookie에 저장된 accessToken을 넘겨받도록 합니다. 

// 로그인
app.post("/api/users/login", async (req, res) => {
  try {
    // accessToken 검사
    const requestedAccessToken = getTokenFromCookie(req, res, "accessToken");
    const isValidAccessToken = verifyToken(requestedAccessToken, accessTokenSecretKey);
    
    // DB에서 user 데이터 확인
    const decodedTokenData = jwt.decode(requestedAccessToken, accessTokenSecretKey);
    const requestedUserId = decodedTokenData.userId;
    const requestedUser = await User.findOne({ userId: requestedUserId });
    
    // refreshToekn 검사
    const isValidRefreshToken = verifyToken(requestedUser.refreshToken, accessTokenSecretKey);
    
    // 자동 로그인 정보 저장
    const isRememberMeBefore = decodedTokenData.isRememberMe;
  • cookie로부터 전달받은 accessToken을 디코드하여 user 데이터를 읽습니다.
  • DB에 일치하는 user 데이터를 찾습니다.
  • 일치하는 user의 refreshToken을 검사합니다.

5) accessToken또는 refreshToken이 만료된 경우 처리를 합니다.

    // 유효하지 않은 토큰이거나 로그인 기간이 만료된 경우
    if (!isValidAccessToken && !isValidRefreshToken) {
      res.clearCookie("accessToken");
      return res.status(409).json({ error: "로그인 기간 만료" });
    }

    // accessToken이 만료되었지만 유효한 refreshToken을 가지고 있는 경우 newAccessToken 발급
    if (!isValidAccessToken && isValidRefreshToken) {
      const accessToken = createAccessToken(requestedUserId, accessTokenSecretKey, isRememberMeBefore);

      const beforeRememberMeData = isRememberMeBefore;
      if (isRememberMe || beforeRememberMeData) {
        res.cookie("accessToken", accessToken);
      }
    }

    // 유효한 accessToken을 가지고 있는 경우 로그인
    const accessToken = createAccessToken(userId, accessTokenSecretKey, isRememberMe);
    return res.status(200).json({
      message: "로그인되었습니다.",
      userId: requestedUser.userId,
      accessToken,
      nickname: requestedUser.nickname,
    });
  • accessToken과 refreshToken이 둘 다 만료된 경우: 쿠키에서 토큰 정보를 삭제하고 로그인 기간 만료 응답을 합니다.
  • refreshToken만 살아있는 경우: 새로운 accessToken을 발급하여 쿠키에 저장합니다.
  • 새로운 accessToken을 발급할 때, 이전에 자동로그인에 체크한 user인 경우 자동로그인을 유지할 수 있도록 합니다.
  • 모두 유효한 경우: accessToken을 도로 반환합니다.

로그아웃 (접근 제한 기능)

구현방법

  • 로그아웃을 하기 위해서는 로그인 된 사용자를 확인하여야 합니다.
  • accessToken을 header로 전달받아 accessToken과 refreshToken을 검사합니다.
  • 토큰 검사 기능은 미들웨어로 구현합니다.

1) 제한된 사용자의 활동을 검증하는 미들웨어의 필요성

//로그아웃
app.post("/api/protected/users/signout", async (req, res) => {
});
  • protected 경로가 포함된 api 주소로 요청을 보낸 user의 제한된 활동 가능 여부를 검증하여야 합니다.
app.use("/api/protected", authenticateToken(accessTokenSecretKey, User));
  • 미들웨어를 생성하여 user의 제한된 활동을 검증합시다.

2) 활동 검증 미들웨어 구현합니다.

const ERROR_MESSAGES = {
  INVALID_TOKEN: "유효하지 않은 토큰",
  INVALID_USERID: "존재하지 않는 userId",
  EXPIRED_LOGIN: "로그인 기간 만료",
};

const EXPIRATION_TIME = "10s";

exports.authenticateToken = (accessTokenSecretKey, UserModel) => async (req, res, next) => {
  try {
    const accessToken = req.headers.authorization && getAccessToken(req.headers.authorization);

    if (!accessToken) {
      return res.status(400).json({ error: ERROR_MESSAGES.INVALID_TOKEN });
    }
  • accessToken을 헤더로 전달하지 않은 user의 경우에는 접근 불가능을 response 합니다.
    // 유저 정보 확인
    const decodedToken = jwt.decode(accessToken, accessTokenSecretKey);
    const requestedUserId = decodedToken && decodedToken.userId;
    const requestedUser = await UserModel.findOne({ userId: requestedUserId });

    // 존재하지 않는 userId인 경우
    if (!requestedUser) {
      console.log(`접근 에러: ${ERROR_MESSAGES.INVALID_USERID}`);
      return res.status(400).json({ error: ERROR_MESSAGES.INVALID_USERID });
    }
  • accessToken을 전달하였으나, 토큰 검사 결과 이상이 발견된 경우 접근 불가능을 response 합니다.
    // accessToken 및 refreshToken 검증
    const isValidAccessToken = verifyToken(accessToken, accessTokenSecretKey);
    const isValidRefreshToken = verifyToken(requestedUser.refreshToken, accessTokenSecretKey);

    // 유효하지 않은 토큰이거나 로그인 기간이 만료된 경우
    if (!isValidAccessToken && !isValidRefreshToken) {
      console.log(`토큰 만료: A(${isValidAccessToken}) & R(${isValidRefreshToken})`);
      res.clearCookie("accessToken");
      return res.status(419).json({ error: `${ERROR_MESSAGES.EXPIRED_LOGIN}` });
    }
  • accessToken과 refreshToken의 만료 여부를 검사합니다.
  • 양쪽 모두 만료된 토큰을 지닌 경우에는 쿠키에 저장된 accessToken을 삭제하고 접근 불가능을 response 합니다.
    // accessToken 만료 + 유효한 refreshToken인 경우, 새로운 accessToken 발급 및 쿠키 설정
    if (!isValidAccessToken && isValidRefreshToken) {
      const newAccessToken = createAccessToken(requestedUserId, accessTokenSecretKey);

      res.locals = { accessToken: newAccessToken };
    } else {
      // 유효한 accessToken을 가지고 있는 경우
      res.locals = { accessToken };
    }

    // 다음 미들웨어로 유효한 accessToken을 소유한 user의 데이터 전달
    const { userId, nickname } = requestedUser;
    res.locals.userId = userId;
    res.locals.nickname = nickname;

    return next();
  • accessToken만 만료된 경우 새로운 accessToken을 발급하여 제한된 활동을 유지할 수 있도록 합니다.
  • 문제가 발견되지 않았다면 다음 미들웨어로 user 정보를 전달합니다.

3) (이제는)검증된 사용자의 로그아웃 구현하기

//로그아웃
app.post("/api/protected/users/signout", async (req, res) => {
  try {
    const { userId } = res.locals;

    // 클라이언트의 db에 존재하는 refreshToken 데이터 초기화
    const updatedUser = await User.findOneAndUpdate(
      { userId },
      { tokenExpiresAt: "", refreshToken: "" },
      { new: true } // 옵션을 설정하여 업데이트된 결과를 반환
    );

    // 로그아웃 성공 시
    if (updatedUser) {
      res.clearCookie("accessToken"); // 클라이언트 쿠키 초기화
      return res.status(200).json({ message: "로그아웃 되었습니다." });
  • 유저의 refreshToken 데이터를 DB에서 삭제합니다.
  • 클라이언트의 accessToken 쿠키를 초기화하고 로그아웃 되었음을 response 합니다.

회원 탈퇴 (접근 제한 기능)

구현 방법

  • DB에 존재하는 user의 데이터를 삭제합니다.
  • 클라이언트의 cookie를 정리합니다.
//회원 탈퇴
app.delete("/api/protected/users/:userId/withdraw", async (req, res) => {
  try {
    const { userId } = res.locals;

    // db에 존재하는 user 데이터 삭제
    await User.deleteOne({ userId });

    // 클라이언트 쿠키 정리
    res.clearCookie("accessToken");
    res.status(200).json({ message: `${userId}님의 회원탈퇴가 완료되었습니다.` });
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "회원탈퇴 작업 수행 중 문제가 발생하였습니다." });
  }
});
  • DB에 존재하는 user의 데이터를 삭제합니다.
  • 클라이언트의 cookie를 정리합니다.
  • 회원 탈퇴 완료를 response 합니다.

'데이터시각화-KMG > Node.js' 카테고리의 다른 글

[Node.js] 게시판 기능 구현하기  (0) 2023.07.28