389 lines
9.2 KiB
Markdown
389 lines
9.2 KiB
Markdown
|
|
# 📋 Phase 3.14: AuthService Raw Query 전환 계획
|
||
|
|
|
||
|
|
## 📋 개요
|
||
|
|
|
||
|
|
AuthService는 **5개의 Prisma 호출**이 있으며, 사용자 인증 및 권한 관리를 담당하는 서비스입니다.
|
||
|
|
|
||
|
|
### 📊 기본 정보
|
||
|
|
|
||
|
|
| 항목 | 내용 |
|
||
|
|
| --------------- | ------------------------------------------ |
|
||
|
|
| 파일 위치 | `backend-node/src/services/authService.ts` |
|
||
|
|
| 파일 크기 | 334 라인 |
|
||
|
|
| Prisma 호출 | 5개 |
|
||
|
|
| **현재 진행률** | **0/5 (0%)** 🔄 **진행 예정** |
|
||
|
|
| 복잡도 | 높음 (보안, 암호화, 세션 관리) |
|
||
|
|
| 우선순위 | 🟡 중간 (Phase 3.14) |
|
||
|
|
| **상태** | ⏳ **대기 중** |
|
||
|
|
|
||
|
|
### 🎯 전환 목표
|
||
|
|
|
||
|
|
- ⏳ **5개 모든 Prisma 호출을 `db.ts`의 `query()`, `queryOne()` 함수로 교체**
|
||
|
|
- ⏳ 사용자 인증 기능 정상 동작
|
||
|
|
- ⏳ 비밀번호 암호화/검증 유지
|
||
|
|
- ⏳ 세션 관리 기능 유지
|
||
|
|
- ⏳ 권한 검증 기능 유지
|
||
|
|
- ⏳ TypeScript 컴파일 성공
|
||
|
|
- ⏳ **Prisma import 완전 제거**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔍 예상 Prisma 사용 패턴
|
||
|
|
|
||
|
|
### 주요 기능 (5개 예상)
|
||
|
|
|
||
|
|
#### 1. **사용자 로그인 (인증)**
|
||
|
|
- findFirst or findUnique
|
||
|
|
- 이메일/사용자명으로 조회
|
||
|
|
- 비밀번호 검증
|
||
|
|
|
||
|
|
#### 2. **사용자 정보 조회**
|
||
|
|
- findUnique
|
||
|
|
- user_id 기준
|
||
|
|
- 권한 정보 포함
|
||
|
|
|
||
|
|
#### 3. **사용자 생성 (회원가입)**
|
||
|
|
- create
|
||
|
|
- 비밀번호 암호화
|
||
|
|
- 중복 검사
|
||
|
|
|
||
|
|
#### 4. **비밀번호 변경**
|
||
|
|
- update
|
||
|
|
- 기존 비밀번호 검증
|
||
|
|
- 새 비밀번호 암호화
|
||
|
|
|
||
|
|
#### 5. **세션 관리**
|
||
|
|
- create, update, delete
|
||
|
|
- 세션 토큰 저장/조회
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 💡 전환 전략
|
||
|
|
|
||
|
|
### 1단계: 인증 관련 전환 (2개)
|
||
|
|
- login() - 사용자 조회 + 비밀번호 검증
|
||
|
|
- getUserInfo() - 사용자 정보 조회
|
||
|
|
|
||
|
|
### 2단계: 사용자 관리 전환 (2개)
|
||
|
|
- createUser() - 사용자 생성
|
||
|
|
- changePassword() - 비밀번호 변경
|
||
|
|
|
||
|
|
### 3단계: 세션 관리 전환 (1개)
|
||
|
|
- manageSession() - 세션 CRUD
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 💻 전환 예시
|
||
|
|
|
||
|
|
### 예시 1: 로그인 (비밀번호 검증)
|
||
|
|
|
||
|
|
**변경 전**:
|
||
|
|
```typescript
|
||
|
|
async login(username: string, password: string) {
|
||
|
|
const user = await prisma.users.findFirst({
|
||
|
|
where: {
|
||
|
|
OR: [
|
||
|
|
{ username: username },
|
||
|
|
{ email: username },
|
||
|
|
],
|
||
|
|
is_active: true,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!user) {
|
||
|
|
throw new Error("User not found");
|
||
|
|
}
|
||
|
|
|
||
|
|
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
|
||
|
|
|
||
|
|
if (!isPasswordValid) {
|
||
|
|
throw new Error("Invalid password");
|
||
|
|
}
|
||
|
|
|
||
|
|
return user;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**변경 후**:
|
||
|
|
```typescript
|
||
|
|
async login(username: string, password: string) {
|
||
|
|
const user = await queryOne<any>(
|
||
|
|
`SELECT * FROM users
|
||
|
|
WHERE (username = $1 OR email = $1)
|
||
|
|
AND is_active = $2`,
|
||
|
|
[username, true]
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!user) {
|
||
|
|
throw new Error("User not found");
|
||
|
|
}
|
||
|
|
|
||
|
|
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
|
||
|
|
|
||
|
|
if (!isPasswordValid) {
|
||
|
|
throw new Error("Invalid password");
|
||
|
|
}
|
||
|
|
|
||
|
|
return user;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 예시 2: 사용자 생성 (비밀번호 암호화)
|
||
|
|
|
||
|
|
**변경 전**:
|
||
|
|
```typescript
|
||
|
|
async createUser(userData: CreateUserDto) {
|
||
|
|
// 중복 검사
|
||
|
|
const existing = await prisma.users.findFirst({
|
||
|
|
where: {
|
||
|
|
OR: [
|
||
|
|
{ username: userData.username },
|
||
|
|
{ email: userData.email },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
if (existing) {
|
||
|
|
throw new Error("User already exists");
|
||
|
|
}
|
||
|
|
|
||
|
|
// 비밀번호 암호화
|
||
|
|
const passwordHash = await bcrypt.hash(userData.password, 10);
|
||
|
|
|
||
|
|
// 사용자 생성
|
||
|
|
const user = await prisma.users.create({
|
||
|
|
data: {
|
||
|
|
username: userData.username,
|
||
|
|
email: userData.email,
|
||
|
|
password_hash: passwordHash,
|
||
|
|
company_code: userData.company_code,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
return user;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**변경 후**:
|
||
|
|
```typescript
|
||
|
|
async createUser(userData: CreateUserDto) {
|
||
|
|
// 중복 검사
|
||
|
|
const existing = await queryOne<any>(
|
||
|
|
`SELECT * FROM users
|
||
|
|
WHERE username = $1 OR email = $2`,
|
||
|
|
[userData.username, userData.email]
|
||
|
|
);
|
||
|
|
|
||
|
|
if (existing) {
|
||
|
|
throw new Error("User already exists");
|
||
|
|
}
|
||
|
|
|
||
|
|
// 비밀번호 암호화
|
||
|
|
const passwordHash = await bcrypt.hash(userData.password, 10);
|
||
|
|
|
||
|
|
// 사용자 생성
|
||
|
|
const user = await queryOne<any>(
|
||
|
|
`INSERT INTO users
|
||
|
|
(username, email, password_hash, company_code, created_at, updated_at)
|
||
|
|
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||
|
|
RETURNING *`,
|
||
|
|
[userData.username, userData.email, passwordHash, userData.company_code]
|
||
|
|
);
|
||
|
|
|
||
|
|
return user;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 예시 3: 비밀번호 변경
|
||
|
|
|
||
|
|
**변경 전**:
|
||
|
|
```typescript
|
||
|
|
async changePassword(
|
||
|
|
userId: number,
|
||
|
|
oldPassword: string,
|
||
|
|
newPassword: string
|
||
|
|
) {
|
||
|
|
const user = await prisma.users.findUnique({
|
||
|
|
where: { user_id: userId },
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!user) {
|
||
|
|
throw new Error("User not found");
|
||
|
|
}
|
||
|
|
|
||
|
|
const isOldPasswordValid = await bcrypt.compare(
|
||
|
|
oldPassword,
|
||
|
|
user.password_hash
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!isOldPasswordValid) {
|
||
|
|
throw new Error("Invalid old password");
|
||
|
|
}
|
||
|
|
|
||
|
|
const newPasswordHash = await bcrypt.hash(newPassword, 10);
|
||
|
|
|
||
|
|
await prisma.users.update({
|
||
|
|
where: { user_id: userId },
|
||
|
|
data: { password_hash: newPasswordHash },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**변경 후**:
|
||
|
|
```typescript
|
||
|
|
async changePassword(
|
||
|
|
userId: number,
|
||
|
|
oldPassword: string,
|
||
|
|
newPassword: string
|
||
|
|
) {
|
||
|
|
const user = await queryOne<any>(
|
||
|
|
`SELECT * FROM users WHERE user_id = $1`,
|
||
|
|
[userId]
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!user) {
|
||
|
|
throw new Error("User not found");
|
||
|
|
}
|
||
|
|
|
||
|
|
const isOldPasswordValid = await bcrypt.compare(
|
||
|
|
oldPassword,
|
||
|
|
user.password_hash
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!isOldPasswordValid) {
|
||
|
|
throw new Error("Invalid old password");
|
||
|
|
}
|
||
|
|
|
||
|
|
const newPasswordHash = await bcrypt.hash(newPassword, 10);
|
||
|
|
|
||
|
|
await query(
|
||
|
|
`UPDATE users
|
||
|
|
SET password_hash = $1, updated_at = NOW()
|
||
|
|
WHERE user_id = $2`,
|
||
|
|
[newPasswordHash, userId]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔧 기술적 고려사항
|
||
|
|
|
||
|
|
### 1. 비밀번호 보안
|
||
|
|
```typescript
|
||
|
|
import bcrypt from "bcrypt";
|
||
|
|
|
||
|
|
// 비밀번호 해싱 (회원가입, 비밀번호 변경)
|
||
|
|
const SALT_ROUNDS = 10;
|
||
|
|
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
|
||
|
|
|
||
|
|
// 비밀번호 검증 (로그인)
|
||
|
|
const isValid = await bcrypt.compare(plainPassword, passwordHash);
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. SQL 인젝션 방지
|
||
|
|
```typescript
|
||
|
|
// ❌ 위험: 직접 문자열 결합
|
||
|
|
const sql = `SELECT * FROM users WHERE username = '${username}'`;
|
||
|
|
|
||
|
|
// ✅ 안전: 파라미터 바인딩
|
||
|
|
const user = await queryOne(`SELECT * FROM users WHERE username = $1`, [username]);
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. 세션 토큰 관리
|
||
|
|
```typescript
|
||
|
|
import crypto from "crypto";
|
||
|
|
|
||
|
|
// 세션 토큰 생성
|
||
|
|
const sessionToken = crypto.randomBytes(32).toString("hex");
|
||
|
|
|
||
|
|
// 세션 저장
|
||
|
|
await query(
|
||
|
|
`INSERT INTO user_sessions (user_id, session_token, expires_at)
|
||
|
|
VALUES ($1, $2, NOW() + INTERVAL '1 day')`,
|
||
|
|
[userId, sessionToken]
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. 권한 검증
|
||
|
|
```typescript
|
||
|
|
async checkPermission(userId: number, permission: string): Promise<boolean> {
|
||
|
|
const result = await queryOne<{ has_permission: boolean }>(
|
||
|
|
`SELECT EXISTS (
|
||
|
|
SELECT 1 FROM user_permissions up
|
||
|
|
JOIN permissions p ON up.permission_id = p.permission_id
|
||
|
|
WHERE up.user_id = $1 AND p.permission_name = $2
|
||
|
|
) as has_permission`,
|
||
|
|
[userId, permission]
|
||
|
|
);
|
||
|
|
|
||
|
|
return result?.has_permission || false;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📝 전환 체크리스트
|
||
|
|
|
||
|
|
### 1단계: Prisma 호출 전환
|
||
|
|
- [ ] `login()` - 사용자 조회 + 비밀번호 검증 (findFirst)
|
||
|
|
- [ ] `getUserInfo()` - 사용자 정보 조회 (findUnique)
|
||
|
|
- [ ] `createUser()` - 사용자 생성 (create with 중복 검사)
|
||
|
|
- [ ] `changePassword()` - 비밀번호 변경 (findUnique + update)
|
||
|
|
- [ ] `manageSession()` - 세션 관리 (create/update/delete)
|
||
|
|
|
||
|
|
### 2단계: 보안 검증
|
||
|
|
- [ ] 비밀번호 해싱 로직 유지 (bcrypt)
|
||
|
|
- [ ] SQL 인젝션 방지 확인
|
||
|
|
- [ ] 세션 토큰 보안 확인
|
||
|
|
- [ ] 중복 계정 방지 확인
|
||
|
|
|
||
|
|
### 3단계: 테스트
|
||
|
|
- [ ] 단위 테스트 작성 (5개)
|
||
|
|
- [ ] 로그인 성공/실패 테스트
|
||
|
|
- [ ] 사용자 생성 테스트
|
||
|
|
- [ ] 비밀번호 변경 테스트
|
||
|
|
- [ ] 세션 관리 테스트
|
||
|
|
- [ ] 권한 검증 테스트
|
||
|
|
- [ ] 보안 테스트
|
||
|
|
- [ ] SQL 인젝션 테스트
|
||
|
|
- [ ] 비밀번호 강도 테스트
|
||
|
|
- [ ] 세션 탈취 방지 테스트
|
||
|
|
- [ ] 통합 테스트 작성 (2개)
|
||
|
|
|
||
|
|
### 4단계: 문서화
|
||
|
|
- [ ] 전환 완료 문서 업데이트
|
||
|
|
- [ ] 보안 가이드 업데이트
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎯 예상 난이도 및 소요 시간
|
||
|
|
|
||
|
|
- **난이도**: ⭐⭐⭐⭐ (높음)
|
||
|
|
- 보안 크리티컬 (비밀번호, 세션)
|
||
|
|
- SQL 인젝션 방지 필수
|
||
|
|
- 철저한 테스트 필요
|
||
|
|
|
||
|
|
- **예상 소요 시간**: 1.5~2시간
|
||
|
|
- Prisma 호출 전환: 40분
|
||
|
|
- 보안 검증: 40분
|
||
|
|
- 테스트: 40분
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ⚠️ 주의사항
|
||
|
|
|
||
|
|
### 보안 필수 체크리스트
|
||
|
|
1. ✅ 모든 사용자 입력은 파라미터 바인딩 사용
|
||
|
|
2. ✅ 비밀번호는 절대 평문 저장 금지 (bcrypt 사용)
|
||
|
|
3. ✅ 세션 토큰은 충분히 길고 랜덤해야 함
|
||
|
|
4. ✅ 비밀번호 실패 시 구체적 오류 메시지 금지 ("User not found" vs "Invalid credentials")
|
||
|
|
5. ✅ 로그인 실패 횟수 제한 (Brute Force 방지)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**상태**: ⏳ **대기 중**
|
||
|
|
**특이사항**: 보안 크리티컬, 비밀번호 암호화, 세션 관리 포함
|
||
|
|
**⚠️ 주의**: 이 서비스는 보안에 매우 중요하므로 신중한 테스트 필수!
|
||
|
|
|