PHP项目怎么实现FIDO2?

wen java案例 2

本文目录导读:

PHP项目怎么实现FIDO2?

  1. 核心概念
  2. 推荐PHP库
  3. 完整实现流程
  4. 数据库表结构
  5. 安全注意事项
  6. 完整示例项目

在PHP项目中实现FIDO2(WebAuthn)认证,通常需要结合前端JavaScript API和后端PHP库,以下是完整的实现方案:

核心概念

  • WebAuthn: 浏览器端API,用于生成和验证凭证
  • FIDO2: 包含WebAuthn + CTAP2(客户端到认证器协议)
  • 认证器: 安全密钥(YubiKey)、平台认证器(Touch ID、Windows Hello)

推荐PHP库

主流选择:

  • web-auth/webauthn-lib(最推荐,功能完善)
  • lbuchs/webauthn(轻量级)
  • spomky-labs/webauthn-framework(企业级)

安装示例(使用composer):

composer require web-auth/webauthn-lib

完整实现流程

1 注册流程

前端(JavaScript)

// 检测浏览器支持
if (!window.PublicKeyCredential) {
    alert('您的浏览器不支持WebAuthn');
    return;
}
// 1. 请求注册选项
async function startRegistration() {
    const response = await fetch('/api/webauthn/register/begin', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' }
    });
    const options = await response.json();
    // 2. 转换base64格式
    options.challenge = Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0));
    options.user.id = Uint8Array.from(atob(options.user.id), c => c.charCodeAt(0));
    // 3. 调用浏览器API创建凭证
    const credential = await navigator.credentials.create({ publicKey: options });
    // 4. 格式化响应数据
    const attestationObject = base64urlEncode(new Uint8Array(credential.response.attestationObject));
    const clientDataJSON = base64urlEncode(new Uint8Array(credential.response.clientDataJSON));
    // 5. 发送到服务器完成注册
    const verificationResponse = await fetch('/api/webauthn/register/complete', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            id: credential.id,
            rawId: base64urlEncode(new Uint8Array(credential.rawId)),
            type: credential.type,
            response: {
                attestationObject,
                clientDataJSON
            }
        })
    });
    const result = await verificationResponse.json();
    console.log(result.success ? '注册成功' : '注册失败');
}
function base64urlEncode(buffer) {
    return btoa(String.fromCharCode(...new Uint8Array(buffer)))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

后端(PHP)

<?php
// 注册起点
require 'vendor/autoload.php';
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialUserEntity;
use Webauthn\PublicKeyCredentialParameters;
use Webauthn\AuthenticatorSelectionCriteria;
// 生成注册选项
function getRegistrationOptions($userId, $username, $displayName) {
    $rpEntity = new PublicKeyCredentialRpEntity(
        'Your App Name',  // 应用名称
        'example.com'     // 域名
    );
    $userEntity = new PublicKeyCredentialUserEntity(
        $username,
        $userId,
        $displayName
    );
    $credentialParameters = [
        new PublicKeyCredentialParameters('public-key', -7),  // ES256
        new PublicKeyCredentialParameters('public-key', -257) // RS256
    ];
    $authenticatorSelection = new AuthenticatorSelectionCriteria(
        AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_PLATFORM,
        AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED,
        false
    );
    $options = new PublicKeyCredentialCreationOptions(
        $rpEntity,
        $userEntity,
        random_bytes(32),  // challenge
        $credentialParameters,
        60000,  // 超时时间
        $authenticatorSelection,
        PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE
    );
    // 保存挑战到session
    $_SESSION['webauthn_challenge'] = base64_encode($options->getChallenge());
    return [
        'challenge' => base64_encode($options->getChallenge()),
        'rp' => ['name' => $options->getRp()->getName(), 'id' => $options->getRp()->getId()],
        'user' => [
            'id' => base64_encode($options->getUser()->getId()),
            'name' => $options->getUser()->getName(),
            'displayName' => $options->getUser()->getDisplayName()
        ],
        'pubKeyCredParams' => [['type' => 'public-key', 'alg' => -7]],
        'authenticatorSelection' => ['authenticatorAttachment' => 'platform', 'userVerification' => 'required'],
        'timeout' => 60000,
        'attestation' => 'none'
    ];
}
// 完成注册
function verifyRegistration($credentialData) {
    // 验证挑战
    $expectedChallenge = base64_decode($_SESSION['webauthn_challenge']);
    // 解析客户端数据
    $clientDataJSON = json_decode(base64_decode($credentialData['response']['clientDataJSON']));
    // 验证challenge匹配
    $receivedChallenge = base64_decode(strtr($clientDataJSON->challenge, '-_', '+/'));
    if ($receivedChallenge !== $expectedChallenge) {
        throw new Exception('Challenge不匹配');
    }
    // 验证来源origin
    $expectedOrigin = 'https://example.com';
    if ($clientDataJSON->origin !== $expectedOrigin) {
        throw new Exception('Origin不匹配');
    }
    // 存储凭证(简化示例)
    $credentialId = base64_decode($credentialData['id']);
    $publicKey = $credentialData['response']['attestationObject']; // 实际需要解析
    // 保存到数据库
    saveCredential($userId, $credentialId, $publicKey);
    return true;
}

2 认证流程

前端

// 认证请求
async function startAuthentication() {
    const response = await fetch('/api/webauthn/login/begin', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username: 'user@example.com' })
    });
    const options = await response.json();
    // 转换格式
    options.challenge = Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0));
    options.allowCredentials = options.allowCredentials.map(cred => ({
        ...cred,
        id: Uint8Array.from(atob(cred.id), c => c.charCodeAt(0))
    }));
    // 调用浏览器API
    const assertion = await navigator.credentials.get({ publicKey: options });
    // 发送到服务器验证
    const verificationResponse = await fetch('/api/webauthn/login/complete', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            id: assertion.id,
            rawId: base64urlEncode(new Uint8Array(assertion.rawId)),
            type: assertion.type,
            response: {
                authenticatorData: base64urlEncode(new Uint8Array(assertion.response.authenticatorData)),
                signature: base64urlEncode(new Uint8Array(assertion.response.signature)),
                clientDataJSON: base64urlEncode(new Uint8Array(assertion.response.clientDataJSON)),
                userHandle: base64urlEncode(new Uint8Array(assertion.response.userHandle))
            }
        })
    });
    const result = await verificationResponse.json();
    return result.success;
}

