SDK 연동 가이드
fp-v1

Device Fingerprint SDK — 타 사이트 적용 가이드

버전: fp-v1 | 최종 수정: 2026-03-30

대상 독자: 이 SDK를 자신의 웹서비스에 연동하려는 개발자


목차

  1. SDK 개요
  2. 동작 원리
  3. 수집 데이터 목록
  4. 식별 키 구조
  5. 리스크 플래그 및 점수
  6. Payload 전체 구조
  7. 연동 방법 A — Script 태그 (바닐라 JS)
  8. 연동 방법 B — ES Module / NPM
  9. 연동 방법 C — React / Next.js
  10. 연동 방법 D — Vue 3
  11. 연동 방법 E — 서버 측 수신 API 구현
  12. SDK 옵션 레퍼런스
  13. API 메서드 레퍼런스
  14. 이벤트 타입 설계 가이드
  15. 보안 및 개인정보 고려사항
  16. 자주 묻는 질문 (FAQ)
  17. 문제 해결

1. SDK 개요

Device Fingerprint SDK는 브라우저 환경 정보를 수집하고 기기 고유 식별 키를 생성하는 클라이언트 사이드 JavaScript 라이브러리입니다.

주요 기능

기능설명
브라우저 지문 수집Canvas, WebGL, Audio, navigator 등 30+ 신호 수집
stableKey 생성기기 고유 식별자 (SHA-256, 세션 무관 안정적)
sessionKey 생성이벤트·세션 컨텍스트 포함 식별자
리스크 평가headless/자동화 탐지 8개 플래그, 0~100점
이벤트 추적page_view, login, important_transaction 등
선택적 API 전송수집 결과를 지정 엔드포인트로 POST

지원 환경

환경지원 여부
Chrome 90+
Firefox 90+
Safari 14+
Edge 90+
IE 11❌ (Web Crypto API 미지원)
Node.js (SSR)❌ 브라우저 전용 — 반드시 클라이언트에서 호출

2. 동작 원리

code
브라우저 이벤트 발생
       │
       ▼
collector.collect("login")
       │
       ├─ getBasicFeatures()    ← navigator, screen, window, storage
       ├─ getCanvasFingerprint() ← Canvas 2D 렌더링 → SHA-256
       ├─ getWebGLFingerprint()  ← GPU 벤더/렌더러 + 3D 렌더링 해시
       └─ getAudioFingerprint()  ← OfflineAudioContext 처리 결과 해시
                │
                ▼
         normalizeFeatures()    ← null 제거, 소문자화, 포맷 통일
                │
                ▼
           buildKeys()
         ├─ stableKey = SHA-256(기기 특성 16개)
         └─ sessionKey = SHA-256(stableKey + 이벤트 컨텍스트)
                │
                ▼
         evaluateRisk()         ← 8개 플래그 판정, 0~100점 산출
                │
                ▼
         FingerprintPayload     ← 최종 객체 반환
                │
                ▼ (endpoint 지정 시)
       POST /api/fingerprint/collect

중요: 모든 해시 생성은 브라우저 내장 Web Crypto API (`crypto.subtle.digest`)를 사용합니다. 외부 암호화 라이브러리 의존성이 없습니다.


3. 수집 데이터 목록

범례

표기의미
SstableKey 해시 입력값으로 사용
SEsessionKey 해시 입력값으로 사용
S + SE두 키 모두에 사용
_(없음)_수집만 함 (리스크 평가 등에 활용)

기본 브라우저 정보

필드수집 방법설명키 사용
userAgentnavigator.userAgent브라우저/OS 식별 문자열
languagenavigator.language기본 언어 (예: "ko")S
languagesnavigator.languages언어 목록 배열S
platformnavigator.platformOS 플랫폼 (예: "MacIntel")S
cookieEnablednavigator.cookieEnabled쿠키 사용 가능 여부
hardwareConcurrencynavigator.hardwareConcurrencyCPU 논리 코어 수S
deviceMemorynavigator.deviceMemory장착 메모리 GB (근사값)S
maxTouchPointsnavigator.maxTouchPoints최대 터치 포인트 수S
webdrivernavigator.webdriver자동화 도구 여부
doNotTracknavigator.doNotTrackDNT 설정
timezoneIntl.DateTimeFormat().resolvedOptions().timeZoneIANA 시간대S
timezoneOffsetnew Date().getTimezoneOffset()UTC 오프셋 (분)S + SE

화면 / 창 정보

