本文目录导读:

在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)
);
安全注意事项
- 使用HTTPS(WebAuthn要求安全上下文)
- 验证origin(防止CSRF)
- 使用安全的随机数生成器生成challenge
- 限制重放攻击(使用nonce和时间戳)
- 存储公钥安全(使用加密存储)
- 实现速率限制(防止暴力攻击)
完整示例项目
推荐参考开源项目:
这是一个完整的FIDO2实现方案,实际部署时需要根据具体需求调整配置和安全措施。