최초커밋
This commit is contained in:
174
backend-node/src/utils/jwtUtils.ts
Normal file
174
backend-node/src/utils/jwtUtils.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
// JWT 토큰 관리 유틸리티
|
||||
// 기존 PersonBean 정보를 JWT 페이로드로 변환
|
||||
|
||||
import jwt from "jsonwebtoken";
|
||||
import { PersonBean, JwtPayload } from "../types/auth";
|
||||
import config from "../config/environment";
|
||||
|
||||
export class JwtUtils {
|
||||
/**
|
||||
* 사용자 정보로 JWT 토큰 생성
|
||||
* 기존 PersonBean 정보를 JWT 페이로드로 변환
|
||||
*/
|
||||
static generateToken(userInfo: PersonBean): string {
|
||||
try {
|
||||
const payload: JwtPayload = {
|
||||
userId: userInfo.userId,
|
||||
userName: userInfo.userName,
|
||||
deptName: userInfo.deptName,
|
||||
companyCode: userInfo.companyCode,
|
||||
userType: userInfo.userType,
|
||||
userTypeName: userInfo.userTypeName,
|
||||
};
|
||||
|
||||
return jwt.sign(payload, config.jwt.secret, {
|
||||
expiresIn: config.jwt.expiresIn,
|
||||
issuer: "PMS-System",
|
||||
audience: "PMS-Users",
|
||||
} as any);
|
||||
} catch (error) {
|
||||
console.error("JWT token generation error:", error);
|
||||
throw new Error("토큰 생성 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰 검증 및 사용자 정보 추출
|
||||
*/
|
||||
static verifyToken(token: string): PersonBean {
|
||||
try {
|
||||
const decoded = jwt.verify(token, config.jwt.secret) as JwtPayload;
|
||||
|
||||
// PersonBean 형태로 변환
|
||||
const personBean: PersonBean = {
|
||||
userId: decoded.userId,
|
||||
userName: decoded.userName,
|
||||
deptName: decoded.deptName,
|
||||
companyCode: decoded.companyCode,
|
||||
userType: decoded.userType,
|
||||
userTypeName: decoded.userTypeName,
|
||||
};
|
||||
|
||||
return personBean;
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
throw new Error("토큰이 만료되었습니다.");
|
||||
} else if (error instanceof jwt.JsonWebTokenError) {
|
||||
throw new Error("유효하지 않은 토큰입니다.");
|
||||
} else {
|
||||
console.error("JWT token verification error:", error);
|
||||
throw new Error("토큰 검증 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰에서 페이로드만 추출 (검증 없이)
|
||||
*/
|
||||
static decodeToken(token: string): JwtPayload | null {
|
||||
try {
|
||||
return jwt.decode(token) as JwtPayload;
|
||||
} catch (error) {
|
||||
console.error("JWT token decode error:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 만료 시간 확인
|
||||
*/
|
||||
static isTokenExpired(token: string): boolean {
|
||||
try {
|
||||
const decoded = jwt.decode(token) as JwtPayload;
|
||||
if (!decoded || !decoded.exp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
return decoded.exp < currentTime;
|
||||
} catch (error) {
|
||||
console.error("Token expiration check error:", error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 갱신 (만료 시간만 연장)
|
||||
*/
|
||||
static refreshToken(token: string): string {
|
||||
try {
|
||||
const decoded = jwt.decode(token) as JwtPayload;
|
||||
if (!decoded) {
|
||||
throw new Error("토큰을 디코드할 수 없습니다.");
|
||||
}
|
||||
|
||||
// 페이로드에서 만료 시간 관련 필드 제거
|
||||
const { iat, exp, aud, iss, ...payload } = decoded;
|
||||
|
||||
return jwt.sign(payload, config.jwt.secret, {
|
||||
expiresIn: config.jwt.expiresIn,
|
||||
issuer: "PMS-System",
|
||||
} as any);
|
||||
} catch (error) {
|
||||
console.error("JWT token refresh error:", error);
|
||||
throw new Error("토큰 갱신 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰에서 사용자 ID 추출
|
||||
*/
|
||||
static getUserIdFromToken(token: string): string | null {
|
||||
try {
|
||||
const decoded = jwt.decode(token) as JwtPayload;
|
||||
return decoded?.userId || null;
|
||||
} catch (error) {
|
||||
console.error("Get user ID from token error:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 유효성 검사 (만료 여부 포함)
|
||||
*/
|
||||
static validateToken(token: string): { isValid: boolean; error?: string } {
|
||||
try {
|
||||
jwt.verify(token, config.jwt.secret);
|
||||
return { isValid: true };
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
return { isValid: false, error: "토큰이 만료되었습니다." };
|
||||
} else if (error instanceof jwt.JsonWebTokenError) {
|
||||
return { isValid: false, error: "유효하지 않은 토큰입니다." };
|
||||
} else {
|
||||
return { isValid: false, error: "토큰 검증 중 오류가 발생했습니다." };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테스트용 메서드 (개발 환경에서만 사용)
|
||||
*/
|
||||
static testJwtUtils(userInfo: PersonBean): void {
|
||||
console.log("=== JWT 토큰 테스트 ===");
|
||||
console.log("사용자 정보:", userInfo);
|
||||
|
||||
const token = this.generateToken(userInfo);
|
||||
console.log("생성된 토큰:", token);
|
||||
|
||||
const decoded = this.decodeToken(token);
|
||||
console.log("디코드된 페이로드:", decoded);
|
||||
|
||||
const verified = this.verifyToken(token);
|
||||
console.log("검증된 사용자 정보:", verified);
|
||||
|
||||
const isExpired = this.isTokenExpired(token);
|
||||
console.log("토큰 만료 여부:", isExpired);
|
||||
|
||||
const userId = this.getUserIdFromToken(token);
|
||||
console.log("토큰에서 추출한 사용자 ID:", userId);
|
||||
|
||||
const validation = this.validateToken(token);
|
||||
console.log("토큰 유효성 검사:", validation);
|
||||
}
|
||||
}
|
||||
66
backend-node/src/utils/logger.ts
Normal file
66
backend-node/src/utils/logger.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import winston from "winston";
|
||||
import config from "../config/environment";
|
||||
|
||||
// 로그 포맷 정의
|
||||
const logFormat = winston.format.combine(
|
||||
winston.format.timestamp({
|
||||
format: "YYYY-MM-DD HH:mm:ss",
|
||||
}),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
);
|
||||
|
||||
// 콘솔 포맷 (개발 환경용)
|
||||
const consoleFormat = winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.timestamp({
|
||||
format: "YYYY-MM-DD HH:mm:ss",
|
||||
}),
|
||||
winston.format.printf(({ timestamp, level, message, stack }) => {
|
||||
if (stack) {
|
||||
return `${timestamp} [${level}]: ${message}\n${stack}`;
|
||||
}
|
||||
return `${timestamp} [${level}]: ${message}`;
|
||||
})
|
||||
);
|
||||
|
||||
// 로거 생성
|
||||
export const logger = winston.createLogger({
|
||||
level: config.logging.level,
|
||||
format: logFormat,
|
||||
transports: [
|
||||
// 파일 로그 (에러)
|
||||
new winston.transports.File({
|
||||
filename: "logs/error.log",
|
||||
level: "error",
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5,
|
||||
}),
|
||||
// 파일 로그 (전체)
|
||||
new winston.transports.File({
|
||||
filename: "logs/combined.log",
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// 개발 환경에서는 콘솔 출력 추가
|
||||
if (config.nodeEnv !== "production") {
|
||||
logger.add(
|
||||
new winston.transports.Console({
|
||||
format: consoleFormat,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 로그 디렉토리 생성
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const logsDir = path.join(process.cwd(), "logs");
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
export default logger;
|
||||
197
backend-node/src/utils/passwordUtils.ts
Normal file
197
backend-node/src/utils/passwordUtils.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
// 기존 Java EncryptUtil 클래스를 Node.js로 포팅
|
||||
// AES/ECB/NoPadding 암호화 방식 사용
|
||||
|
||||
import crypto from "crypto";
|
||||
|
||||
// 기존 Java Constants에서 가져온 값들
|
||||
const KEY_NAME = "ILJIAESSECRETKEY";
|
||||
const ALGORITHM = "AES";
|
||||
const MASTER_PWD = "qlalfqjsgh11";
|
||||
|
||||
export class PasswordUtils {
|
||||
/**
|
||||
* 기존 Java EncryptUtil.encrypt() 메서드 포팅
|
||||
* AES/ECB/NoPadding 방식으로 암호화
|
||||
*/
|
||||
static encrypt(source: string): string {
|
||||
try {
|
||||
// 16바이트 키 생성 (AES-128)
|
||||
const key = Buffer.from(KEY_NAME, "utf8").slice(0, 16);
|
||||
|
||||
// 패딩 추가 (16바이트 블록 크기에 맞춤)
|
||||
const paddedData = this.addPadding(Buffer.from(source, "utf8"));
|
||||
|
||||
// AES 암호화
|
||||
const cipher = crypto.createCipher("aes-128-ecb", key);
|
||||
cipher.setAutoPadding(false); // NoPadding 모드
|
||||
|
||||
let encrypted = cipher.update(paddedData);
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
|
||||
// 16진수 문자열로 변환
|
||||
return this.fromHex(encrypted);
|
||||
} catch (error) {
|
||||
console.error("Password encryption error:", error);
|
||||
throw new Error("암호화 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 Java EncryptUtil.decrypt() 메서드 포팅
|
||||
*/
|
||||
static decrypt(source: string): string {
|
||||
try {
|
||||
// 16바이트 키 생성
|
||||
const key = Buffer.from(KEY_NAME, "utf8").slice(0, 16);
|
||||
|
||||
// 16진수 문자열을 바이트 배열로 변환
|
||||
const encryptedData = this.toBytes(source);
|
||||
|
||||
// AES 복호화
|
||||
const decipher = crypto.createDecipher("aes-128-ecb", key);
|
||||
decipher.setAutoPadding(false); // NoPadding 모드
|
||||
|
||||
let decrypted = decipher.update(encryptedData);
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||
|
||||
// 패딩 제거
|
||||
const unpaddedData = this.removePadding(decrypted);
|
||||
|
||||
return unpaddedData.toString("utf8");
|
||||
} catch (error) {
|
||||
console.error("Password decryption error:", error);
|
||||
throw new Error("복호화 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 Java EncryptUtil.encryptSha256() 메서드 포팅
|
||||
*/
|
||||
static encryptSha256(s: string): string {
|
||||
try {
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(s, "utf8");
|
||||
return hash.digest("hex");
|
||||
} catch (error) {
|
||||
console.error("SHA256 encryption error:", error);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 검증 (기존 Java 로직과 동일)
|
||||
*/
|
||||
static matches(plainPassword: string, encryptedPassword: string): boolean {
|
||||
try {
|
||||
// 마스터 패스워드 체크
|
||||
if (MASTER_PWD === plainPassword) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 일반 패스워드 암호화 후 비교
|
||||
const encryptedPlainPassword = this.encrypt(plainPassword);
|
||||
return encryptedPlainPassword === encryptedPassword;
|
||||
} catch (error) {
|
||||
console.error("Password matching error:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 Java addPadding() 메서드 포팅
|
||||
* 16바이트 블록 크기에 맞춰 패딩 추가
|
||||
*/
|
||||
private static addPadding(pBytes: Buffer): Buffer {
|
||||
const pCount = pBytes.length;
|
||||
const tCount = pCount + (16 - (pCount % 16));
|
||||
const tBytes = Buffer.alloc(tCount);
|
||||
|
||||
pBytes.copy(tBytes, 0);
|
||||
|
||||
// 나머지 바이트를 0x00으로 채움
|
||||
for (let rIndex = pCount; rIndex < tCount; rIndex++) {
|
||||
tBytes[rIndex] = 0x00;
|
||||
}
|
||||
|
||||
return tBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 Java removePadding() 메서드 포팅
|
||||
* 패딩 제거
|
||||
*/
|
||||
private static removePadding(pBytes: Buffer): Buffer {
|
||||
const pCount = pBytes.length;
|
||||
let index = 0;
|
||||
let loop = true;
|
||||
|
||||
while (loop) {
|
||||
if (index === pCount || pBytes[index] === 0x00) {
|
||||
loop = false;
|
||||
index--;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
const tBytes = Buffer.alloc(index);
|
||||
pBytes.copy(tBytes, 0, 0, index);
|
||||
|
||||
return tBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 Java toBytes() 메서드 포팅
|
||||
* 16진수 문자열을 바이트 배열로 변환
|
||||
*/
|
||||
private static toBytes(pSource: string): Buffer {
|
||||
const buff = pSource;
|
||||
const bCount = Math.floor(buff.length / 2);
|
||||
const bArr = Buffer.alloc(bCount);
|
||||
|
||||
for (let bIndex = 0; bIndex < bCount; bIndex++) {
|
||||
const hexByte = buff.substring(2 * bIndex, 2 * bIndex + 2);
|
||||
bArr[bIndex] = parseInt(hexByte, 16);
|
||||
}
|
||||
|
||||
return bArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 Java fromHex() 메서드 포팅
|
||||
* 바이트 배열을 16진수 문자열로 변환
|
||||
*/
|
||||
private static fromHex(pBytes: Buffer): string {
|
||||
const pCount = pBytes.length;
|
||||
let buff = "";
|
||||
|
||||
for (let pIndex = 0; pIndex < pCount; pIndex++) {
|
||||
const byte = pBytes[pIndex] & 0xff;
|
||||
if (byte < 0x10) {
|
||||
buff += "0";
|
||||
}
|
||||
buff += byte.toString(16);
|
||||
}
|
||||
|
||||
return buff;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테스트용 메서드 (개발 환경에서만 사용)
|
||||
*/
|
||||
static testEncryption(plainText: string): void {
|
||||
console.log("=== 암호화 테스트 ===");
|
||||
console.log("원본 텍스트:", plainText);
|
||||
|
||||
const encrypted = this.encrypt(plainText);
|
||||
console.log("암호화 결과:", encrypted);
|
||||
|
||||
const decrypted = this.decrypt(encrypted);
|
||||
console.log("복호화 결과:", decrypted);
|
||||
|
||||
const isMatch = this.matches(plainText, encrypted);
|
||||
console.log("비밀번호 일치:", isMatch);
|
||||
|
||||
const sha256Hash = this.encryptSha256(plainText);
|
||||
console.log("SHA256 해시:", sha256Hash);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user