后端

// 生成认证选项
function getAuthenticationOptions($username) {
    // 从数据库获取用户的凭证
    $credentials = getUserCredentials($username);
    $allowedCredentials = [];
    foreach ($credentials as $cred) {
        $allowedCredentials[] = new PublicKeyCredentialDescriptor(
            PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
            base64_decode($cred['credential_id'])
        );
    }
    $options = new PublicKeyCredentialRequestOptions(
        random_bytes(32),  // challenge
        60000,
        'example.com',
        $allowedCredentials,
        UserVerificationRequirement::REQUIRED
    );
    $_SESSION['auth_challenge'] = base64_encode($options->getChallenge());
    return $options;
}
// 验证认证
function verifyAuthentication($response) {
    $expectedChallenge = base64_decode($_SESSION['auth_challenge']);
    // 从数据库获取公钥
    $credentialId = base64_decode($response['id']);
    $publicKey = getPublicKeyFromDatabase($credentialId);
    // 验证签名(简化,实际使用库完成)
    // $verification = \Webauthn\PublicKeyCredentialLoader::loadAndCheckAssertion(...);
    return true;
}

数据库表结构

CREATE TABLE webauthn_credentials (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    credential_id VARCHAR(255) NOT NULL UNIQUE,
    public_key TEXT NOT NULL,
    counter INT DEFAULT 0,
    credential_type VARCHAR(50) DEFAULT 'public-key',
    transports JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

安全注意事项

  1. 使用HTTPS(WebAuthn要求安全上下文)
  2. 验证origin(防止CSRF)
  3. 使用安全的随机数生成器生成challenge
  4. 限制重放攻击(使用nonce和时间戳)
  5. 存储公钥安全(使用加密存储)
  6. 实现速率限制(防止暴力攻击)

完整示例项目

推荐参考开源项目:

这是一个完整的FIDO2实现方案,实际部署时需要根据具体需求调整配置和安全措施。

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