필드수집 방법설명키 사용
screenWidth/Heightscreen.width/height물리 화면 해상도S
screenAvailWidth/Heightscreen.availWidth/Height실제 사용 가능 화면 크기
screenColorDepthscreen.colorDepth색상 깊이 (비트)S
devicePixelRatiowindow.devicePixelRatio디바이스 픽셀 밀도S
windowInnerWidth/Heightwindow.innerWidth/Height뷰포트 크기
windowOuterWidth/Heightwindow.outerWidth/Height브라우저 창 전체 크기

스토리지 / 환경

필드수집 방법설명키 사용
localStorageAvailable쓰기/삭제 테스트localStorage 접근 가능 여부SE
sessionStorageAvailable쓰기/삭제 테스트sessionStorage 접근 가능 여부SE
pluginsLengthnavigator.plugins.length설치된 플러그인 수SE
prefersColorSchemeDarkmatchMedia("(prefers-color-scheme: dark)")다크모드 설정
prefersReducedMotionmatchMedia("(prefers-reduced-motion: reduce)")모션 감소 설정
notificationPermissionNotification.permission알림 권한 상태

페이지 컨텍스트

필드수집 방법설명키 사용
hrefwindow.location.href현재 페이지 전체 URLSE
hostnamewindow.location.hostname호스트명SE
referrerdocument.referrer이전 페이지 URL
visibilityStatedocument.visibilityState탭 가시성 상태SE

렌더링 지문 (고유성이 높은 신호)

필드수집 방법설명키 사용
canvasHashCanvas 2D 복잡한 도형/텍스트 렌더링 → SHA-256GPU·폰트 렌더링 차이S
webglVendorWEBGL_debug_renderer_info 확장GPU 제조사S
webglRendererWEBGL_debug_renderer_info 확장GPU 모델명S
webglImageHashWebGL 3D 삼각형 렌더링 → 픽셀 샘플 → SHA-256GPU 렌더링 차이S
audioHashOfflineAudioContext oscillator+compressor → SHA-256오디오 엔진 처리 차이S

모든 수집 항목은 try/catch로 감싸여 있습니다. 브라우저 제한이나 권한 거부 시 해당 필드는 null로 반환되며 앱이 중단되지 않습니다.


4. 식별 키 구조

stableKey (기기 고유 키)

기기와 브라우저 특성만을 기반으로 생성합니다. 세션이 바뀌어도, 날짜가 달라도 동일한 기기/브라우저라면 같은 값이 나옵니다.

code
stableKey = SHA-256(
  language | languages | platform | timezone | timezoneOffset |
  screenResolution | colorDepth | devicePixelRatio |
  hardwareConcurrency | deviceMemory | maxTouchPoints |
  canvasHash | webglVendor | webglRenderer | webglImageHash | audioHash
)

활용 예시: 로그아웃 후 재로그인 시 같은 기기 여부 확인, 기기 변경 탐지

sessionKey (이벤트 컨텍스트 키)

stableKey 위에 현재 페이지·이벤트 컨텍스트를 추가하여 생성합니다. 같은 기기여도 페이지나 이벤트가 다르면 다른 값이 나옵니다.

code
sessionKey = SHA-256(
  stableKey | hostname | href |
  visibilityState | localStorageAvailable | sessionStorageAvailable |
  pluginsLength | timezoneOffset | eventType | YYYY-MM (월 버킷)
)

활용 예시: 동일 세션 내 동일 페이지에서 행동 패턴 추적, 세션 하이재킹 탐지


5. 리스크 플래그 및 점수

각 플래그가 탐지되면 점수가 누적됩니다. 최종 점수는 0~100으로 제한됩니다.

플래그가중치탐지 조건
webdriverDetected+40navigator.webdriver === true (Selenium/Puppeteer)
headlessLikeSignals+30webdriver + no-plugins + headless UA 등 2개 이상 복합 탐지
touchUAMismatch+15maxTouchPoints > 5이지만 모바일 UA 없음
suspiciousNoStorage+10localStorage와 sessionStorage 모두 차단
unusualPluginState+10plugins.length === 0 (headless 환경 기본값)
timezoneLanguageMismatch+10언어 코드와 시간대 조합이 비정상적
screenWindowMismatch+10창 크기가 화면보다 크거나 100px 이하
unusualWebGLState+10WebGL 벤더/렌더러 정보 없음

점수 해석 기준

점수 범위위험도권장 조치
0 ~ 30낮음 (정상)정상 처리
30 ~ 70중간추가 인증 또는 모니터링 강화
70 ~ 100높음 (위험)거래 차단 또는 OTP 추가 인증 요구

6. Payload 전체 구조

collector.collect(eventType) 호출 시 반환되는 객체의 전체 타입 및 예시입니다.

