PHP 怎么WebAuthn

wen PHP项目 1

本文目录导读:

PHP 怎么WebAuthn

  1. 基础概念
  2. 使用 PHP WebAuthn 库
  3. 完整实现示例
  4. 安全注意事项
  5. 常见问题

我来详细介绍如何在 PHP 中实现 WebAuthn。

基础概念

WebAuthn 允许网站使用生物识别、安全密钥或设备 PIN 进行无密码认证,PHP 实现需要处理注册和认证两个主要流程。

使用 PHP WebAuthn 库

推荐使用成熟的开源库,如 web-auth/webauthn-framework

composer require web-auth/webauthn-framework

完整实现示例

1 服务端配置类

<?php
// WebAuthnService.php
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialUserEntity;
use Webauthn\PublicKeyCredentialParameters;
use Webauthn\PublicKeyCredentialDescriptor;
use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\PublicKeyCredentialLoader;
use Webauthn\AuthenticatorAttestationResponseValidator;
use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\PublicKeyCredentialSourceRepository;
use Webauthn\PublicKeyCredentialSource;
use Cose\Algorithms;
use Nyholm\Psr7\ServerRequest;
class WebAuthnService
{
    private $repository;
    private $rpEntity;
    private $loader;
    public function __construct()
    {
        // Relying Party 信息
        $this->rpEntity = new PublicKeyCredentialRpEntity(
            'My Website',  // 站点名称
            'localhost',   // 域名
            null           // 图标 URL(可选)
        );
        // 加载器
        $this->loader = PublicKeyCredentialLoader::create();
        // 凭证存储仓库(需要实现)
        $this->repository = new UserCredentialRepository();
    }
    /**
     * 生成注册选项
     */
    public function generateRegistrationOptions(string $username, string $userId): array
    {
        // 创建用户实体
        $userEntity = new PublicKeyCredentialUserEntity(
            $username,
            $userId,
            $username
        );
        // 支持的算法
        $credentialParameters = [
            new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_ES256),
            new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_RS256)
        ];
        // 认证器选择条件
        $authenticatorSelection = new AuthenticatorSelectionCriteria(
            AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_PLATFORM,
            AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED,
            AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_DISCOURAGED
        );
        // 创建注册选项
        $options = new PublicKeyCredentialCreationOptions(
            $this->rpEntity,
            $userEntity,
            base64_encode(random_bytes(32)),  // challenge
            $credentialParameters,
        );
        // 设置超时(30秒)
        $options->setTimeout(30000);
        // 排除已注册的凭证
        $registeredCredentials = $this->repository->findAllForUserEntity($userEntity);
        if (!empty($registeredCredentials)) {
            $options->excludeCredentials = $registeredCredentials;
        }
        // 存储 challenge 到 session
        $_SESSION['webauthn_registration_challenge'] = base64_encode($options->getChallenge());
        $_SESSION['webauthn_registration_user'] = json_encode([
            'username' => $username,
            'userId' => $userId
        ]);
        // 返回给前端的数据
        return $options->jsonSerialize();
    }
    /**
     * 验证注册响应
     */
    public function verifyRegistrationResponse(array $response): bool
    {
        try {
            // 获取 session 中的 challenge
            $storedChallenge = $_SESSION['webauthn_registration_challenge'] ?? null;
            if (!$storedChallenge) {
                throw new \Exception('No registration challenge found');
            }
            // 创建服务器请求对象
            $serverRequest = new ServerRequest('POST', '/webauthn/register');
            $serverRequest = $serverRequest->withParsedBody($response);
            // 加载凭证
            $publicKeyCredential = $this->loader->loadArray($response);
            // 获取用户信息
            $userData = json_decode($_SESSION['webauthn_registration_user'], true);
            $userEntity = new PublicKeyCredentialUserEntity(
                $userData['username'],
                $userData['userId'],
                $userData['username']
            );
            // 创建验证器
            $validator = new AuthenticatorAttestationResponseValidator(
                $this->repository
            );
            // 验证响应
            $publicKeyCredentialSource = $validator->check(
                $publicKeyCredential->getResponse(),
                $publicKeyCredential->getRawId(),
                $this->rpEntity,
                $userEntity,
                $storedChallenge,
                $serverRequest
            );
            // 存储凭证
            $this->repository->saveCredentialSource($publicKeyCredentialSource);
            // 清除 session
            unset($_SESSION['webauthn_registration_challenge']);
            unset($_SESSION['webauthn_registration_user']);
            return true;
        } catch (\Exception $e) {
            error_log('WebAuthn registration error: ' . $e->getMessage());
            return false;
        }
    }
    /**
     * 生成认证选项(登录)
     */
    public function generateAuthenticationOptions(string $username): array
    {
        // 查找用户
        $userRepository = new UserRepository();
        $user = $userRepository->findByUsername($username);
        if (!$user) {
            throw new \Exception('User not found');
        }
        // 创建用户实体
        $userEntity = new PublicKeyCredentialUserEntity(
            $user->getUsername(),
            (string)$user->getId(),
            $user->getDisplayName()
        );
        // 获取用户凭证
        $credentials = $this->repository->findAllForUserEntity($userEntity);
        // 创建认证选项
        $options = new PublicKeyCredentialRequestOptions(
            base64_encode(random_bytes(32)),  // challenge
        );
        $options->setTimeout(30000);
        $options->setAllowCredentials($credentials);
        $options->setUserVerification(
            AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED
        );
        // 存储挑战
        $_SESSION['webauthn_authentication_challenge'] = base64_encode($options->getChallenge());
        $_SESSION['webauthn_authentication_username'] = $username;
        return $options->jsonSerialize();
    }
    /**
     * 验证认证响应(登录)
     */
    public function verifyAuthenticationResponse(array $response): bool
    {
        try {
            // 获取存储的挑战
            $storedChallenge = $_SESSION['webauthn_authentication_challenge'] ?? null;
            if (!$storedChallenge) {
                throw new \Exception('No authentication challenge found');
            }
            // 创建服务器请求
            $serverRequest = new ServerRequest('POST', '/webauthn/login');
            // 加载凭证
            $publicKeyCredential = $this->loader->loadArray($response);
            // 获取用户信息
            $username = $_SESSION['webauthn_authentication_username'] ?? null;
            if (!$username) {
                throw new \Exception('No username found');
            }
            $userRepository = new UserRepository();
            $user = $userRepository->findByUsername($username);
            // 获取凭证源
            $credentialSource = $this->repository->findOneByCredentialId(
                $publicKeyCredential->getRawId()
            );
            if (!$credentialSource) {
                throw new \Exception('Credential not found');
            }
            // 创建验证器
            $validator = new AuthenticatorAssertionResponseValidator(
                $this->repository
            );
            // 验证认证
            $validator->check(
                $publicKeyCredential->getResponse(),
                $publicKeyCredential->getRawId(),
                $credentialSource,
                $storedChallenge,
                $request,
                $publicKeyCredential->getRawId()
            );
            // 更新计数器
            $this->repository->updateCredentialSource($credentialSource);
            // 清除 session
            unset($_SESSION['webauthn_authentication_challenge']);
            unset($_SESSION['webauthn_authentication_username']);
            // 登录成功
            $_SESSION['user_logged_in'] = true;
            $_SESSION['user_username'] = $username;
            return true;
        } catch (\Exception $e) {
            error_log('WebAuthn authentication error: ' . $e->getMessage());
            return false;
        }
    }
}

