288 lines
7.3 KiB
Markdown
288 lines
7.3 KiB
Markdown
# 서비스별 인증 처리 리팩토링 가이드
|
|
|
|
## 1) 목적과 원칙
|
|
|
|
여러 서비스 앱에 흩어진 인증 로직을 `@common/auth` 기반으로 통일하기 위한 표준 가이드입니다.
|
|
이 문서를 **위에서 아래로** 적용하면 `authService` + `AuthWrapper` 패턴이 갖춰집니다.
|
|
|
|
핵심 원칙:
|
|
|
|
- 인증 **정책·유틸**은 `@common/auth`에 모은다.
|
|
- 서비스별 차이는 `authConfig` 설정으로만 표현한다.
|
|
- 앱은 API 오케스트레이션(`authService`)과 진입점(`AuthWrapper`)만 구현한다.
|
|
- `login` / `silentRefresh` 성공 시 토큰을 저장한다.
|
|
- 세션 연장은 confirm **확인** 시 `silentRefresh`로 수행한다.
|
|
→ [세션 연장 Confirm 적용 가이드](./session-extend-confirm-guide.md)
|
|
|
|
참고 구현: `apps/as/src/entities/auth/api/auth-service.ts`
|
|
|
|
---
|
|
|
|
## 2) 목표 아키텍처
|
|
|
|
### 2-1. 레이어 책임
|
|
|
|
| 레이어 | 경로 | 역할 |
|
|
| --- | --- | --- |
|
|
| 환경 변수 | `shared/config/env.ts` | `CLAIM_NAME` 등 키/도메인 값 노출 |
|
|
| 인증 설정 | `shared/config/auth.ts` | env → `AuthConfig` 매핑 |
|
|
| 토큰 매니저 | `entities/auth/lib/token-manager.ts` | `createTokenManager(authConfig)` + re-export |
|
|
| 인증 서비스 | `entities/auth/api/auth-service.ts` | login / silentRefresh / declineSessionExtend / logout |
|
|
| 인증 래퍼 | `entities/auth/lib/auth-wrapper.tsx` | auth 체크, `auth:logout` 수신 |
|
|
| 배럴 | `entities/auth/index.ts` | 외부 import 경로 통일 |
|
|
| 레이아웃 | `BaseLayout` 등 | `authService`를 Header까지 전달 |
|
|
|
|
### 2-2. 런타임 흐름
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[login / silentRefresh 성공] --> B[setToken]
|
|
C[세션 연장 confirm 확인] --> D[silentRefresh]
|
|
D --> B
|
|
E[세션 연장 confirm 취소] --> F[declineSessionExtend]
|
|
F --> G[clearRefreshTimer]
|
|
H[갱신 실패 / logout] --> I[clearAuthToken + auth:logout]
|
|
```
|
|
|
|
세션 연장 confirm UX는 [세션 연장 가이드](./session-extend-confirm-guide.md)에서 적용한다.
|
|
|
|
---
|
|
|
|
## 3) 공통 계약
|
|
|
|
### 3-1. AuthConfig
|
|
|
|
```ts
|
|
interface AuthConfig {
|
|
authUserId: string;
|
|
claimName: string;
|
|
cookieDomain?: string;
|
|
accessToken?: string;
|
|
refreshToken?: string;
|
|
}
|
|
```
|
|
|
|
### 3-2. AuthService
|
|
|
|
```ts
|
|
interface AuthService {
|
|
login(payload: AuthPayload): Promise<void>;
|
|
silentRefresh(): Promise<void>;
|
|
declineSessionExtend(): void;
|
|
logout(): Promise<void>;
|
|
}
|
|
```
|
|
|
|
### 3-3. TokenManager API
|
|
|
|
앱에서 사용하는 메서드:
|
|
|
|
| 메서드 | 용도 |
|
|
| --- | --- |
|
|
| `setToken` / `getToken` | 쿠키 저장·조회 |
|
|
| `clearRefreshTimer` | `declineSessionExtend`에 연결 |
|
|
| `clearAuthToken` | 인증 쿠키 일괄 삭제 |
|
|
|
|
---
|
|
|
|
## 4) 적용 순서
|
|
|
|
의존 관계 순으로 진행한다.
|
|
|
|
### 4-1. 환경 변수 통일
|
|
|
|
파일: `shared/config/env.ts`
|
|
|
|
```ts
|
|
export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
|
|
export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
|
|
export const ACCESS_TOKEN =
|
|
process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token";
|
|
export const REFRESH_TOKEN =
|
|
process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token";
|
|
export const COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN;
|
|
```
|
|
|
|
### 4-2. 인증 설정
|
|
|
|
파일: `shared/config/auth.ts`
|
|
|
|
```ts
|
|
import type { AuthConfig } from "@common/auth";
|
|
import {
|
|
ACCESS_TOKEN,
|
|
AUTH_USER_ID,
|
|
CLAIM_NAME,
|
|
COOKIE_DOMAIN,
|
|
REFRESH_TOKEN,
|
|
} from "./env";
|
|
|
|
export const authConfig: AuthConfig = {
|
|
authUserId: AUTH_USER_ID,
|
|
claimName: CLAIM_NAME,
|
|
cookieDomain: COOKIE_DOMAIN,
|
|
accessToken: ACCESS_TOKEN,
|
|
refreshToken: REFRESH_TOKEN,
|
|
};
|
|
```
|
|
|
|
### 4-3. 토큰 매니저
|
|
|
|
파일: `entities/auth/lib/token-manager.ts`
|
|
|
|
```ts
|
|
import { authConfig } from "@/shared/config/auth";
|
|
import { createTokenManager } from "@common/auth";
|
|
|
|
export const tokenManager = createTokenManager(authConfig);
|
|
|
|
export const {
|
|
getToken,
|
|
setToken,
|
|
removeToken,
|
|
clearRefreshTimer,
|
|
getAccessToken,
|
|
getRefreshToken,
|
|
getUserId,
|
|
} = tokenManager;
|
|
```
|
|
|
|
### 4-4. 인증 서비스
|
|
|
|
파일: `entities/auth/api/auth-service.ts`
|
|
참고: `apps/as/src/entities/auth/api/auth-service.ts`
|
|
|
|
#### login
|
|
|
|
```ts
|
|
setToken(CLAIM_NAME, accessToken);
|
|
setToken(AUTH_USER_ID, tokenId);
|
|
```
|
|
|
|
#### silentRefresh
|
|
|
|
```ts
|
|
async function silentRefresh() {
|
|
try {
|
|
// SILENT_REFRESH API → setToken(...)
|
|
} catch (error) {
|
|
// clearAuthToken은 내부에서 this를 쓰므로 tokenManager 객체로 호출
|
|
tokenManager.clearAuthToken();
|
|
if (typeof window !== "undefined") {
|
|
window.dispatchEvent(new CustomEvent("auth:logout"));
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### declineSessionExtend
|
|
|
|
```ts
|
|
declineSessionExtend: clearRefreshTimer,
|
|
```
|
|
|
|
| 사용자 선택 | 호출 |
|
|
| --- | --- |
|
|
| confirm **확인** | `silentRefresh()` |
|
|
| confirm **취소** | `declineSessionExtend()` |
|
|
|
|
#### logout
|
|
|
|
```ts
|
|
async function logout() {
|
|
tokenManager.clearAuthToken();
|
|
clearDeviceId();
|
|
|
|
if (typeof window !== "undefined") {
|
|
window.dispatchEvent(new CustomEvent("auth:logout"));
|
|
}
|
|
}
|
|
```
|
|
|
|
httpOnly refresh 쿠키 삭제가 필요하면 `fetch("/api/auth/logout")`를 먼저 호출한다 (`apps/st` 참고).
|
|
|
|
#### export
|
|
|
|
```ts
|
|
export const authService: AuthService = {
|
|
login,
|
|
silentRefresh,
|
|
declineSessionExtend: clearRefreshTimer,
|
|
logout,
|
|
};
|
|
```
|
|
|
|
### 4-5. AuthWrapper
|
|
|
|
파일: `entities/auth/lib/auth-wrapper.tsx`
|
|
인증 체크와 `auth:logout` 수신을 구현한다.
|
|
|
|
```tsx
|
|
useEffect(() => {
|
|
// authCheck, 로그인 리다이렉트
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
function handleAuthLogout() {
|
|
setUserInfo(null);
|
|
if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
|
|
router.replace(AUTH_LOGIN_REDIRECT_URL);
|
|
}
|
|
}
|
|
|
|
window.addEventListener("auth:logout", handleAuthLogout);
|
|
return () => window.removeEventListener("auth:logout", handleAuthLogout);
|
|
}, []);
|
|
```
|
|
|
|
### 4-6. 배럴
|
|
|
|
파일: `entities/auth/index.ts`
|
|
|
|
```ts
|
|
export { authService } from "./api/auth-service";
|
|
export { AuthWrapper } from "./lib/auth-wrapper";
|
|
export { tokenManager } from "./lib/token-manager";
|
|
```
|
|
|
|
### 4-7. 레이아웃·Provider 연결
|
|
|
|
```tsx
|
|
import { authService } from "@/entities/auth";
|
|
|
|
<BaseLayout authService={authService} /* ... */>
|
|
{children}
|
|
</BaseLayout>
|
|
```
|
|
|
|
`AppProvider`에 `AuthWrapper`를 마운트한다.
|
|
|
|
---
|
|
|
|
## 5) 적용 체크리스트
|
|
|
|
- [ ] `env.ts`에 인증 관련 환경 변수가 정의되어 있다
|
|
- [ ] `authConfig`가 `AuthConfig`로 매핑되어 있다
|
|
- [ ] `createTokenManager(authConfig)`로 tokenManager를 생성한다
|
|
- [ ] `authService`가 `AuthService` 네 메서드를 구현한다
|
|
- [ ] `entities/auth` 배럴로 외부 참조가 통일되어 있다
|
|
- [ ] `BaseLayout`에 `authService`가 주입된다
|
|
- [ ] `AppProvider`에 `AuthWrapper`가 마운트되어 있다
|
|
|
|
---
|
|
|
|
## 6) 후속 단계
|
|
|
|
위 체크리스트 통과 후 [세션 연장 Confirm 적용 가이드](./session-extend-confirm-guide.md)를 진행한다.
|
|
|
|
---
|
|
|
|
## 7) 참고 구현
|
|
|
|
| 앱 | auth-service |
|
|
| --- | --- |
|
|
| as (기준) | `apps/as/src/entities/auth/api/auth-service.ts` |
|
|
| cs | `apps/cs/src/entities/auth/api/auth-service.ts` |
|
|
| st | `apps/st/src/entities/auth/api/auth-service.ts` |
|
|
| cm | `apps/cm/src/entities/auth/api/auth-service.ts` |
|