本文目录导读:

我来详细介绍如何在 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 注册和认证流程,包含了前端和后端的完整代码,可以根据实际需求进行调整和优化。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播