PHP 怎么Passkey

wen PHP项目 2

本文目录导读:

PHP 怎么Passkey

  1. Passkey 概述
  2. PHP 实现方案
  3. 最佳实践建议
  4. 常见问题处理

我来详细解释PHP中如何实现Passkey(通行密钥)功能。

Passkey 概述

Passkey 是基于 WebAuthn 标准的无密码认证方式,使用公钥加密技术,用户通过生物识别或设备PIN完成认证。

PHP 实现方案

使用现成库(推荐)

Webauthn/webauthn-lib(最流行)

composer require web-auth/webauthn-lib

基础实现示例

注册流程(Registration)

<?php
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialUserEntity;
use Webauthn\PublicKeyCredentialParameters;
use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialLoader;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
class PasskeyService {
    private $server;
    public function __construct() {
        // 初始化服务器
        $rpEntity = new PublicKeyCredentialRpEntity(
            'your-domain.com',  // 域名
            'Your App'          // 应用名称
        );
        // 配置存储(需要实现)
        $this->server = new WebauthnServer($rpEntity);
    }
    // 1. 生成注册选项
    public function getRegistrationOptions($userId, $userName) {
        $userEntity = new PublicKeyCredentialUserEntity(
            $userId,
            $userName,
            $userName
        );
        $options = new PublicKeyCredentialCreationOptions(
            $userEntity,
            new PublicKeyCredentialParameters('public-key', -7),  // ES256算法
            $this->server->getRpEntity(),
            $this->generateChallenge()
        );
        return $options;
    }
    // 2. 处理注册响应
    public function handleRegistration($requestData, $userId) {
        $publicKeyCredentialLoader = new PublicKeyCredentialLoader();
        try {
            $credential = $publicKeyCredentialLoader->loadJSON($requestData);
            if ($credential->getResponse() instanceof AuthenticatorAttestationResponse) {
                // 验证并存储凭证
                $publicKeyCredentialSource = $this->server->processCredentialCreation(
                    $requestData,
                    $credential
                );
                // 保存到数据库
                $this->saveCredential($userId, $publicKeyCredentialSource);
                return ['success' => true];
            }
        } catch (\Exception $e) {
            return ['success' => false, 'error' => $e->getMessage()];
        }
    }
    // 3. 生成认证选项
    public function getAuthenticationOptions($userCredentials) {
        $options = new PublicKeyCredentialRequestOptions(
            $this->generateChallenge(),
            $userCredentials
        );
        return $options;
    }
    // 4. 处理认证响应
    public function handleAuthentication($requestData, $userCredentials) {
        try {
            $publicKeyCredentialLoader = new PublicKeyCredentialLoader();
            $credential = $publicKeyCredentialLoader->loadJSON($requestData);
            if ($credential->getResponse() instanceof AuthenticatorAssertionResponse) {
                $authenticatorData = $this->server->processCredentialAuthentication(
                    $requestData,
                    $credential,
                    $userCredentials
                );
                // 认证成功
                return ['success' => true, 'credentialId' => $credential->getId()];
            }
        } catch (\Exception $e) {
            return ['success' => false, 'error' => $e->getMessage()];
        }
    }
    private function generateChallenge() {
        return base64_encode(random_bytes(32));
    }
    private function saveCredential($userId, $credential) {
        // 实现数据库存储逻辑
    }
}

前端集成

HTML + JavaScript

