318 lines
8.3 KiB
Markdown
318 lines
8.3 KiB
Markdown
# 생체인증 가이드 (Credential Management API)
|
|
|
|
## 📱 개요
|
|
|
|
TWA(Trusted Web Activity) 환경에서 WebAuthn / Credential Management API를 활용한 생체인증 기능을 제공합니다.
|
|
|
|
## ✨ 주요 기능
|
|
|
|
### 1. 생체인증 등록
|
|
- 로그인 후 생체인증(지문, 얼굴 인식 등)을 등록할 수 있습니다
|
|
- 사용자의 사번과 이름으로 credential을 생성합니다
|
|
- 등록된 credential은 Supabase에 안전하게 저장됩니다
|
|
|
|
### 2. 생체인증 로그인
|
|
- 등록된 생체인증으로 빠르게 로그인할 수 있습니다
|
|
- 비밀번호 입력 없이 지문/얼굴 인식만으로 인증됩니다
|
|
- 플랫폼 인증 기능을 사용하여 보안성이 높습니다
|
|
|
|
### 3. 생체인증 해제
|
|
- 필요시 생체인증을 해제할 수 있습니다
|
|
- 해제 후 다시 등록할 수 있습니다
|
|
|
|
## 🏗️ 아키텍처
|
|
|
|
### Frontend (Vue.js/TypeScript)
|
|
|
|
#### 1. Composable: `useBiometric.ts`
|
|
```typescript
|
|
// 생체인증 지원 여부 확인
|
|
await biometric.checkSupport()
|
|
|
|
// 생체인증 등록
|
|
const result = await biometric.register(employeeId, userName)
|
|
|
|
// 생체인증 로그인
|
|
const result = await biometric.authenticate()
|
|
|
|
// 생체인증 해제
|
|
const result = await biometric.unregister()
|
|
```
|
|
|
|
#### 2. 상태 관리
|
|
- `isSupported`: 브라우저가 WebAuthn을 지원하는지 여부
|
|
- `isAvailable`: 플랫폼 생체인증이 사용 가능한지 여부
|
|
- `isEnrolled`: 사용자가 생체인증을 등록했는지 여부
|
|
- `canUse`: 생체인증을 사용할 수 있는지 여부
|
|
- `canRegister`: 생체인증을 등록할 수 있는지 여부
|
|
- `canAuthenticate`: 생체인증으로 로그인할 수 있는지 여부
|
|
|
|
### Backend (Flask/Python)
|
|
|
|
#### 1. 생체인증 등록 Challenge
|
|
```
|
|
POST /api/biometric/register-challenge
|
|
{
|
|
"employeeId": "20240001"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"challenge": "random_base64_string",
|
|
"userId": "20240001"
|
|
}
|
|
```
|
|
|
|
#### 2. Credential 등록
|
|
```
|
|
POST /api/biometric/register
|
|
{
|
|
"employeeId": "20240001",
|
|
"credential": {
|
|
"id": "credential_id",
|
|
"rawId": "base64_raw_id",
|
|
"response": {
|
|
"clientDataJSON": "base64_client_data",
|
|
"attestationObject": "base64_attestation"
|
|
},
|
|
"type": "public-key"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3. 로그인 Challenge
|
|
```
|
|
POST /api/biometric/login-challenge
|
|
{
|
|
"employeeId": "20240001"
|
|
}
|
|
```
|
|
|
|
#### 4. 생체인증 로그인
|
|
```
|
|
POST /api/biometric/login
|
|
{
|
|
"employeeId": "20240001",
|
|
"assertion": {
|
|
"id": "credential_id",
|
|
"rawId": "base64_raw_id",
|
|
"response": {
|
|
"clientDataJSON": "base64_client_data",
|
|
"authenticatorData": "base64_auth_data",
|
|
"signature": "base64_signature"
|
|
},
|
|
"type": "public-key"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"user": {
|
|
"id": 1,
|
|
"employee_id": "20240001",
|
|
"name": "홍길동",
|
|
"email": "hong@humetro.busan.kr",
|
|
"department_id": 1
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 5. 생체인증 해제
|
|
```
|
|
POST /api/biometric/unregister
|
|
{
|
|
"employeeId": "20240001"
|
|
}
|
|
```
|
|
|
|
## 🗄️ 데이터베이스 스키마
|
|
|
|
### biometric_credentials 테이블
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS public.biometric_credentials (
|
|
id SERIAL PRIMARY KEY,
|
|
employee_id VARCHAR(50) NOT NULL REFERENCES public.users(employee_id) ON DELETE CASCADE,
|
|
credential_id VARCHAR(255) UNIQUE NOT NULL,
|
|
credential_data JSONB NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
last_used_at TIMESTAMPTZ,
|
|
UNIQUE(employee_id, credential_id)
|
|
);
|
|
|
|
CREATE INDEX idx_biometric_employee_id ON public.biometric_credentials(employee_id);
|
|
CREATE INDEX idx_biometric_credential_id ON public.biometric_credentials(credential_id);
|
|
```
|
|
|
|
**필드 설명:**
|
|
- `employee_id`: 사용자 사번 (외래키)
|
|
- `credential_id`: WebAuthn credential ID (고유값)
|
|
- `credential_data`: Credential 전체 데이터 (JSON)
|
|
- `created_at`: 등록 일시
|
|
- `last_used_at`: 마지막 사용 일시
|
|
|
|
## 🔐 보안 고려사항
|
|
|
|
### 1. Challenge-Response 인증
|
|
- 서버에서 생성한 랜덤 challenge를 사용합니다
|
|
- Challenge는 세션에 저장되고 10분간 유효합니다
|
|
- 각 인증 시도마다 새로운 challenge를 생성합니다
|
|
|
|
### 2. 플랫폼 인증
|
|
- `authenticatorAttachment: 'platform'`을 사용하여 기기 내장 생체인식만 허용합니다
|
|
- `userVerification: 'required'`로 사용자 검증을 필수로 합니다
|
|
|
|
### 3. Credential 검증
|
|
- 서버에 저장된 credential과 비교하여 검증합니다
|
|
- 실제 프로덕션에서는 공개키 암호화를 사용한 서명 검증이 필요합니다 (현재는 간소화된 버전)
|
|
|
|
### 4. 세션 관리
|
|
- 생체인증 성공 시 Flask 세션에 사용자 정보를 저장합니다
|
|
- 세션은 httponly 쿠키로 안전하게 관리됩니다
|
|
|
|
## 📱 TWA 환경에서의 동작
|
|
|
|
### 1. Android TWA
|
|
- Chrome의 WebAuthn API를 통해 Android 생체인식(지문, 얼굴)을 사용합니다
|
|
- Android Keystore에 안전하게 credential이 저장됩니다
|
|
- Google Play Services를 통한 FIDO2 인증을 지원합니다
|
|
|
|
### 2. 지원 여부 확인
|
|
```javascript
|
|
// PublicKeyCredential API 지원 확인
|
|
if (!window.PublicKeyCredential) {
|
|
console.log('생체인증 미지원')
|
|
}
|
|
|
|
// 플랫폼 인증 가능 여부
|
|
const available = await PublicKeyCredential
|
|
.isUserVerifyingPlatformAuthenticatorAvailable()
|
|
```
|
|
|
|
### 3. 사용자 경험
|
|
1. 로그인 페이지에서 생체인증 가능 여부를 자동으로 확인합니다
|
|
2. 등록된 생체인증이 있으면 "생체인증으로 로그인" 버튼이 표시됩니다
|
|
3. 버튼 클릭 시 기기의 생체인증 UI가 표시됩니다
|
|
4. 인증 성공 시 자동으로 로그인됩니다
|
|
|
|
## 🚀 사용 방법
|
|
|
|
### 1. 생체인증 등록 (로그인 후)
|
|
|
|
```typescript
|
|
import { useBiometric } from '@/composables/useBiometric'
|
|
|
|
const biometric = useBiometric()
|
|
|
|
// 지원 여부 확인
|
|
await biometric.checkSupport()
|
|
|
|
if (biometric.canRegister.value) {
|
|
// 등록
|
|
const result = await biometric.register(employeeId, userName)
|
|
|
|
if (result.success) {
|
|
console.log('생체인증 등록 성공')
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. 생체인증 로그인
|
|
|
|
```typescript
|
|
if (biometric.canAuthenticate.value) {
|
|
const result = await biometric.authenticate()
|
|
|
|
if (result.success && result.user) {
|
|
// 로그인 성공
|
|
authStore.setUser(result.user)
|
|
router.push('/')
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. LoginView에서의 통합
|
|
|
|
```vue
|
|
<template>
|
|
<!-- 생체인증 버튼 -->
|
|
<button
|
|
v-if="biometric.canAuthenticate.value"
|
|
@click="handleBiometricLogin"
|
|
:disabled="biometric.loading.value"
|
|
>
|
|
생체인증으로 로그인
|
|
</button>
|
|
|
|
<!-- 등록 안내 -->
|
|
<div v-else-if="!biometric.isEnrolled.value && biometric.canUse.value">
|
|
로그인 후 생체인증을 등록할 수 있습니다.
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { useBiometric } from '@/composables/useBiometric'
|
|
|
|
const biometric = useBiometric()
|
|
|
|
onMounted(async () => {
|
|
await biometric.checkSupport()
|
|
})
|
|
|
|
async function handleBiometricLogin() {
|
|
const result = await biometric.authenticate()
|
|
// 처리...
|
|
}
|
|
</script>
|
|
```
|
|
|
|
## 🔧 문제 해결
|
|
|
|
### 1. 생체인증이 표시되지 않음
|
|
- 브라우저가 WebAuthn을 지원하는지 확인
|
|
- HTTPS 환경에서만 동작 (localhost는 예외)
|
|
- 기기에 생체인증이 설정되어 있는지 확인
|
|
|
|
### 2. 등록 실패
|
|
- Challenge가 만료되었을 수 있음 (10분)
|
|
- 네트워크 연결 확인
|
|
- 콘솔 로그에서 에러 메시지 확인
|
|
|
|
### 3. 로그인 실패
|
|
- 등록된 credential이 삭제되었을 수 있음
|
|
- localStorage에서 `biometric_credential_id` 확인
|
|
- 데이터베이스에 credential이 존재하는지 확인
|
|
|
|
## 📝 TODO (향후 개선사항)
|
|
|
|
1. **서명 검증 구현**
|
|
- 현재는 credential_id만 비교하는 간소화된 버전
|
|
- 실제 공개키 암호화 서명 검증 추가 필요
|
|
|
|
2. **여러 Credential 지원**
|
|
- 한 사용자가 여러 기기에서 생체인증 등록 가능하도록
|
|
|
|
3. **Credential 관리 UI**
|
|
- 등록된 기기 목록 표시
|
|
- 개별 credential 삭제 기능
|
|
|
|
4. **마지막 사용 시간 업데이트**
|
|
- 로그인 시 `last_used_at` 필드 업데이트
|
|
|
|
5. **에러 로깅 개선**
|
|
- 생체인증 관련 에러를 audit_logs에 기록
|
|
|
|
## 🔗 참고 자료
|
|
|
|
- [WebAuthn Guide](https://webauthn.guide/)
|
|
- [MDN - Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API)
|
|
- [FIDO Alliance](https://fidoalliance.org/)
|
|
- [Chrome WebAuthn](https://developers.google.com/web/updates/2018/05/webauthn)
|
|
|