feat: 세션 관리 및 로그인 후 사용자 홈 화면 구현 (Phase 2 완료)
- express-session을 이용한 로그인 상태 유지 구현 - 인증 성공 시 /home.html 리디렉션 및 정보 표시 기능 추가 - 로그아웃 기능 구현 - UI 흐름 개선: 로그인 성공 시 홈 화면으로 자동 이동
This commit is contained in:
95
package-lock.json
generated
95
package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"dotenv": "^17.4.1",
|
||||
"express": "^5.2.1",
|
||||
"express-session": "^1.19.0",
|
||||
"jose": "^6.2.2",
|
||||
"openid-client": "^5.7.1"
|
||||
}
|
||||
@@ -285,6 +286,50 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz",
|
||||
"integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "~0.7.2",
|
||||
"cookie-signature": "~1.0.7",
|
||||
"debug": "~2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-headers": "~1.1.0",
|
||||
"parseurl": "~1.3.3",
|
||||
"safe-buffer": "~5.2.1",
|
||||
"uid-safe": "~2.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express-session/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
@@ -596,6 +641,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -676,6 +730,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -716,6 +779,26 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -877,6 +960,18 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"random-bytes": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"dependencies": {
|
||||
"dotenv": "^17.4.1",
|
||||
"express": "^5.2.1",
|
||||
"express-session": "^1.19.0",
|
||||
"jose": "^6.2.2",
|
||||
"openid-client": "^5.7.1"
|
||||
}
|
||||
|
||||
@@ -101,7 +101,13 @@ loginForm.addEventListener('submit', async (e) => {
|
||||
successTitle.textContent = currentMode === 'phone' ? '인증링크 발송 완료' : '로그인 성공';
|
||||
successDescription.textContent = currentMode === 'phone'
|
||||
? `${identifier} 번호로 인증 링크를 보냈습니다.`
|
||||
: `${identifier} 계정으로 접속되었습니다.`;
|
||||
: `${identifier} 계정으로 접속되었습니다. 잠시 후 홈 화면으로 이동합니다...`;
|
||||
|
||||
if (result.redirectTo) {
|
||||
setTimeout(() => {
|
||||
window.location.href = result.redirectTo;
|
||||
}, 1500);
|
||||
}
|
||||
} else {
|
||||
alert(result.message || '오류가 발생했습니다.');
|
||||
submitButton.disabled = false;
|
||||
|
||||
75
public/home.html
Normal file
75
public/home.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Home - Headless Login Demo</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<style>
|
||||
.home-container {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
.user-info {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
text-align: left;
|
||||
}
|
||||
.info-item {
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.info-label {
|
||||
color: #ccc;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.logout-btn {
|
||||
background: #ff4757;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container home-container">
|
||||
<h1 class="login-title">환영합니다!</h1>
|
||||
<p>SSO를 통해 안전하게 로그인되었습니다.</p>
|
||||
|
||||
<div id="userInfo" class="user-info">
|
||||
<p>사용자 정보를 불러오는 중...</p>
|
||||
</div>
|
||||
|
||||
<button id="logoutBtn" class="login-button logout-btn">로그아웃</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function fetchUserInfo() {
|
||||
try {
|
||||
const response = await fetch('/api/me');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const user = data.user;
|
||||
document.getElementById('userInfo').innerHTML = `
|
||||
<div class="info-item"><span class="info-label">사용자 ID:</span> <span>${user.id}</span></div>
|
||||
<div class="info-item"><span class="info-label">이름:</span> <span>${user.name}</span></div>
|
||||
<div class="info-item"><span class="info-label">로그인 시간:</span> <span>${new Date(user.loginTime).toLocaleString()}</span></div>
|
||||
`;
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch user info:', err);
|
||||
window.location.href = '/';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
||||
await fetch('/api/logout', { method: 'POST' });
|
||||
window.location.href = '/';
|
||||
});
|
||||
|
||||
fetchUserInfo();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
36
server.js
36
server.js
@@ -1,5 +1,6 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const session = require('express-session');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { generateKeyPair, exportJWK } = require('jose');
|
||||
@@ -9,8 +10,23 @@ const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.use(express.json());
|
||||
app.use(session({
|
||||
secret: 'headless-demo-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: { secure: false } // 개발 환경이므로 false
|
||||
}));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// 인증 체크 미들웨어
|
||||
const isAuthenticated = (req, res, next) => {
|
||||
if (req.session.user) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ success: false, message: '인증이 필요합니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
let jwks;
|
||||
let oidcClient;
|
||||
|
||||
@@ -75,12 +91,17 @@ app.post('/api/login', async (req, res) => {
|
||||
await delay(1200);
|
||||
|
||||
if (loginId && password) {
|
||||
const userData = { id: loginId, name: '사용자(SSO)', loginTime: new Date().toISOString() };
|
||||
console.log(`[OIDC Success] ID Token received and verified using SSO public keys.`);
|
||||
|
||||
// 세션에 저장
|
||||
req.session.user = userData;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'SSO(OIDC) 인증 성공',
|
||||
user: { id: loginId, name: '사용자(SSO)' },
|
||||
redirectTo: '/home'
|
||||
user: userData,
|
||||
redirectTo: '/home.html'
|
||||
});
|
||||
} else {
|
||||
res.status(401).json({ success: false, message: 'SSO 인증 실패: 아이디 또는 비밀번호를 확인해주세요.' });
|
||||
@@ -113,6 +134,17 @@ app.post('/api/send-link', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 내 정보 확인 API
|
||||
app.get('/api/me', isAuthenticated, (req, res) => {
|
||||
res.json({ success: true, user: req.session.user });
|
||||
});
|
||||
|
||||
// 로그아웃 API
|
||||
app.post('/api/logout', (req, res) => {
|
||||
req.session.destroy();
|
||||
res.json({ success: true, message: '로그아웃 되었습니다.' });
|
||||
});
|
||||
|
||||
// OIDC Callback (모바일 앱 리디렉션 등 처리용)
|
||||
app.get('/callback', (req, res) => {
|
||||
const params = oidcClient.callbackParams(req);
|
||||
|
||||
Reference in New Issue
Block a user