typescript
// TypeScript 타입 정의
interface FingerprintPayload {
  version: "fp-v1";          // SDK 버전 (서버 파싱 시 분기 기준)
  appId: string;             // 초기화 시 지정한 앱 ID
  eventType: string;         // "page_view" | "login" | "important_transaction" | 커스텀
  collectedAt: string;       // ISO 8601 타임스탬프 (예: "2024-01-15T10:30:00.000Z")

  page: {
    href: string | null;     // 현재 페이지 URL 전체
    hostname: string | null; // 호스트명 (예: "example.com")
    referrer: string | null; // 이전 페이지 URL
  };

  raw: RawFeatures;          // 수집된 원시 데이터 (30+ 필드)
  normalized: NormalizedFeatures; // 정규화된 데이터 (해시 생성에 사용된 입력값)

  keys: {
    stableKey: string;       // 기기 고유 키 (64자 hex SHA-256)
    sessionKey: string;      // 세션 컨텍스트 키 (64자 hex SHA-256)
  };

  risk: {
    score: number;           // 0~100 리스크 점수
    flags: {
      webdriverDetected: boolean;
      suspiciousNoStorage: boolean;
      unusualPluginState: boolean;
      timezoneLanguageMismatch: boolean;
      touchUAMismatch: boolean;
      screenWindowMismatch: boolean;
      headlessLikeSignals: boolean;
      unusualWebGLState: boolean;
    };
    details: string[];       // 각 플래그 탐지 이유 설명 배열
  };
}

실제 예시값 (정상 크롬 브라우저):

json
{
  "version": "fp-v1",
  "appId": "my-banking-app",
  "eventType": "login",
  "collectedAt": "2024-01-15T10:30:00.123Z",
  "page": {
    "href": "https://bank.example.com/login",
    "hostname": "bank.example.com",
    "referrer": ""
  },
  "keys": {
    "stableKey": "a3f9c2e1d4b5f6789abcdef0123456789abcdef0123456789abcdef01234567",
    "sessionKey": "7b4d9a2c1e3f5678def012345abcdef0123456789abcdef0123456789abcde"
  },
  "risk": {
    "score": 0,
    "flags": {
      "webdriverDetected": false,
      "suspiciousNoStorage": false,
      "unusualPluginState": false,
      "timezoneLanguageMismatch": false,
      "touchUAMismatch": false,
      "screenWindowMismatch": false,
      "headlessLikeSignals": false,
      "unusualWebGLState": false
    },
    "details": []
  }
}

7. 연동 방법 A — Script 태그 (바닐라 JS)

Next.js 프로젝트를 빌드하면 SDK를 포함한 번들이 생성됩니다.

현재는 Next.js 앱 자체를 호스팅하거나, 추후 별도 UMD 번들을 빌드하여 <script> 태그로 삽입하는 방식을 사용합니다.

7-1. 최소 적용 (인라인 스크립트)

html
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>My Service</title>
</head>
<body>

  <button id="login-btn">로그인</button>
  <button id="transfer-btn">이체 실행</button>

  <!--
    TODO: 빌드된 SDK 번들을 삽입하거나
          Next.js 앱의 API만 별도로 호스팅하여 사용
    <script src="https://your-host.com/sdk/fingerprint-sdk.umd.js"></script>
  -->

  <script>
    // ──────────────────────────────────────────────────
    // SDK를 직접 인라인으로 초기화하는 예시
    // (실제로는 번들 파일을 script src로 삽입)
    // ──────────────────────────────────────────────────

    // 1. SDK 초기화 (페이지 로드 시 1회)
    const collector = FingerprintSDK.createDeviceCollector({
      // 수집 결과를 보낼 서버 API 엔드포인트
      // 없으면 로컬에서만 payload를 사용
      endpoint: "https://your-api.example.com/api/fingerprint/collect",

      // 서비스 식별자 (서버에서 멀티 앱 구분에 사용)
      appId: "my-service-prod",

      // 개발 시 true로 설정하면 콘솔에 상세 로그 출력
      debug: false,

      // 페이지 진입 시 자동으로 page_view 이벤트 수집
      autoCollectPageView: true,

      // 수집 완료 시 콜백
      onCollected: function(payload) {
        console.log("[FP] 수집 완료:", payload.keys.stableKey.slice(0, 16));
        console.log("[FP] 리스크 점수:", payload.risk.score);

        // 고위험 탐지 시 즉시 대응
        if (payload.risk.score >= 70) {
          showSecurityAlert();
        }
      },

      // 수집 실패 시 콜백 (앱은 정상 동작 계속)
      onError: function(err) {
        console.warn("[FP] 수집 오류 (무시):", err.message);
      }
    });

    // 2. 로그인 버튼 이벤트 연결
    document.getElementById("login-btn").addEventListener("click", async function() {
      try {
        const payload = await collector.collect("login");

        // 로그인 API 요청에 기기 식별 키 포함
        const response = await fetch("/api/auth/login", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            username: document.getElementById("username").value,
            password: document.getElementById("password").value,
            // 기기 정보 추가
            deviceKey: payload.keys.stableKey,
            riskScore: payload.risk.score,
            riskFlags: payload.risk.flags
          })
        });

        const result = await response.json();
        if (result.success) {
          window.location.href = "/dashboard";
        }

      } catch (err) {
        // SDK 오류가 나도 로그인은 계속 진행 (fail-open)
        console.warn("지문 수집 실패, 로그인은 계속:", err);
        proceedWithLogin();
      }
    });

    // 3. 이체 버튼 이벤트 연결
    document.getElementById("transfer-btn").addEventListener("click", async function() {
      try {
        const payload = await collector.collect("important_transaction");

        // 리스크 점수 기반 추가 인증
        if (payload.risk.score >= 50) {
          const confirmed = confirm(
            "이상 기기 신호가 탐지되었습니다. 추가 인증이 필요합니다. 계속하시겠습니까?"
          );
          if (!confirmed) return;
          await requestOTP();
        }

        await processTransfer({ deviceKey: payload.keys.stableKey });

      } catch (err) {
        console.warn("지문 수집 실패:", err);
        // 보안 정책에 따라 실패 시 차단 or 통과 선택
        // fail-closed (보수적): throw err;
        // fail-open (가용성 우선): await processTransfer({});
      }
    });

    function showSecurityAlert() {
      document.body.insertAdjacentHTML("afterbegin",
        '<div style="background:red;color:white;padding:10px;text-align:center">' +
        '⚠ 보안 위협이 탐지되었습니다. 관리자에게 문의하세요.' +
        '</div>'
      );
    }
  </script>