2 凭证仓库实现

<?php
// UserCredentialRepository.php
use Webauthn\PublicKeyCredentialSourceRepository;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\PublicKeyCredentialUserEntity;
class UserCredentialRepository implements PublicKeyCredentialSourceRepository
{
    private $pdo;
    public function __construct()
    {
        // 数据库连接
        $this->pdo = new PDO('mysql:host=localhost;dbname=webauthn', 'username', 'password');
        $this->createTables();
    }
    private function createTables()
    {
        $this->pdo->exec("
            CREATE TABLE IF NOT EXISTS webauthn_credentials (
                id VARCHAR(255) PRIMARY KEY,
                user_id VARCHAR(255) NOT NULL,
                type VARCHAR(50) NOT NULL,
                transports JSON,
                attestation_type VARCHAR(50),
                trust_path JSON,
                aaguid VARCHAR(50),
                credential_public_key LONGBLOB,
                user_handle VARCHAR(255),
                counter BIGINT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                INDEX idx_user (user_id)
            )
        ");
    }
    public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource
    {
        $stmt = $this->pdo->prepare("SELECT * FROM webauthn_credentials WHERE id = ?");
        $stmt->execute([$publicKeyCredentialId]);
        $data = $stmt->fetch(\PDO::FETCH_ASSOC);
        if (!$data) {
            return null;
        }
        return $this->hydrateCredentialSource($data);
    }
    public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array
    {
        $stmt = $this->pdo->prepare("SELECT * FROM webauthn_credentials WHERE user_id = ?");
        $stmt->execute([$publicKeyCredentialUserEntity->getId()]);
        $credentials = [];
        while ($data = $stmt->fetch(\PDO::FETCH_ASSOC)) {
            $credentials[] = $this->hydrateCredentialSource($data);
        }
        return $credentials;
    }
    public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void
    {
        $stmt = $this->pdo->prepare("
            INSERT INTO webauthn_credentials 
            (id, user_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ON DUPLICATE KEY UPDATE 
            counter = VALUES(counter)
        ");
        $stmt->execute([
            $publicKeyCredentialSource->getPublicKeyCredentialId(),
            $publicKeyCredentialSource->getUserHandle(),
            $publicKeyCredentialSource->getType(),
            json_encode($publicKeyCredentialSource->getTransports()),
            $publicKeyCredentialSource->getAttestationType(),
            json_encode($publicKeyCredentialSource->getTrustPath()),
            $publicKeyCredentialSource->getAaguid(),
            $publicKeyCredentialSource->getCredentialPublicKey(),
            $publicKeyCredentialSource->getUserHandle(),
            $publicKeyCredentialSource->getCounter()
        ]);
    }
    public function updateCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void
    {
        $this->saveCredentialSource($publicKeyCredentialSource);
    }
    private function hydrateCredentialSource(array $data): PublicKeyCredentialSource
    {
        return PublicKeyCredentialSource::createFromArray([
            'publicKeyCredentialId' => $data['id'],
            'type' => $data['type'],
            'transports' => json_decode($data['transports'], true),
            'attestationType' => $data['attestation_type'],
            'trustPath' => json_decode($data['trust_path'], true),
            'aaguid' => $data['aaguid'],
            'credentialPublicKey' => $data['credential_public_key'],
            'userHandle' => $data['user_handle'],
            'counter' => (int)$data['counter']
        ]);
    }
}

3 前端实现

<!-- webauthn.js -->
<script>
class WebAuthnHandler {
    constructor() {
        this.publicKeyOptions = null;
    }
    // 注册新用户
    async register(username) {
        try {
            // 1. 获取注册选项
            const response = await fetch('/api/webauthn/register-options', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ username })
            });
            const options = await response.json();
            // 2. 转换选项格式
            const publicKey = this.convertToPublicKeyOptions(options);
            // 3. 调用 WebAuthn API
            const credential = await navigator.credentials.create({ publicKey });
            // 4. 发送凭证给服务器
            const verifyResponse = await fetch('/api/webauthn/register-verify', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    id: credential.id,
                    rawId: this.arrayBufferToBase64(credential.rawId),
                    response: {
                        clientDataJSON: this.arrayBufferToBase64(credential.response.clientDataJSON),
                        attestationObject: this.arrayBufferToBase64(credential.response.attestationObject)
                    },
                    type: credential.type
                })
            });
            const result = await verifyResponse.json();
            if (result.success) {
                alert('注册成功!');
            } else {
                alert('注册失败:' + result.error);
            }
        } catch (error) {
            console.error('WebAuthn registration error:', error);
            alert('注册错误:' + error.message);
        }
    }
    // 登录
    async login(username) {
        try {
            // 1. 获取认证选项
            const response = await fetch('/api/webauthn/login-options', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ username })
            });
            const options = await response.json();
            // 2. 转换选项格式
            const publicKey = this.convertToPublicKeyOptions(options);
            // 3. 调用 WebAuthn API
            const assertion = await navigator.credentials.get({ publicKey });
            // 4. 发送断言给服务器
            const verifyResponse = await fetch('/api/webauthn/login-verify', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    id: assertion.id,
                    rawId: this.arrayBufferToBase64(assertion.rawId),
                    response: {
                        authenticatorData: this.arrayBufferToBase64(assertion.response.authenticatorData),
                        clientDataJSON: this.arrayBufferToBase64(assertion.response.clientDataJSON),
                        signature: this.arrayBufferToBase64(assertion.response.signature),
                        userHandle: assertion.response.userHandle ? 
                            this.arrayBufferToBase64(assertion.response.userHandle) : null
                    },
                    type: assertion.type
                })
            });
            const result = await verifyResponse.json();
            if (result.success) {
                alert('登录成功!');
                window.location.href = '/dashboard';
            } else {
                alert('登录失败:' + result.error);
            }
        } catch (error) {
            console.error('WebAuthn login error:', error);
            alert('登录错误:' + error.message);
        }
    }
    // 将服务器选项转换为浏览器可用的格式
    convertToPublicKeyOptions(options) {
        const publicKey = { ...options };
        // 转换 challenge
        if (publicKey.challenge) {
            publicKey.challenge = this.base64ToArrayBuffer(publicKey.challenge);
        }
        // 转换用户信息
        if (publicKey.user && publicKey.user.id) {
            publicKey.user.id = this.base64ToArrayBuffer(publicKey.user.id);
        }
        // 转换排除的凭证
        if (publicKey.excludeCredentials) {
            publicKey.excludeCredentials = publicKey.excludeCredentials.map(cred => ({
                ...cred,
                id: this.base64ToArrayBuffer(cred.id)
            }));
        }
        // 转换允许的凭证
        if (publicKey.allowCredentials) {
            publicKey.allowCredentials = publicKey.allowCredentials.map(cred => ({
                ...cred,
                id: this.base64ToArrayBuffer(cred.id)
            }));
        }
        return publicKey;
    }
    // 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);
    }
    // Base64 转 ArrayBuffer
    base64ToArrayBuffer(base64) {
        const binary = atob(base64);
        const bytes = new Uint8Array(binary.length);
        for (let i = 0; i < binary.length; i++) {
            bytes[i] = binary.charCodeAt(i);
        }
        return bytes.buffer;
    }
}
</script>