<!DOCTYPE html>
<html>
<head>Passkey Demo</title>
</head>
<body>
    <button id="registerBtn">注册Passkey</button>
    <button id="loginBtn">Passkey登录</button>
    <script>
    class PasskeyClient {
        constructor() {
            this.baseUrl = 'https://your-domain.com';
        }
        // 注册
        async register() {
            try {
                // 1. 获取注册选项
                const optionsResponse = await fetch(`${this.baseUrl}/api/passkey/register-options`, {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({userId: 'user-123'})
                });
                const options = await optionsResponse.json();
                // 2. 调用浏览器API
                const credential = await navigator.credentials.create({
                    publicKey: options
                });
                // 3. 发送给服务器验证
                const verifyResponse = await fetch(`${this.baseUrl}/api/passkey/register`, {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({
                        credential: this.transformCredential(credential)
                    })
                });
                if (verifyResponse.ok) {
                    alert('注册成功!');
                }
            } catch (error) {
                console.error('注册失败:', error);
            }
        }
        // 登录
        async login() {
            try {
                // 1. 获取认证选项
                const optionsResponse = await fetch(`${this.baseUrl}/api/passkey/verify-options`, {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({username: 'user@example.com'})
                });
                const options = await optionsResponse.json();
                // 2. 调用浏览器API
                const credential = await navigator.credentials.get({
                    publicKey: options
                });
                // 3. 验证
                const verifyResponse = await fetch(`${this.baseUrl}/api/passkey/verify`, {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({
                        credential: this.transformCredential(credential)
                    })
                });
                if (verifyResponse.ok) {
                    alert('登录成功!');
                }
            } catch (error) {
                console.error('登录失败:', error);
            }
        }
        // 转换凭证格式
        transformCredential(credential) {
            return {
                id: credential.id,
                rawId: this.arrayBufferToBase64(credential.rawId),
                type: credential.type,
                response: {
                    clientDataJSON: this.arrayBufferToBase64(credential.response.clientDataJSON),
                    authenticatorData: credential.response.authenticatorData ? 
                        this.arrayBufferToBase64(credential.response.authenticatorData) : null,
                    signature: credential.response.signature ? 
                        this.arrayBufferToBase64(credential.response.signature) : null,
                    attestationObject: credential.response.attestationObject ? 
                        this.arrayBufferToBase64(credential.response.attestationObject) : null
                }
            };
        }
        // ArrayBuffer转Base64
        arrayBufferToBase64(buffer) {
            const bytes = new Uint8Array(buffer);
            let binary = '';
            for (let i = 0; i < bytes.byteLength; i++) {
                binary += String.fromCharCode(bytes[i]);
            }
            return btoa(binary);
        }
    }
    // 初始化事件
    document.getElementById('registerBtn').addEventListener('click', () => {
        const client = new PasskeyClient();
        client.register();
    });
    document.getElementById('loginBtn').addEventListener('click', () => {
        const client = new PasskeyClient();
        client.login();
    });
    </script>
</body>
</html>

数据库设计