</body>
</html>

8. 연동 방법 B — ES Module / NPM

8-1. 파일 복사 방식

현재 SDK는 src/lib/fingerprint-sdk/ 디렉터리 전체를 복사하여 사용할 수 있습니다.

bash
# 상대 경로는 환경에 맞게 수정
cp -r browserfp/src/lib/fingerprint-sdk ./your-project/src/lib/
cp browserfp/src/lib/diff.ts ./your-project/src/lib/

8-2. 기본 사용 예시

typescript
// src/services/fingerprint.ts
import { createDeviceCollector } from "@/lib/fingerprint-sdk";
import type { FingerprintPayload, EventType } from "@/lib/fingerprint-sdk";

// 싱글톤 인스턴스 (앱 전역에서 1개만 생성)
export const fpCollector = createDeviceCollector({
  endpoint: process.env.NEXT_PUBLIC_FP_ENDPOINT ?? "/api/fingerprint/collect",
  appId: process.env.NEXT_PUBLIC_APP_ID ?? "my-app",
  debug: process.env.NODE_ENV === "development",
  autoCollectPageView: true,
  onCollected: (payload: FingerprintPayload) => {
    // 분석 도구에 stableKey 전달 (선택)
    if (typeof window.analytics !== "undefined") {
      window.analytics.identify(payload.keys.stableKey);
    }
  },
});

// 편의 함수
export async function collectEvent(eventType: EventType): Promise<FingerprintPayload> {
  return fpCollector.collect(eventType);
}

8-3. 로그인 플로우에 통합

typescript
// src/services/auth.ts
import { collectEvent } from "./fingerprint";

export async function login(username: string, password: string): Promise<void> {
  // 로그인 시점 지문 수집 (실패해도 로그인 진행)
  let deviceKey: string | undefined;
  let riskScore: number | undefined;

  try {
    const payload = await collectEvent("login");
    deviceKey = payload.keys.stableKey;
    riskScore = payload.risk.score;

    // 고위험 자동화 탐지 시 사전 차단
    if (payload.risk.flags.webdriverDetected) {
      throw new Error("자동화 도구가 탐지되었습니다. 접근이 차단됩니다.");
    }
  } catch (fpError) {
    // SDK 오류는 로그만 남기고 로그인은 진행 (fail-open 정책)
    console.warn("[Auth] 지문 수집 실패:", fpError);
  }

  // 서버에 로그인 요청
  const response = await fetch("/api/auth/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      username,
      password,
      deviceKey,   // stableKey (서버에서 기기 이력 조회에 사용)
      riskScore,   // 서버에서 정책 적용
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message ?? "로그인 실패");
  }
}

9. 연동 방법 C — React / Next.js

9-1. 커스텀 훅

typescript
// src/hooks/useFingerprint.ts
"use client"; // Next.js App Router: Client Component에서만 사용