4 PHP API 路由

<?php
// api/webauthn.php
require_once 'vendor/autoload.php';
session_start();
header('Content-Type: application/json');
$service = new WebAuthnService();
$action = $_GET['action'] ?? $_POST['action'] ?? '';
switch ($action) {
    case 'register-options':
        $data = json_decode(file_get_contents('php://input'), true);
        try {
            $options = $service->generateRegistrationOptions(
                $data['username'],
                uniqid('user_')  // 生成唯一用户ID
            );
            echo json_encode(['success' => true, 'options' => $options]);
        } catch (\Exception $e) {
            http_response_code(400);
            echo json_encode([
                'success' => false, 
                'error' => $e->getMessage()
            ]);
        }
        break;
    case 'register-verify':
        $response = json_decode(file_get_contents('php://input'), true);
        $success = $service->verifyRegistrationResponse($response);
        echo json_encode(['success' => $success]);
        break;
    case 'login-options':
        $data = json_decode(file_get_contents('php://input'), true);
        try {
            $options = $service->generateAuthenticationOptions($data['username']);
            echo json_encode(['success' => true, 'options' => $options]);
        } catch (\Exception $e) {
            http_response_code(400);
            echo json_encode([
                'success' => false, 
                'error' => $e->getMessage()
            ]);
        }
        break;
    case 'login-verify':
        $response = json_decode(file_get_contents('php://input'), true);
        $success = $service->verifyAuthenticationResponse($response);
        echo json_encode(['success' => $success]);
        break;
    default:
        http_response_code(404);
        echo json_encode(['success' => false, 'error' => 'Unknown action']);
}