-- 用户表
CREATE TABLE users (
    id VARCHAR(64) PRIMARY KEY,
    username VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Passkey凭证表
CREATE TABLE passkey_credentials (
    id VARCHAR(255) PRIMARY KEY,
    user_id VARCHAR(64) NOT NULL,
    public_key BLOB NOT NULL,
    sign_count INT DEFAULT 0,
    credential_id VARCHAR(255) NOT NULL,
    aaguid VARCHAR(32),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 挑战表(用于存储临时认证挑战)
CREATE TABLE authentication_challenges (
    id VARCHAR(64) PRIMARY KEY,
    user_id VARCHAR(64),
    challenge BLOB NOT NULL,
    expires_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

完整示例代码(PHP 8+)

<?php
require 'vendor/autoload.php';
use Webauthn\Server;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialUserEntity;
use Webauthn\AttestationStatement\AttestationObjectLoader;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
class PasskeyController {
    private $server;
    private $db;
    public function __construct() {
        $this->db = new PDO('mysql:host=localhost;dbname=passkey_db', 'user', 'password');
        // 配置WebAuthn服务器
        $rpEntity = new \Webauthn\PublicKeyCredentialRpEntity(
            'localhost',
            'Passkey Demo'
        );
        $this->server = Server::create($rpEntity);
        $this->server->setCredentialRepository($this->getCredentialRepository());
    }
    // 处理注册请求
    public function handleRegistration() {
        $input = json_decode(file_get_contents('php://input'), true);
        if (isset($input['username'])) {
            // 创建新用户
            $userId = bin2hex(random_bytes(16));
            $username = $input['username'];
            // 保存用户会话数据
            $_SESSION['registration_user_id'] = $userId;
            // 生成注册选项
            $userEntity = new PublicKeyCredentialUserEntity(
                $userId,
                $username,
                $username
            );
            $options = $this->server->generateRegistration(
                $userEntity,
                [
                    \Webauthn\PublicKeyCredentialParameters::create('public-key', -7)
                ]
            );
            // 返回给前端
            header('Content-Type: application/json');
            echo json_encode($options);
        }
    }
    // 验证注册响应
    public function verifyRegistration() {
        $input = json_decode(file_get_contents('php://input'), true);
        if (isset($input['credential'])) {
            try {
                $credential = $this->server->loadAndCheckAttestationResponse(
                    $input['credential'],
                    $_SESSION['registration_user_id']
                );
                // 获取公钥
                $publicKey = $credential->getPublicKeyCredentialSource();
                // 保存到数据库
                $stmt = $this->db->prepare(
                    'INSERT INTO passkey_credentials (id, user_id, public_key, credential_id) 
                     VALUES (?, ?, ?, ?)'
                );
                $stmt->execute([
                    bin2hex(random_bytes(16)),
                    $_SESSION['registration_user_id'],
                    $publicKey->getPublicKey(),
                    $credential->getId()
                ]);
                echo json_encode(['success' => true]);
            } catch (\Exception $e) {
                http_response_code(400);
                echo json_encode(['error' => $e->getMessage()]);
            }
        }
    }
    // 认证流程
    public function handleAuthentication() {
        $input = json_decode(file_get_contents('php://input'), true);
        if (isset($input['username'])) {
            // 获取用户凭证
            $stmt = $this->db->prepare(
                'SELECT * FROM passkey_credentials 
                 WHERE user_id = (SELECT id FROM users WHERE username = ?)'
            );
            $stmt->execute([$input['username']]);
            $credentials = $stmt->fetchAll(PDO::FETCH_ASSOC);
            if ($credentials) {
                // 生成认证选项
                $options = $this->server->generateAuthentication(
                    $this->getCredentialSources($credentials)
                );
                header('Content-Type: application/json');
                echo json_encode($options);
            }
        }
    }
    // 验证认证响应
    public function verifyAuthentication() {
        $input = json_decode(file_get_contents('php://input'), true);
        try {
            $result = $this->server->loadAndCheckAssertionResponse(
                $input['credential'],
                $this->getCredentialSources()
            );
            // 认证成功
            $_SESSION['user_id'] = $result->getUser()->getId();
            echo json_encode(['success' => true]);
        } catch (\Exception $e) {
            http_response_code(400);
            echo json_encode(['error' => $e->getMessage()]);
        }
    }
    // 获取凭证仓库
    private function getCredentialRepository() {
        return new class implements \Webauthn\PublicKeyCredentialSourceRepository {
            private $db;
            public function __construct() {
                $this->db = new PDO('mysql:host=localhost;dbname=passkey_db', 'user', 'password');
            }
            public function findOneByCredentialId(string $publicKeyCredentialId): 
                ?\Webauthn\PublicKeyCredentialSource {
                // 从数据库查找凭证
                $stmt = $this->db->prepare(
                    'SELECT * FROM passkey_credentials WHERE credential_id = ?'
                );
                $stmt->execute([$publicKeyCredentialId]);
                $credential = $stmt->fetch(PDO::FETCH_ASSOC);
                if ($credential) {
                    return \Webauthn\PublicKeyCredentialSource::createFromArray(
                        json_decode($credential['public_key'], true)
                    );
                }
                return null;
            }
            public function saveCredentialSource(
                \Webauthn\PublicKeyCredentialSource $publicKeyCredentialSource
            ): void {
                // 保存凭证
            }
            public function findAllByUserHandle(string $userHandle): array {
                // 获取用户的所有凭证
                $stmt = $this->db->prepare(
                    'SELECT * FROM passkey_credentials WHERE user_id = ?'
                );
                $stmt->execute([$userHandle]);
                $credentials = $stmt->fetchAll(PDO::FETCH_ASSOC);
                $sources = [];
                foreach ($credentials as $credential) {
                    $sources[] = \Webauthn\PublicKeyCredentialSource::createFromArray(
                        json_decode($credential['public_key'], true)
                    );
                }
                return $sources;
            }
        };
    }
}

路由配置

// routes.php
$router->post('/api/passkey/register-options', 'PasskeyController@handleRegistration');
$router->post('/api/passkey/register', 'PasskeyController@verifyRegistration');
$router->post('/api/passkey/verify-options', 'PasskeyController@handleAuthentication');
$router->post('/api/passkey/verify', 'PasskeyController@verifyAuthentication');

最佳实践建议

  1. 安全要求

    • 使用HTTPS(必须)
    • 有效且安全的域名
    • 实施速率限制
    • 使用CSRF保护
  2. 用户体验

    • 提供降级方案(密码登录)
    • 清晰的错误提示
    • 支持多设备
  3. 配置要求

    • 确保现代浏览器支持
    • 正确配置CORS
    • 处理移动端兼容性
  4. 测试建议

    • 使用浏览器模拟器测试
    • 测试不同认证器设备
    • 确保错误处理完善

常见问题处理

// 错误处理示例
try {
    // Passkey操作
} catch (\Webauthn\Exception\InvalidDataException $e) {
    // 无效数据
    http_response_code(400);
} catch (\Webauthn\Exception\ChallengeException $e) {
    // 挑战验证失败
    http_response_code(401);
} catch (\Webauthn\Exception\UserNotFoundException $e) {
    // 用户不存在
    http_response_code(404);
} catch (\Throwable $e) {
    // 其他错误
    error_log($e->getMessage());
    http_response_code(500);
}

这是一个完整的Passkey实现方案,建议使用成熟库处理底层细节,并完善错误处理和日志记录。

抱歉,评论功能暂时关闭!