import { useRef, useCallback, useEffect } from "react";
import { createDeviceCollector } from "@/lib/fingerprint-sdk";
import type { FingerprintPayload, EventType } from "@/lib/fingerprint-sdk";

interface UseFingerprintOptions {
  /** 자동 page_view 수집 여부 (기본: true) */
  autoPageView?: boolean;
  /** 고위험 시 콜백 */
  onHighRisk?: (payload: FingerprintPayload) => void;
}

export function useFingerprint(options: UseFingerprintOptions = {}) {
  const { autoPageView = true, onHighRisk } = options;

  // ref로 관리하여 리렌더링에 영향 없이 싱글톤 유지
  const collectorRef = useRef(
    createDeviceCollector({
      endpoint: "/api/fingerprint/collect",
      appId: process.env.NEXT_PUBLIC_APP_ID ?? "my-nextjs-app",
      debug: process.env.NODE_ENV === "development",
    })
  );

  // 마운트 시 자동 수집
  useEffect(() => {
    if (!autoPageView) return;

    collectorRef.current.collect("page_view").then((payload) => {
      if (payload.risk.score >= 70) {
        onHighRisk?.(payload);
      }
    }).catch(console.warn);
  }, [autoPageView, onHighRisk]);

  /** 이벤트 수집 함수 */
  const collect = useCallback(
    async (eventType: EventType): Promise<FingerprintPayload> => {
      const payload = await collectorRef.current.collect(eventType);

      if (payload.risk.score >= 70) {
        onHighRisk?.(payload);
      }

      return payload;
    },
    [onHighRisk]
  );

  return { collect };
}

9-2. 로그인 페이지 적용 예시

tsx
// src/app/login/page.tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { useFingerprint } from "@/hooks/useFingerprint";

export default function LoginPage() {
  const router = useRouter();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const { collect } = useFingerprint({
    autoPageView: true,
    onHighRisk: (payload) => {
      console.warn("고위험 기기 탐지:", payload.risk.score, payload.risk.details);
      // 필요시 사용자에게 경고 표시
    },
  });

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setIsLoading(true);
    setError(null);

    try {
      // 로그인 버튼 클릭 시점에 지문 수집
      const fpPayload = await collect("login");

      // webdriver 탐지 시 즉시 차단
      if (fpPayload.risk.flags.webdriverDetected) {
        setError("자동화 도구가 탐지되었습니다.");
        return;
      }

      const form = e.currentTarget;
      const formData = new FormData(form);

      const res = await fetch("/api/auth/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          username: formData.get("username"),
          password: formData.get("password"),
          // 기기 지문 정보 첨부
          fp: {
            stableKey: fpPayload.keys.stableKey,
            sessionKey: fpPayload.keys.sessionKey,
            riskScore: fpPayload.risk.score,
          },
        }),
      });

      if (!res.ok) throw new Error("로그인 실패");
      router.push("/dashboard");

    } catch (err) {
      setError(err instanceof Error ? err.message : "오류가 발생했습니다.");
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" type="text" placeholder="아이디" required />
      <input name="password" type="password" placeholder="비밀번호" required />
      {error && <p style={{ color: "red" }}>{error}</p>}
      <button type="submit" disabled={isLoading}>
        {isLoading ? "로그인 중..." : "로그인"}
      </button>
    </form>
  );
}

9-3. Next.js Middleware와 연동 (서버 측)

