해결된 질문
작성
·
81
0
비슷한 질문이 있어서 5-2강까지 강의를 들어서 adminLogin까지 구현을 하였습니다.
앞의 4-6강에서 로그아웃을 시도하였을 때, 400 Bad Request : 이미 로그아웃된 상태로 나옵니다.
그래서 console.log(req.cookies.token)을 해보았을 때, undefined가 나옵니다.
adminLogin을 진행하여 브라우저에 쿠키가 제대로 저장되었는지 확인해보았는데, localhost:5173에서도 localhost:3000에서도 cookie에 token값이 저장되어있었습니다.
index.js에 cookie-parser 또한 존재하는 상태입니다.
질문을 올리기 전에 여러가지를 시도해보았는데, router.post() 에서는 req.cookies.token의 값을 undefined로 가져오고, router.get()에서는 정상적인 토큰값을 반환했습니다.
어떻게 해야 router.post() 에서도 req.cookies.token값을 가져올 수 있을까요?
thunder client로 GET http://localhost:3000/api/auth/getCookie를 했을 때도 token은 undefined 값이 출력되었다가 브라우저에서 주소로 접근하니 token값이 정상적으로 출력되었습니다.
아래의 사진은 thunder client로 get 방식과 post 방식으로 보냈을 때의 차이를 담은 사진입니다.
브라우저에 cookie가 정상적으로 저장된 사진입니다.
// index.js
require("dotenv").config();
const express = require("express");
const mongoose = require("mongoose");
const cookieParser = require("cookie-parser");
const cors = require("cors");
const userRoutes = require("./routes/user");
const app = express();
const PORT = 3000;
app.use(cors({
origin: "http://localhost:5173",
credentials: true,
}));
app.use(express.json())
app.use(express.urlencoded({extended : true}))
app.use(cookieParser());
app.use("/api/auth", userRoutes);
app.get("/", (req, res) => {
res.send("Hello world");
console.log("token: " + req.cookies.token);
});
app.post("/cookie", (req, res) => {
console.log(req.cookies.token)
res.send("api/auth");
})
mongoose
.connect(process.env.MONGO_URL)
.then(() => console.log("MongoDB와 연결이 되었습니다."))
.catch((error) => console.log("MongoDB와 연결에 실패했습니다: ", error));
app.listen(PORT, () => {
console.log("Server is running");
});
// 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");
// const cookieParser = require("cookie-parser");
// router.use(express.json())
// router.use(express.urlencoded({extended : true}))
// router.use(cookieParser());
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 (error) {
// console.log("IP 주소를 가져오던 중 오류 발생: ", error.message);
// }
await user.save();
console.log("로그인 성공");
const token = jwt.sign(
{ userId: user._id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: "24h" }
);
console.log(token);
res.cookie("token", token, {
httpOnly: true,
secure: "production",
sameSite: "strict",
maxAge: 24 * 60 * 60 * 1000,
});
console.log("쿠키 설정", );
const userWithoutPassword = user.toObject();
delete userWithoutPassword.password;
res.json({ user: userWithoutPassword });
console.log("json 전달 후 종료");
} catch (error) {
console.log("서버 오류: ", error.message);
res.status(500).json({ message: "서버 오류가 발생했습니다." });
}
});
router.post("/logout", async (req, res) => {
try {
const token = req.cookies.token;
console.log(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: "서버 오류가 발생했습니다." });
}
});
router.post("/verify-token", (req, res) => {
const token = req.cookies.token;
if (!token) {
return res
.status(400)
.json({ isValid: false, message: "토큰이 없습니다." });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
return res.status(200).json({ isValid: true, user: decoded });
} catch (error) {
return res
.status(401)
.json({ isValid: false, message: "유효하지 않은 토큰입니다." });
}
});
// router.get("/getCookie", async (req, res) => {
// res.send(req.cookies.token)
// console.log("getCookie's token : " , req.cookies.token)
// })
// router.post("/postCookie", async (req, res) => {
// res.send(req.cookies.token)
// console.log("postCookie's token : ", req.cookies.token)
// })
module.exports = router;
답변 1
0
안녕하세요. 남겨주신 질문에 답변 드리겠습니다.
router.get()
에서는 쿠키(req.cookies.token
)가 정상적으로 전달되었지만, router.post()
에서는 undefined
가 발생한 이유는 쿠키의 설정 값 때문입니다.
secure: "production"
설정 문제
secure
옵션은 true
또는 false
로 설정해야 하는데, "production"
(문자열)로 설정하면 브라우저가 이를 인식하지 못하고 쿠키를 설정하지 않습니다.
결과적으로 POST 요청에서 쿠키가 제대로 전달되지 않았습니다.
sameSite: "strict"
설정 문제
sameSite: "strict"
는 같은 도메인 내 요청에서만 쿠키를 포함하도록 제한합니다.
GET 요청은 단순 요청이라 쿠키가 포함될 수 있었지만, POST 요청은 보안 정책에 의해 쿠키가 전송되지 않았습니다.
백엔드 user.js 96줄 기준으로 다음과 같이 수정하면 GET과 POST 요청에서 모두 쿠키를 정상적으로 받을 수 있습니다.
res.cookie("token", token, {
httpOnly: true,
secure: false,
sameSite: "None", // CORS 환경에서 POST 요청 허용
maxAge: 24 * 60 * 60 * 1000,
});
추가적으로, 위 해결 방법을 적용하셨음에도 불구하고 req.cookies.token
이 undefined
로 출력되는 경우, 번거로우시겠지만 ch5-2 GitHub 저장소에서 소스코드를 다운로드받아 해당 코드에서 테스트해 주시기를 부탁드립니다.
📌 🔗 5-2 GitHub 저장소
정확한 원인을 파악하는 데 있어 조금 더 구체적인 환경에서 테스트가 필요하여 요청드리는 점, 불편을 끼쳐드려 죄송합니다. 그리고 귀한 시간을 내어 확인해 주셔서 진심으로 감사드립니다.
secure: "production"
이 어떻게 작동하나요?A1. 강의 중 작성된 secure: "production"
은 실수입니다. 강의 코드에서 secure: "production"
은 문자열이므로 Boolean(true/false
) 값으로 올바르게 해석되지 않습니다.
res.cookie("token", token, {
httpOnly: true,
secure: "production", // ❌ 문자열이라서 에러가 발생할 가능성이 있음
sameSite: "None",
maxAge: 24 * 60 * 60 * 1000,
});
다만, 제가 직접 진행한 강의에서 secure: "production"
을 사용했지만, 일부 환경에서는 허용될 수도 있습니다.
일부 브라우저나 환경에서 "production"
을 truthy 값으로 인식하여 true
처럼 동작
"production"
을 Boolean 변환하면 true
가 됨 → secure: true
처럼 보일 가능성이 있습니다.
console.log(Boolean("production")); // true
하지만 이는 보장되지 않는 동작이며, 일부 브라우저나 라이브러리에서 예상과 다르게 동작할 수 있습니다.
따라서, "production" -> false로 쓰는 것이 가장 정확합니다. 강의 중 실수로 인해 불편을 끼쳐 죄송합니다.
A2. 김주형님께서 하신 질문을 정리하면, Thunder Client로 localhost:3000
(백엔드)에 요청을 보냈을 때, 브라우저(localhost:5173
)에서 쿠키가 보이지 않거나 요청이 다르게 동작하는 이유가 무엇인가? 라고 이해했습니다. 혹시 제 해석이 다르다면, 다시 말씀해 주시면 감사하겠습니다.
프론트엔드 브라우저와 Thunder Client는 서로 다른 환경에서 쿠키 저장 및 브라우저 세션이 별개이며 동작 방식도 다릅니다.
Thunder Client는 단순한 HTTP 요청 도구
직접 백엔드(localhost:3000
)에 요청을 보냄.
CORS 정책을 적용하지 않음.
쿠키 저장 및 브라우저 세션과는 별개.
브라우저(프론트엔드)는 웹 보안 정책을 따름
프론트엔드(localhost:5173
)에서 백엔드(localhost:3000
)로 요청 시 CORS 정책 적용.
secure
, sameSite
, httpOnly
설정에 따라 쿠키가 저장되지 않을 수도 있음.
요청 시 기본적으로 쿠키가 포함되지 않음 (WithCredentials: "true"
필요).
추가적으로 궁금하신게 있으시다면 질문 부탁드립니다. 감사합니다.
감사합니다. 덕분에 해결되었습니다.
추가적인 질문이 있습니다.
강의에서 사용하신 코드에는 secure: "production"으로 쓰셨는데, 이 코드가 어떻게 작동하는 건가요?
thunder client에서 보내는 HTTP 요청은 브라우저 localhost:3000에 반영되지 않던데 원래 그런건가요?