diff --git a/gateway/html/dashboard.html b/gateway/html/dashboard.html
index 9606890..1d2e0c4 100644
--- a/gateway/html/dashboard.html
+++ b/gateway/html/dashboard.html
@@ -773,10 +773,6 @@
ssoCookie.set('sso_user', JSON.stringify(data.user), 7);
if (data.refresh_token) ssoCookie.set('sso_refresh_token', data.refresh_token, 30);
- localStorage.setItem('sso_token', data.access_token);
- localStorage.setItem('sso_user', JSON.stringify(data.user));
- if (data.refresh_token) localStorage.setItem('sso_refresh_token', data.refresh_token);
-
var redirect = new URLSearchParams(location.search).get('redirect');
if (redirect && isSafeRedirect(redirect)) {
window.location.href = redirect;
diff --git a/gateway/html/shared/nav-header.js b/gateway/html/shared/nav-header.js
index 4126af2..6e9a4fb 100644
--- a/gateway/html/shared/nav-header.js
+++ b/gateway/html/shared/nav-header.js
@@ -31,11 +31,11 @@
*/
window.SSOAuth = {
getToken: function() {
- return cookieGet('sso_token') || localStorage.getItem('sso_token');
+ return cookieGet('sso_token');
},
getUser: function() {
- var raw = cookieGet('sso_user') || localStorage.getItem('sso_user');
+ var raw = cookieGet('sso_user');
try { return JSON.parse(raw); } catch(e) { return null; }
},
diff --git a/gateway/nginx.conf b/gateway/nginx.conf
index c365922..ff8fe4e 100644
--- a/gateway/nginx.conf
+++ b/gateway/nginx.conf
@@ -4,6 +4,10 @@ server {
root /usr/share/nginx/html;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+
# 대시보드 (로그인 포함)
location = /dashboard {
add_header Cache-Control "no-store, no-cache, must-revalidate";
diff --git a/sso-auth-service/controllers/authController.js b/sso-auth-service/controllers/authController.js
index 243387f..e8be7d0 100644
--- a/sso-auth-service/controllers/authController.js
+++ b/sso-auth-service/controllers/authController.js
@@ -127,8 +127,17 @@ async function loginForm(req, res, next) {
return res.status(400).json({ detail: 'Missing username or password' });
}
+ // Rate limiting (동일 로직: /login과 공유)
+ const attemptKey = `login_attempts:${username}`;
+ const attempts = parseInt(await redis.get(attemptKey)) || 0;
+ if (attempts >= MAX_LOGIN_ATTEMPTS) {
+ return res.status(429).json({ detail: '로그인 시도 횟수를 초과했습니다. 5분 후 다시 시도하세요' });
+ }
+
const user = await userModel.findByUsername(username);
if (!user) {
+ await redis.incr(attemptKey);
+ await redis.expire(attemptKey, LOGIN_LOCKOUT_SECONDS);
return res.status(401).json({ detail: 'Incorrect username or password' });
}
@@ -139,9 +148,14 @@ async function loginForm(req, res, next) {
const valid = await userModel.verifyPassword(password, user.password_hash);
if (!valid) {
+ await redis.incr(attemptKey);
+ await redis.expire(attemptKey, LOGIN_LOCKOUT_SECONDS);
return res.status(401).json({ detail: 'Incorrect username or password' });
}
+ // 로그인 성공 시 시도 횟수 초기화
+ await redis.del(attemptKey);
+
await userModel.updateLastLogin(user.user_id);
const payload = createTokenPayload(user);
diff --git a/system1-factory/api/config/security.js b/system1-factory/api/config/security.js
index e58037e..4c27453 100644
--- a/system1-factory/api/config/security.js
+++ b/system1-factory/api/config/security.js
@@ -19,7 +19,7 @@ const helmetOptions = {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
- scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], // 개발 중 unsafe-eval 허용
+ scriptSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:", "blob:"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
connectSrc: ["'self'", "https://api.technicalkorea.com"],
diff --git a/system1-factory/web/index.html b/system1-factory/web/index.html
index af33734..c631df7 100644
--- a/system1-factory/web/index.html
+++ b/system1-factory/web/index.html
@@ -10,7 +10,7 @@
if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then(function(r){r.forEach(function(reg){reg.unregister()});})}
if('caches' in window){caches.keys().then(function(k){k.forEach(function(key){caches.delete(key)})})}
-
+
-
+
-
+