本文目录导读:

我来详细介绍如何在 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>
安全注意事项
- HTTPS 必须:WebAuthn 要求安全上下文
- Challenge 验证:每次都必须验证
- 计数器检查:防止凭证克隆
- 用户验证:确保用户是真正的所有者
- 数据加密:存储凭证时考虑加密
常见问题
- 浏览器兼容性:检查支持的浏览器
- 测试环境:localhost 支持,生产必须 HTTPS
- 多凭证管理:一个用户可以绑定多个设备
- 错误处理:提供友好的用户提示
这个实现提供了完整的 WebAuthn 注册和认证流程,包含了前端和后端的完整代码,可以根据实际需求进行调整和优化。