세션은 서버 측에서 사용자의 상태나 데이터를 유지하기 위한 방법입니다. 사용자가 서버에 접속할 때 서버는 세션을 생성하고 이를 통해 각 사용자의 정보를 서버에 저장합니다. 세션은 로그인 정보와 같은 데이터를 저장하여 사용자가 웹사이트를 이동할 때마다 다시 로그인할 필요가 없도록 합니다.
1 ) 서버 측 저장
2 ) 보안성
3 ) 세션 ID와 쿠키의 관계
4 ) 유효 기간
5 ) 서버의 자원 사용
6 ) 사용자 별 고유성
세션과 쿠키에 대한 자세한 설명은 https://velog.io/@njt6419/Node.js-cookie%EC%99%80-session 을 참고해주세요.
├── app.js
├── src
│ ├── middleware
│ │ └── auth-middleware.js
│ ├── routes
│ │ ├── login-routes.js
│ │ ├── logout-routes.js
│ │ ├── user-routes.js
│ │ └── check-login-routes.js
└── └── db
└── user.json
npm i nodemon express cors express-session bcrypt
app.js
const express = require("express");
const cors = require("cors");
const session = require("express-session");
const loginRouter = require("./src/routes/login-router");
const logoutRouter = require("./src/routes/logout-router");
const userRouter = require("./src/routes/user-router");
const checkLoginRouter = require("./src/routes/check-login-router");
const {
isAuthenticated, // 인증된 사용자만 접근 가능한 미들웨어
isNotAuthenticated, // 인증되지 않은 사용자만 접근 가능한 미들웨어
} = require("./src/middleware/auth-middleware");
const app = express();
const PORT = 8080;
app.use(cors({ origin: "http://localhost:3000", credentials: true }));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 세션 설정
app.use(
session({
secret: "mysecretkey", // 세션 암호화를 위한 비밀키 (필수)
resave: false, // 세션을 변경하지 않는 한 매 요청마다 다시 저장하지 않음
saveUninitialized: false, // 초기화되지 않은 세션도 저장 (로그인하지 않아도 세션 생성 가능)
cookie: {
maxAge: 1000 * 60 * 60 * 24, // 쿠키 유효기간: 24시간
secure: false, // HTTPS 환경에서만 쿠키 전송, 여기서는 HTTP이므로 false로 설정
httpOnly: true, // 브라우저의 JavaScript로 쿠키에 접근하지 못하도록 설정
},
})
);
// 라우터 설정
app.use("/login", isNotAuthenticated, loginRouter); // 로그인되지 않은 사용자만 접근 가능
app.use("/logout", logoutRouter); // 로그아웃
app.use("/user", isAuthenticated, userRouter); // 유저 조회 로그인된 사용자만 접근 가능
app.use("/check-login", checkLoginRouter); // 로그인 확인 => 클라이언트 측에서 로그인 상태를 확인하고 접근 설정을 위해
// 에러 처리 미들웨어
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send("Something wrong!");
});
// 서버 실행
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`); // 서버가 정상적으로 실행되었을 때 콘솔에 출력
});
login-router.js
req.session.user
로 사용자 이메일을 저장합니다. 이를 통해 로그인 상태를 유지합니다.const express = require("express");
const router = express.Router();
const bcrypt = require("bcrypt");
const fs = require("fs");
const path = require("path");
// 로그인 처리
router.post("/", (req, res) => {
const { email, password } = req.body;
// 유저 데이터 불러오기 (유저: email: 'test@gmail.com' password:'123123')
const dataPath = path.join(__dirname, "../db/users.json");
const fileData = fs.readFileSync(dataPath, "utf-8");
const users = JSON.parse(fileData).users;
// 사용자 찾기
const user = users.find((user) => user.email === email);
if (!user) {
return res
.status(400)
.json({ message: "이메일 혹은 비밀번호가 일치하지 않습니다." });
}
// 비밀번호 비교
bcrypt.compare(password, user.password, (err, isMatch) => {
if (err) throw err;
if (isMatch) {
// 로그인 성공, 세션에 사용자 정보 저장
req.session.user = user.email;
req.session.save((err) => {
if (err) {
console.log(err);
throw err;
}
return res.status(200).json({ message: "로그인 성공" });
});
} else {
return res
.status(400)
.json({ message: "이메일 혹은 비밀번호가 일치하지 않습니다." });
}
});
});
module.exports = router;
logout-router.js
req.session.destroy()
로 세션을 파괴하고 클라이언트 측 쿠키도 삭제한 후, 로그아웃 성공 메시지를 반환합니다.
const express = require("express");
const router = express.Router();
// 로그아웃 처리
router.get("/", (req, res) => {
req.session.destroy((err) => {
if (err) {
console.log(err);
throw err;
}
res.clearCookie("connect.sid"); // 세션 쿠키 삭제
res.status(200).json({ message: "로그아웃 성공" });
});
});
module.exports = router;
user-router.js
사용자의 세션 정보가 존재할 경우, 해당 사용자 정보를 클라이언트에 전달합니다.
const express = require("express");
const router = express.Router();
router.get("/", (req, res) => {
return res
.status(200)
.json({ message: "유저 조회 성공", user: req.session.user });
});
module.exports = router;
check-login-router.js
세션에 사용자가 있는지 확인하여, 로그인된 상태인지 아닌지를 클라이언트에 응답합니다.
const express = require("express");
const router = express.Router();
router.get("/", (req, res) => {
if (req.session && req.session.user) {
// 세션에 사용자가 존재하면 로그인된 상태로 간주
res.json({ loggedIn: true, user: req.session.user });
} else {
// 로그인되지 않은 상태
res.json({ loggedIn: false });
}
});
module.exports = router;
auth-middleware.js
사용자가 로그인된 상태인지 아닌지를 확인하고, 로그인된 사용자는 특정 페이지에 접근할 수 없게 제한하거나 로그인되지 않은 사용자는 로그인 페이지로 리디렉션합니다.
// 로그인 확인 미들웨어
function isAuthenticated(req, res, next) {
if (req.session.user) {
return next(); // 로그인되어 있으면 요청을 계속 처리
} else {
return res.status(403).json({ message: "로그인이 필요합니다." }); // 로그인되지 않은 경우 로그인 페이지로 리다이렉트
}
}
// 접근 제한 미들웨어
function isNotAuthenticated(req, res, next) {
if (!req.session.user) {
return next(); // 로그인되지 않은 경우 요청을 계속 처리
} else {
return res.status(403).json({ message: "이미 로그인되어 있습니다." }); // 이미 로그인된 경우 메인 페이지로 리다이렉트
}
}
module.exports = { isAuthenticated, isNotAuthenticated };
users.json
해시 처리된 비밀번호는 ‘123123’입니다.
{
"users": [
{
"id": 1,
"email": "test@gmail.com",
"password": "$2b$12$0aqbU8z3KKir9ApMcpxxP.LekvVNQyXxZ9vRyOMLwH80LqixwM592"
}
]
}
nodemon app.js
├── index.html
├── src
│ ├── js
│ │ └── login.js
│ | └── main.js
│ ├── css
│ │ └── style.css
└── └── pages
└── login.html
npm i http-server
index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Main Page</title>
<link rel="stylesheet" href="/src/css/style.css">
</head>
<body>
<div class="main-container">
<h2>메인 페이지</h2>
<p id="welcome-message"></p>
<button id="logout">로그아웃</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="src/js/main.js"></script>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login</title>
<link rel="stylesheet" href="../css/style.css" />
</head>
<body>
<div class="login-container">
<h2>로그인</h2>
<form id="login-form">
<label for="email">이메일</label>
<input type="email" id="email" name="email" placeholder="Email" required />
<label for="password">비밀번호</label>
<input type="password" id="password" name="password" placeholder="Password" required />
<button type="submit">로그인</button>
</form>
<div id="message"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="../js/login.js"></script>
</body>
</html>
main.js
페이지가 로드될 때 서버에 로그인 상태를 확인하여 로그인되지 않은 경우 로그인 페이지로 리디렉션합니다. 로그아웃 버튼을 클릭하면 로그아웃 요청을 보내고, 로그인 페이지로 이동합니다.
document.addEventListener("DOMContentLoaded", async function () {
const welcomeMessage = document.getElementById("welcome-message");
let user;
// 서버에 로그인 상태 확인 요청
try {
const response = await axios.get("http://localhost:8080/check-login", {
withCredentials: true,
});
// 로그인 상태가 아니라면 로그인 페이지로 리디렉션
if (!response.data.loggedIn) {
window.location.href = "/src/pages/login.html";
}
} catch (error) {
console.error(error);
}
// 유저 데이터 조회
try {
const response = await axios.get("http://localhost:8080/user", {
withCredentials: true,
});
user = response.data.user;
} catch (error) {
console.error(error);
// 만약 로그인 상태가 아닌 경우 로그인 페이지로 리디렉션
if (error.response.status === 403) {
window.location.href = "/src/pages/login.html";
}
}
welcomeMessage.textContent = `${user}님 환영합니다!`;
// 로그아웃 버튼 처리
document
.getElementById("logout")
.addEventListener("click", async function () {
try {
await axios.get("http://localhost:8080/logout", {
withCredentials: true,
});
window.location.href = "/src/pages/login.html";
} catch (error) {
console.error(error);
}
});
});
login.js
사용자가 로그인 폼을 제출하면 서버에 이메일과 비밀번호를 전송하여 로그인을 시도합니다. 성공 시 메인 페이지로 리디렉션하고, 실패 시 오류 메시지를 화면에 표시합니다.
document.addEventListener("DOMContentLoaded", async function () {
try {
// 서버에 로그인 상태 확인 요청
const response = await axios.get("http://localhost:8080/check-login", {
withCredentials: true, // 세션 쿠키 포함
});
// 로그인 상태라면 메인 페이지로 리디렉션
if (response.data.loggedIn) {
window.location.href = "/index.html";
}
} catch (error) {
console.error(error);
}
document
.getElementById("login-form")
.addEventListener("submit", async function (event) {
event.preventDefault();
const email = document.getElementById("email").value;
const password = document.getElementById("password").value;
const messageDiv = document.getElementById("message");
try {
await axios.post(
"http://localhost:8080/login",
{
email,
password,
},
{
withCredentials: true,
}
);
// 로그인 성공 시
window.location.href = "/index.html"; // 메인 페이지로 리디렉션
} catch (error) {
if (error.response) {
// 서버에서 반환된 오류 메시지 출력
messageDiv.textContent = `로그인 실패: ${error.response.data.message}`;
} else {
messageDiv.textContent = "로그인 실패: 네트워크 오류";
}
}
});
});
style.css
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.login-container,
.main-container {
width: 300px;
margin: 100px auto;
padding: 20px;
background-color: white;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
text-align: center;
}
input {
width: 100%;
padding: 10px;
margin: 10px 0;
box-sizing: border-box;
}
button {
width: 100%;
padding: 10px;
background-color: #28a745;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background-color: #218838;
}
label {
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
#message {
color: red;
margin-top: 10px;
font-size: 11px;
}
npx http-server -p 3000
완성된 프로젝트를 확인하시려면 github 레포지토리를 참고해주세요.