typescript
// src/middleware.ts
import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  // 클라이언트가 전송한 stableKey를 헤더에서 읽어 서버 로직에 활용
  const deviceKey = request.headers.get("x-device-key");
  const riskScore = parseInt(request.headers.get("x-risk-score") ?? "0", 10);

  // 고위험 점수 요청은 추가 검증 페이지로 리다이렉트
  if (request.nextUrl.pathname.startsWith("/transfer") && riskScore >= 70) {
    return NextResponse.redirect(new URL("/verify", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/transfer/:path*", "/payment/:path*"],
};

10. 연동 방법 D — Vue 3

10-1. Composable 패턴

typescript
// src/composables/useFingerprint.ts
import { ref, onMounted } from "vue";
import { createDeviceCollector } from "@/lib/fingerprint-sdk";
import type { FingerprintPayload, EventType } from "@/lib/fingerprint-sdk";

const collector = createDeviceCollector({
  endpoint: import.meta.env.VITE_FP_ENDPOINT ?? "/api/fingerprint/collect",
  appId: import.meta.env.VITE_APP_ID ?? "my-vue-app",
  debug: import.meta.env.DEV,
});

export function useFingerprint() {
  const lastPayload = ref<FingerprintPayload | null>(null);
  const isCollecting = ref(false);
  const error = ref<string | null>(null);

  const collect = async (eventType: EventType): Promise<FingerprintPayload | null> => {
    isCollecting.value = true;
    error.value = null;

    try {
      const payload = await collector.collect(eventType);
      lastPayload.value = payload;
      return payload;
    } catch (err) {
      error.value = err instanceof Error ? err.message : "수집 실패";
      return null;
    } finally {
      isCollecting.value = false;
    }
  };

  // 컴포넌트 마운트 시 page_view 수집
  onMounted(() => {
    collect("page_view");
  });

  return { collect, lastPayload, isCollecting, error };
}

10-2. Vue 컴포넌트 적용

vue
<!-- src/components/LoginForm.vue -->
<template>
  <form @submit.prevent="handleLogin">
    <input v-model="username" type="text" placeholder="아이디" />
    <input v-model="password" type="password" placeholder="비밀번호" />

    <!-- 리스크 경고 배지 -->
    <div v-if="lastPayload && lastPayload.risk.score >= 40" class="risk-warning">
      ⚠ 보안 이상 신호 탐지 (점수: {{ lastPayload.risk.score }})
    </div>

    <button type="submit" :disabled="isCollecting">
      {{ isCollecting ? "확인 중..." : "로그인" }}
    </button>
  </form>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { useFingerprint } from "@/composables/useFingerprint";
import { useRouter } from "vue-router";

const router = useRouter();
const username = ref("");
const password = ref("");
const { collect, lastPayload, isCollecting } = useFingerprint();

async function handleLogin() {
  const payload = await collect("login");
  if (!payload) return; // 수집 실패

  await fetch("/api/auth/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      username: username.value,
      password: password.value,
      deviceKey: payload.keys.stableKey,
      riskScore: payload.risk.score,
    }),
  });

  router.push("/dashboard");
}
</script>

11. 연동 방법 E — 서버 측 수신 API 구현

11-1. Next.js App Router (현재 프로젝트에 포함됨)

typescript
// src/app/api/fingerprint/collect/route.ts (이미 구현됨)
import { NextRequest, NextResponse } from "next/server";
import type { FingerprintPayload } from "@/lib/fingerprint-sdk/types";

export async function POST(req: NextRequest) {
  const payload: FingerprintPayload = await req.json();

  // 기본 유효성 검증
  if (payload.version !== "fp-v1") {
    return NextResponse.json({ success: false, error: "Invalid version" }, { status: 400 });
  }

  // ── 여기에 비즈니스 로직 추가 ──

  // 1. DB 저장
  // await prisma.fingerprintEvent.create({ data: payload });

  // 2. 동일 stableKey로 다른 계정 접근 탐지
  // const prevUser = await redis.get(`fp:${payload.keys.stableKey}`);
  // if (prevUser && prevUser !== currentUserId) alertMultiAccount();

  // 3. FDS 연동 (고위험 시 알림)
  // if (payload.risk.score >= 70) await fds.sendAlert(payload);

  return NextResponse.json({ success: true });
}

11-2. Express.js 서버 예시

javascript
// server.js
const express = require("express");
const app = express();
app.use(express.json({ limit: "1mb" }));

// CORS 설정 (다른 도메인에서 SDK 사용 시 필요)
app.use((req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", "https://your-frontend.com");
  res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
  if (req.method === "OPTIONS") return res.sendStatus(204);
  next();
});

app.post("/api/fingerprint/collect", async (req, res) => {
  const payload = req.body;

  // 기본 유효성 검증
  if (!payload || payload.version !== "fp-v1") {
    return res.status(400).json({ success: false, error: "Invalid payload" });
  }

  console.log(`[FP] ${payload.eventType} | risk: ${payload.risk.score} | key: ${payload.keys.stableKey.slice(0, 16)}...`);

  // 비즈니스 로직 (예시)
  if (payload.risk.flags.webdriverDetected) {
    console.warn(`[FP] ⚠ 자동화 도구 탐지! appId: ${payload.appId}`);
    // 알림 발송, 계정 잠금 등
  }

  // TODO: DB 저장, 캐시 업데이트 등

  res.json({ success: true, receivedAt: new Date().toISOString() });
});

app.listen(3001, () => console.log("FP API Server running on :3001"));

11-3. Python / FastAPI 예시

python
# main.py
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Optional, Dict, Any
from datetime import datetime

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://your-frontend.com"],
    allow_methods=["POST", "OPTIONS"],
    allow_headers=["Content-Type"],
)

class FingerprintPayload(BaseModel):
    version: str
    appId: str
    eventType: str
    collectedAt: str
    keys: Dict[str, str]
    risk: Dict[str, Any]
    raw: Dict[str, Any]
    normalized: Dict[str, Any]

