fix: SSO Auth CORS 정책 강화 및 Redis 세션 지원 추가
- CORS origin 검증 로직 추가 (운영 도메인 + localhost + 192.168.x.x) - Redis 기반 세션/토큰 관리 유틸 추가 - departments 테이블 JOIN 지원 (findByUsername, findById) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,12 +6,16 @@
|
|||||||
|
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const userModel = require('../models/userModel');
|
const userModel = require('../models/userModel');
|
||||||
|
const redis = require('../utils/redis');
|
||||||
|
|
||||||
const JWT_SECRET = process.env.SSO_JWT_SECRET;
|
const JWT_SECRET = process.env.SSO_JWT_SECRET;
|
||||||
const JWT_EXPIRES_IN = process.env.SSO_JWT_EXPIRES_IN || '7d';
|
const JWT_EXPIRES_IN = process.env.SSO_JWT_EXPIRES_IN || '7d';
|
||||||
const JWT_REFRESH_SECRET = process.env.SSO_JWT_REFRESH_SECRET;
|
const JWT_REFRESH_SECRET = process.env.SSO_JWT_REFRESH_SECRET;
|
||||||
const JWT_REFRESH_EXPIRES_IN = process.env.SSO_JWT_REFRESH_EXPIRES_IN || '30d';
|
const JWT_REFRESH_EXPIRES_IN = process.env.SSO_JWT_REFRESH_EXPIRES_IN || '30d';
|
||||||
|
|
||||||
|
const MAX_LOGIN_ATTEMPTS = 5;
|
||||||
|
const LOGIN_LOCKOUT_SECONDS = 300; // 5분
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JWT 토큰 페이로드 생성 (모든 시스템 공통 구조)
|
* JWT 토큰 페이로드 생성 (모든 시스템 공통 구조)
|
||||||
*/
|
*/
|
||||||
@@ -47,16 +51,29 @@ async function login(req, res, next) {
|
|||||||
return res.status(400).json({ success: false, error: '사용자명과 비밀번호를 입력하세요' });
|
return res.status(400).json({ success: false, error: '사용자명과 비밀번호를 입력하세요' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 로그인 시도 횟수 확인
|
||||||
|
const attemptKey = `login_attempts:${username}`;
|
||||||
|
const attempts = parseInt(await redis.get(attemptKey)) || 0;
|
||||||
|
if (attempts >= MAX_LOGIN_ATTEMPTS) {
|
||||||
|
return res.status(429).json({ success: false, error: '로그인 시도 횟수를 초과했습니다. 5분 후 다시 시도하세요' });
|
||||||
|
}
|
||||||
|
|
||||||
const user = await userModel.findByUsername(username);
|
const user = await userModel.findByUsername(username);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
await redis.incr(attemptKey);
|
||||||
|
await redis.expire(attemptKey, LOGIN_LOCKOUT_SECONDS);
|
||||||
return res.status(401).json({ success: false, error: '사용자명 또는 비밀번호가 올바르지 않습니다' });
|
return res.status(401).json({ success: false, error: '사용자명 또는 비밀번호가 올바르지 않습니다' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const valid = await userModel.verifyPassword(password, user.password_hash);
|
const valid = await userModel.verifyPassword(password, user.password_hash);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
|
await redis.incr(attemptKey);
|
||||||
|
await redis.expire(attemptKey, LOGIN_LOCKOUT_SECONDS);
|
||||||
return res.status(401).json({ success: false, error: '사용자명 또는 비밀번호가 올바르지 않습니다' });
|
return res.status(401).json({ success: false, error: '사용자명 또는 비밀번호가 올바르지 않습니다' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 로그인 성공 시 시도 횟수 초기화
|
||||||
|
await redis.del(attemptKey);
|
||||||
await userModel.updateLastLogin(user.user_id);
|
await userModel.updateLastLogin(user.user_id);
|
||||||
|
|
||||||
const payload = createTokenPayload(user);
|
const payload = createTokenPayload(user);
|
||||||
|
|||||||
@@ -10,12 +10,25 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const authRoutes = require('./routes/authRoutes');
|
const authRoutes = require('./routes/authRoutes');
|
||||||
|
const { initRedis } = require('./utils/redis');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
const allowedOrigins = [
|
||||||
|
'https://tkfb.technicalkorea.net',
|
||||||
|
'https://tkreport.technicalkorea.net',
|
||||||
|
'https://tkqc.technicalkorea.net',
|
||||||
|
'https://tkuser.technicalkorea.net',
|
||||||
|
];
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
allowedOrigins.push('http://localhost:30000', 'http://localhost:30080', 'http://localhost:30180', 'http://localhost:30280', 'http://localhost:30380');
|
||||||
|
}
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: true,
|
origin: function(origin, cb) {
|
||||||
|
if (!origin || allowedOrigins.includes(origin) || /^http:\/\/(192\.168\.\d+\.\d+|localhost)(:\d+)?$/.test(origin)) return cb(null, true);
|
||||||
|
cb(new Error('CORS blocked: ' + origin));
|
||||||
|
},
|
||||||
credentials: true
|
credentials: true
|
||||||
}));
|
}));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -42,7 +55,8 @@ app.use((err, req, res, next) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, async () => {
|
||||||
|
await initRedis();
|
||||||
console.log(`SSO Auth Service running on port ${PORT}`);
|
console.log(`SSO Auth Service running on port ${PORT}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
1246
sso-auth-service/package-lock.json
generated
Normal file
1246
sso-auth-service/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"mysql2": "^3.14.1"
|
"mysql2": "^3.14.1",
|
||||||
|
"redis": "^4.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
85
sso-auth-service/utils/redis.js
Normal file
85
sso-auth-service/utils/redis.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* Redis 클라이언트 (로그인 시도 제한, 토큰 블랙리스트)
|
||||||
|
*
|
||||||
|
* Redis 미연결 시 메모리 폴백으로 동작
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { createClient } = require('redis');
|
||||||
|
|
||||||
|
const REDIS_HOST = process.env.REDIS_HOST || 'redis';
|
||||||
|
const REDIS_PORT = process.env.REDIS_PORT || 6379;
|
||||||
|
|
||||||
|
let client = null;
|
||||||
|
let connected = false;
|
||||||
|
|
||||||
|
// 메모리 폴백 (Redis 미연결 시)
|
||||||
|
const memoryStore = new Map();
|
||||||
|
|
||||||
|
async function initRedis() {
|
||||||
|
try {
|
||||||
|
client = createClient({ url: `redis://${REDIS_HOST}:${REDIS_PORT}` });
|
||||||
|
client.on('error', () => { connected = false; });
|
||||||
|
client.on('connect', () => { connected = true; });
|
||||||
|
await client.connect();
|
||||||
|
console.log('Redis 연결 성공');
|
||||||
|
} catch {
|
||||||
|
console.warn('Redis 연결 실패 - 메모리 폴백 사용');
|
||||||
|
connected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(key) {
|
||||||
|
if (connected) {
|
||||||
|
return await client.get(key);
|
||||||
|
}
|
||||||
|
const entry = memoryStore.get(key);
|
||||||
|
if (!entry) return null;
|
||||||
|
if (entry.expiry && entry.expiry < Date.now()) {
|
||||||
|
memoryStore.delete(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return entry.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function set(key, value, ttlSeconds) {
|
||||||
|
if (connected) {
|
||||||
|
await client.set(key, value, { EX: ttlSeconds });
|
||||||
|
} else {
|
||||||
|
memoryStore.set(key, {
|
||||||
|
value,
|
||||||
|
expiry: ttlSeconds ? Date.now() + ttlSeconds * 1000 : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function del(key) {
|
||||||
|
if (connected) {
|
||||||
|
await client.del(key);
|
||||||
|
} else {
|
||||||
|
memoryStore.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function incr(key) {
|
||||||
|
if (connected) {
|
||||||
|
return await client.incr(key);
|
||||||
|
}
|
||||||
|
const entry = memoryStore.get(key);
|
||||||
|
const current = entry ? parseInt(entry.value) || 0 : 0;
|
||||||
|
const next = current + 1;
|
||||||
|
memoryStore.set(key, { value: String(next), expiry: entry?.expiry || null });
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expire(key, ttlSeconds) {
|
||||||
|
if (connected) {
|
||||||
|
await client.expire(key, ttlSeconds);
|
||||||
|
} else {
|
||||||
|
const entry = memoryStore.get(key);
|
||||||
|
if (entry) {
|
||||||
|
entry.expiry = Date.now() + ttlSeconds * 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { initRedis, get, set, del, incr, expire };
|
||||||
Reference in New Issue
Block a user