5 HTML 前端页面

<!DOCTYPE html>
<html>
<head>WebAuthn Demo</title>
</head>
<body>
    <h1>WebAuthn 认证演示</h1>
    <div>
        <h2>注册新用户</h2>
        <input type="text" id="register-username" placeholder="用户名">
        <button onclick="webauthnHandler.register(document.getElementById('register-username').value)">
            注册
        </button>
    </div>
    <div>
        <h2>登录</h2>
        <input type="text" id="login-username" placeholder="用户名">
        <button onclick="webauthnHandler.login(document.getElementById('login-username').value)">
            登录
        </button>
    </div>
    <script src="webauthn.js"></script>
    <script>
        const webauthnHandler = new WebAuthnHandler();
    </script>
</body>
</html>

安全注意事项

  1. HTTPS 必须:WebAuthn 要求安全上下文
  2. Challenge 验证:每次都必须验证
  3. 计数器检查:防止凭证克隆
  4. 用户验证:确保用户是真正的所有者
  5. 数据加密:存储凭证时考虑加密

常见问题

  • 浏览器兼容性:检查支持的浏览器
  • 测试环境:localhost 支持,生产必须 HTTPS
  • 多凭证管理:一个用户可以绑定多个设备
  • 错误处理:提供友好的用户提示

这个实现提供了完整的 WebAuthn 注册和认证流程,包含了前端和后端的完整代码,可以根据实际需求进行调整和优化。

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