@app.post("/api/fingerprint/collect")
async def collect(payload: FingerprintPayload):
    if payload.version != "fp-v1":
        raise HTTPException(status_code=400, detail="Invalid payload version")

    print(f"[FP] {payload.eventType} | risk: {payload.risk['score']} | key: {payload.keys['stableKey'][:16]}...")

    # 고위험 탐지 처리
    if payload.risk.get("flags", {}).get("webdriverDetected"):
        print(f"[FP] ⚠ 자동화 도구 탐지! appId: {payload.appId}")
        # 알림 발송 로직

    # TODO: PostgreSQL 저장
    # await db.execute("INSERT INTO fingerprint_events ...", payload.dict())

    return {"success": True, "receivedAt": datetime.utcnow().isoformat()}

12. SDK 옵션 레퍼런스

typescript
const collector = createDeviceCollector({
  // ─── 필수 아님 ───────────────────────────────────────

  /**
   * 수집 결과를 전송할 API 엔드포인트 URL
   * 미지정 시 POST 전송 없이 payload만 로컬에서 사용
   * 다른 도메인이면 CORS 설정 필요
   * @example "/api/fingerprint/collect"
   * @example "https://api.example.com/fingerprint"
   */
  endpoint?: string;

  /**
   * 서비스 식별자 — payload의 appId 필드에 포함됨
   * 서버에서 멀티 서비스 환경 구분에 사용
   * @default "default-app"
   */
  appId?: string;

  /**
   * 디버그 모드 — true 시 콘솔에 상세 수집 과정 출력
   * 배포 환경에서는 반드시 false로 설정
   * @default false
   */
  debug?: boolean;

  /**
   * 초기화 직후 자동으로 page_view 이벤트 수집 여부
   * @default false
   */
  autoCollectPageView?: boolean;

  /**
   * 수집 완료 시 호출되는 콜백
   * endpoint와 함께 사용 시: API 전송 직전에 호출됨
   * @param payload 수집된 전체 payload
   */
  onCollected?: (payload: FingerprintPayload) => void;

  /**
   * 수집 오류 시 호출되는 콜백
   * 콜백이 없어도 에러는 조용히 처리됨 (앱 미중단)
   * @param error Error 객체
   */
  onError?: (error: Error) => void;
});

13. API 메서드 레퍼런스

collector.collect(eventType)

브라우저 정보를 수집하고 payload를 반환합니다.

typescript
const payload: FingerprintPayload = await collector.collect(eventType);
매개변수타입설명
eventTypestring이벤트 타입. "page_view", "login", "important_transaction" 또는 커스텀 문자열
반환값타입설명
payloadPromise<FingerprintPayload>수집 완료된 payload 객체

주의: Canvas/WebGL/Audio 수집에 약 100ms~1,000ms가 소요될 수 있습니다.

버튼 클릭 핸들러 내에서 await로 호출하는 것을 권장합니다.


collector.getLastPayload()

마지막으로 수집한 payload를 반환합니다. 수집 전이면 null을 반환합니다.

typescript
const last: FingerprintPayload | null = collector.getLastPayload();

14. 이벤트 타입 설계 가이드

SDK는 eventType을 자유롭게 지정할 수 있습니다. 서비스 특성에 맞게 설계하세요.

권장 이벤트 타입

이벤트 타입수집 시점활용 목적
page_view페이지 최초 진입기기 최초 등록, 기준 지문 수집
login로그인 버튼 클릭기기 인증, 새 기기 알림
important_transaction이체·결제 버튼 클릭거래 시점 기기 검증
password_change비밀번호 변경 요청계정 탈취 탐지
profile_edit개인정보 수정변경 이력 기기 추적
withdrawal출금 요청고위험 거래 검증
api_key_createAPI 키 발급개발자 계정 보호

커스텀 이벤트 사용

typescript
// 커스텀 이벤트도 그대로 string으로 사용 가능
await collector.collect("otp_request");
await collector.collect("address_change");
await collector.collect("account_link");

15. 보안 및 개인정보 고려사항

개인정보 처리

항목내용
수집 데이터 성격브라우저/기기 환경 정보 (이름, 이메일 등 개인식별정보 미포함)
식별 키SHA-256 단방향 해시 — 원본 복원 불가
IP 주소SDK에서 직접 수집하지 않음 (서버 수신 시 request IP 활용)
쿠키 사용SDK 자체는 쿠키를 설정하지 않음

개인정보처리방침 고지 권장 문구

