관리자 메뉴 토큰문제 수정정
This commit is contained in:
@@ -836,6 +836,180 @@ export const logger = winston.createLogger({
|
||||
11. **메뉴 API 완료**: `/api/admin/menus`와 `/api/admin/user-menus` API가 성공적으로 구현되어 프론트엔드 메뉴 표시가 정상 작동
|
||||
12. **JWT 토큰 관리**: 프론트엔드 API 클라이언트에서 JWT 토큰을 자동으로 포함하여 인증 문제 해결
|
||||
13. **환경변수 관리**: Prisma 스키마에서 직접 데이터베이스 URL 설정으로 환경변수 로딩 문제 해결
|
||||
14. **어드민 메뉴 인증**: 새 탭에서 열리는 어드민 페이지의 토큰 인증 문제 해결 - localStorage 공유 활용
|
||||
|
||||
## 🔐 인증 및 보안 가이드
|
||||
|
||||
### 어드민 메뉴 토큰 인증 문제 해결
|
||||
|
||||
#### 문제 상황
|
||||
|
||||
- 어드민 버튼 클릭 시 새 탭에서 어드민 페이지가 열림
|
||||
- 새 탭에서 토큰 인증 문제 발생 가능성
|
||||
- URL 파라미터로 토큰 전달은 보안상 위험
|
||||
|
||||
#### 해결 방안 (권장)
|
||||
|
||||
**1. localStorage 공유 활용 (가장 간단)**
|
||||
|
||||
```typescript
|
||||
// AdminButton.tsx - 수정 없음
|
||||
const handleAdminClick = () => {
|
||||
const adminUrl = `${window.location.origin}/admin`;
|
||||
window.open(adminUrl, "_blank");
|
||||
};
|
||||
|
||||
// admin/page.tsx - AuthGuard 적용
|
||||
("use client");
|
||||
import { AuthGuard } from "@/components/auth/AuthGuard";
|
||||
import { CompanyManagement } from "@/components/admin/CompanyManagement";
|
||||
|
||||
export default function AdminPage() {
|
||||
return (
|
||||
<AuthGuard requireAdmin={true}>
|
||||
<CompanyManagement />
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**2. BroadcastChannel API 활용 (고급)**
|
||||
|
||||
```typescript
|
||||
// utils/tabCommunication.ts
|
||||
export class TabCommunication {
|
||||
private channel: BroadcastChannel;
|
||||
|
||||
constructor() {
|
||||
this.channel = new BroadcastChannel("auth-channel");
|
||||
}
|
||||
|
||||
// 토큰 요청
|
||||
requestToken(): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
resolve(localStorage.getItem("authToken"));
|
||||
}, 100);
|
||||
|
||||
this.channel.postMessage({ type: "REQUEST_TOKEN" });
|
||||
|
||||
const handler = (event: MessageEvent) => {
|
||||
if (event.data.type === "TOKEN_RESPONSE") {
|
||||
clearTimeout(timeout);
|
||||
this.channel.removeEventListener("message", handler);
|
||||
resolve(event.data.token);
|
||||
}
|
||||
};
|
||||
|
||||
this.channel.addEventListener("message", handler);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. 쿠키 기반 토큰 (가장 안전)**
|
||||
|
||||
```typescript
|
||||
// backend-node/src/controllers/authController.ts
|
||||
static async login(req: Request, res: Response): Promise<void> {
|
||||
// HTTPOnly 쿠키로 토큰 설정
|
||||
res.cookie('authToken', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24시간
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### 보안 고려사항
|
||||
|
||||
1. **URL 파라미터 사용 금지**: 토큰이 URL에 노출되어 보안 위험
|
||||
2. **HTTPS 필수**: 프로덕션 환경에서는 반드시 HTTPS 사용
|
||||
3. **토큰 만료 처리**: 자동 갱신 또는 재로그인 유도
|
||||
4. **CSRF 방지**: 토큰 기반 요청 검증
|
||||
5. **로그아웃 처리**: 모든 탭에서 토큰 제거
|
||||
|
||||
#### 구현 우선순위
|
||||
|
||||
**1단계 (즉시 적용)**
|
||||
|
||||
- AuthGuard를 사용한 어드민 페이지 보호
|
||||
- localStorage 공유 활용
|
||||
|
||||
**2단계 (1-2일 내)**
|
||||
|
||||
- 토큰 유효성 검증 API 추가
|
||||
- 에러 처리 개선
|
||||
|
||||
**3단계 (3-5일 내)**
|
||||
|
||||
- 세션 관리 개선
|
||||
- 토큰 갱신 로직 추가
|
||||
|
||||
### JWT 토큰 관리 모범 사례
|
||||
|
||||
#### 프론트엔드 토큰 관리
|
||||
|
||||
```typescript
|
||||
// lib/api/client.ts
|
||||
const TokenManager = {
|
||||
getToken: (): string | null => {
|
||||
if (typeof window !== "undefined") {
|
||||
return localStorage.getItem("authToken");
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
isTokenExpired: (token: string): boolean => {
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split(".")[1]));
|
||||
return payload.exp * 1000 < Date.now();
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
#### 백엔드 토큰 검증
|
||||
|
||||
```typescript
|
||||
// middleware/authMiddleware.ts
|
||||
export const authenticateToken = (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void => {
|
||||
try {
|
||||
const authHeader = req.get("Authorization");
|
||||
const token = authHeader && authHeader.split(" ")[1];
|
||||
|
||||
if (!token) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: "TOKEN_MISSING",
|
||||
details: "인증 토큰이 필요합니다.",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const userInfo: PersonBean = JwtUtils.verifyToken(token);
|
||||
req.user = userInfo;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: "INVALID_TOKEN",
|
||||
details: "토큰 검증에 실패했습니다.",
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 🎯 성공 지표
|
||||
|
||||
@@ -847,6 +1021,6 @@ export const logger = winston.createLogger({
|
||||
---
|
||||
|
||||
**마지막 업데이트**: 2024년 12월 20일
|
||||
**버전**: 1.7.0
|
||||
**버전**: 1.8.0
|
||||
**작성자**: AI Assistant
|
||||
**현재 상태**: Phase 1 완료, Phase 2-1A 완료, Phase 2-1B 완료, Phase 2-2A 완료 ✅ (메뉴 API 구현 완료)
|
||||
**현재 상태**: Phase 1 완료, Phase 2-1A 완료, Phase 2-1B 완료, Phase 2-2A 완료 ✅ (메뉴 API 구현 완료, 어드민 메뉴 인증 문제 해결)
|
||||
|
||||
Reference in New Issue
Block a user