안녕하세요
[Node.js] 회원 기능 구현하기 본문
회원가입, 로그인, 로그아웃, 회원탈퇴 이렇게 네가지를 구현합니다.
// 회원 가입
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 |
---|