code
본 서비스는 보안 및 부정 이용 방지를 위해 브라우저 환경 정보
(화면 해상도, 시간대, 플러그인 목록 등)를 수집합니다.
수집된 정보는 식별 해시값으로 변환되어 저장되며,
개인을 직접 식별하는 데 사용되지 않습니다.

보안 고려사항

code
⚠ 주의사항

1. endpoint는 HTTPS로만 설정하세요 (HTTP는 중간자 공격에 취약)
2. 서버 API에서 수신한 payload는 반드시 유효성 검증 후 저장하세요
3. stableKey를 세션 토큰 대용으로 사용하지 마세요 (기기 공유 가능)
4. debug: true는 개발환경에서만 사용하세요 (콘솔에 민감 정보 출력)
5. CORS는 신뢰할 수 있는 origin만 허용하세요

16. 자주 묻는 질문 (FAQ)

Q. SDK가 항상 같은 stableKey를 반환하나요?

A. 동일 기기·브라우저라면 Canvas, WebGL, Audio 렌더링 결과가 같으므로 stableKey는 안정적으로 유지됩니다. 단, 브라우저 업데이트, 하드웨어 변경, 프라이버시 브라우저(Tor, 브레이브 Shields 활성화)에서는 달라질 수 있습니다.


Q. 시크릿 모드(인코그니토)에서도 동작하나요?

A. 동작합니다. 단, 시크릿 모드에서는 localStorage가 차단되고 플러그인이 다를 수 있어 stableKey가 일반 모드와 다르게 나올 수 있습니다. suspiciousNoStorage 플래그가 발생할 수 있습니다.


Q. Safari에서 WebGL 정보가 없을 때는요?

A. Safari는 iOS 16.4+부터 WEBGL_debug_renderer_info 확장을 차단합니다. SDK는 이를 감지하고 webglVendor/Renderer를 기본값으로 fallback합니다. stableKey 계산 시 해당 필드는 빈 문자열로 처리됩니다.


Q. 수집에 얼마나 걸리나요?

A. 기기 성능에 따라 다르지만 일반적으로 200ms ~ 800ms입니다. Canvas와 Audio 지문이 대부분의 시간을 차지합니다. 두 수집은 내부적으로 Promise.all로 병렬 처리됩니다.


Q. 서버 API가 없어도 사용할 수 있나요?

A. 네. endpoint를 생략하면 API 전송 없이 payload를 로컬에서만 사용합니다. onCollected 콜백으로 결과를 받아 활용할 수 있습니다.


Q. 여러 페이지에서 각각 초기화해도 되나요?

A. 가능하지만 성능상 싱글톤으로 관리하는 것을 권장합니다. React에서는 useRef, Vue에서는 모듈 레벨 변수, 바닐라 JS에서는 window 전역 변수로 관리하세요.


17. 문제 해결

수집 결과가 null인 필드가 많을 때

  • 원인: 브라우저 보안 정책(Firefox Enhanced Tracking Protection, Brave Shields 등)
  • 대응: null 필드는 stableKey 계산 시 빈 문자열로 처리되어 동작합니다. 정상입니다.

항상 riskScore가 높게 나올 때

  • 원인: 개발 환경(localhost)에서 일부 브라우저 동작이 다름
  • 대응: 배포 환경에서 테스트하거나, risk.details 배열로 어떤 플래그가 발생했는지 확인

endpoint 전송이 실패할 때

  • 원인 1: CORS 미설정 → 서버에 Access-Control-Allow-Origin 헤더 추가
  • 원인 2: HTTPS 미적용 → Mixed Content 오류 발생 가능
  • 원인 3: 네트워크 오류 → onError 콜백에서 확인
typescript
const collector = createDeviceCollector({
  endpoint: "https://api.example.com/api/fingerprint/collect", // 반드시 HTTPS
  debug: true, // 전송 과정 콘솔에서 확인
  onError: (err) => console.error("[FP 오류]", err.message),
});

브라우저 콘솔에서 로그 확인

typescript
// debug: true 설정 시 다음과 같은 로그가 출력됩니다
// [FingerprintSDK] collect() 시작 — eventType: login
// [FingerprintSDK] 기본 정보 수집 중...
// [FingerprintSDK] Canvas / WebGL / Audio 지문 수집 중...
// [FingerprintSDK] Raw 수집 완료 {userAgent: "...", ...}
// [FingerprintSDK] 식별 키 생성 완료 {stableKey: "a3f...", sessionKey: "7b4..."}
// [FingerprintSDK] 리스크 평가 완료 — score: 0
// [FingerprintSDK] POST 전송 중 → /api/fingerprint/collect
// [FingerprintSDK] POST 전송 완료 — 200

*Device Fingerprint SDK — 브라우저 기반 기기 인텔리전스 라이브러리*