本文目录导读:

我来详细解释PHP中如何实现Passkey(通行密钥)功能。
Passkey 概述
Passkey 是基于 WebAuthn 标准的无密码认证方式,使用公钥加密技术,用户通过生物识别或设备PIN完成认证。
PHP 实现方案
使用现成库(推荐)
Webauthn/webauthn-lib(最流行)
composer require web-auth/webauthn-lib
基础实现示例
注册流程(Registration)
<?php
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialUserEntity;
use Webauthn\PublicKeyCredentialParameters;
use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialLoader;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
class PasskeyService {
private $server;
public function __construct() {
// 初始化服务器
$rpEntity = new PublicKeyCredentialRpEntity(
'your-domain.com', // 域名
'Your App' // 应用名称
);
// 配置存储(需要实现)
$this->server = new WebauthnServer($rpEntity);
}
// 1. 生成注册选项
public function getRegistrationOptions($userId, $userName) {
$userEntity = new PublicKeyCredentialUserEntity(
$userId,
$userName,
$userName
);
$options = new PublicKeyCredentialCreationOptions(
$userEntity,
new PublicKeyCredentialParameters('public-key', -7), // ES256算法
$this->server->getRpEntity(),
$this->generateChallenge()
);
return $options;
}
// 2. 处理注册响应
public function handleRegistration($requestData, $userId) {
$publicKeyCredentialLoader = new PublicKeyCredentialLoader();
try {
$credential = $publicKeyCredentialLoader->loadJSON($requestData);
if ($credential->getResponse() instanceof AuthenticatorAttestationResponse) {
// 验证并存储凭证
$publicKeyCredentialSource = $this->server->processCredentialCreation(
$requestData,
$credential
);
// 保存到数据库
$this->saveCredential($userId, $publicKeyCredentialSource);
return ['success' => true];
}
} catch (\Exception $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
}
// 3. 生成认证选项
public function getAuthenticationOptions($userCredentials) {
$options = new PublicKeyCredentialRequestOptions(
$this->generateChallenge(),
$userCredentials
);
return $options;
}
// 4. 处理认证响应
public function handleAuthentication($requestData, $userCredentials) {
try {
$publicKeyCredentialLoader = new PublicKeyCredentialLoader();
$credential = $publicKeyCredentialLoader->loadJSON($requestData);
if ($credential->getResponse() instanceof AuthenticatorAssertionResponse) {
$authenticatorData = $this->server->processCredentialAuthentication(
$requestData,
$credential,
$userCredentials
);
// 认证成功
return ['success' => true, 'credentialId' => $credential->getId()];
}
} catch (\Exception $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
}
private function generateChallenge() {
return base64_encode(random_bytes(32));
}
private function saveCredential($userId, $credential) {
// 实现数据库存储逻辑
}
}
前端集成
HTML + JavaScript
<!DOCTYPE html>
<html>
<head>Passkey Demo</title>
</head>
<body>
<button id="registerBtn">注册Passkey</button>
<button id="loginBtn">Passkey登录</button>
<script>
class PasskeyClient {
constructor() {
this.baseUrl = 'https://your-domain.com';
}
// 注册
async register() {
try {
// 1. 获取注册选项
const optionsResponse = await fetch(`${this.baseUrl}/api/passkey/register-options`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({userId: 'user-123'})
});
const options = await optionsResponse.json();
// 2. 调用浏览器API
const credential = await navigator.credentials.create({
publicKey: options
});
// 3. 发送给服务器验证
const verifyResponse = await fetch(`${this.baseUrl}/api/passkey/register`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
credential: this.transformCredential(credential)
})
});
if (verifyResponse.ok) {
alert('注册成功!');
}
} catch (error) {
console.error('注册失败:', error);
}
}
// 登录
async login() {
try {
// 1. 获取认证选项
const optionsResponse = await fetch(`${this.baseUrl}/api/passkey/verify-options`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: 'user@example.com'})
});
const options = await optionsResponse.json();
// 2. 调用浏览器API
const credential = await navigator.credentials.get({
publicKey: options
});
// 3. 验证
const verifyResponse = await fetch(`${this.baseUrl}/api/passkey/verify`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
credential: this.transformCredential(credential)
})
});
if (verifyResponse.ok) {
alert('登录成功!');
}
} catch (error) {
console.error('登录失败:', error);
}
}
// 转换凭证格式
transformCredential(credential) {
return {
id: credential.id,
rawId: this.arrayBufferToBase64(credential.rawId),
type: credential.type,
response: {
clientDataJSON: this.arrayBufferToBase64(credential.response.clientDataJSON),
authenticatorData: credential.response.authenticatorData ?
this.arrayBufferToBase64(credential.response.authenticatorData) : null,
signature: credential.response.signature ?
this.arrayBufferToBase64(credential.response.signature) : null,
attestationObject: credential.response.attestationObject ?
this.arrayBufferToBase64(credential.response.attestationObject) : null
}
};
}
// 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);
}
}
// 初始化事件
document.getElementById('registerBtn').addEventListener('click', () => {
const client = new PasskeyClient();
client.register();
});
document.getElementById('loginBtn').addEventListener('click', () => {
const client = new PasskeyClient();
client.login();
});
</script>
</body>
</html>
数据库设计
-- 用户表
CREATE TABLE users (
id VARCHAR(64) PRIMARY KEY,
username VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Passkey凭证表
CREATE TABLE passkey_credentials (
id VARCHAR(255) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
public_key BLOB NOT NULL,
sign_count INT DEFAULT 0,
credential_id VARCHAR(255) NOT NULL,
aaguid VARCHAR(32),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 挑战表(用于存储临时认证挑战)
CREATE TABLE authentication_challenges (
id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64),
challenge BLOB NOT NULL,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
完整示例代码(PHP 8+)
<?php
require 'vendor/autoload.php';
use Webauthn\Server;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialUserEntity;
use Webauthn\AttestationStatement\AttestationObjectLoader;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
class PasskeyController {
private $server;
private $db;
public function __construct() {
$this->db = new PDO('mysql:host=localhost;dbname=passkey_db', 'user', 'password');
// 配置WebAuthn服务器
$rpEntity = new \Webauthn\PublicKeyCredentialRpEntity(
'localhost',
'Passkey Demo'
);
$this->server = Server::create($rpEntity);
$this->server->setCredentialRepository($this->getCredentialRepository());
}
// 处理注册请求
public function handleRegistration() {
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['username'])) {
// 创建新用户
$userId = bin2hex(random_bytes(16));
$username = $input['username'];
// 保存用户会话数据
$_SESSION['registration_user_id'] = $userId;
// 生成注册选项
$userEntity = new PublicKeyCredentialUserEntity(
$userId,
$username,
$username
);
$options = $this->server->generateRegistration(
$userEntity,
[
\Webauthn\PublicKeyCredentialParameters::create('public-key', -7)
]
);
// 返回给前端
header('Content-Type: application/json');
echo json_encode($options);
}
}
// 验证注册响应
public function verifyRegistration() {
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['credential'])) {
try {
$credential = $this->server->loadAndCheckAttestationResponse(
$input['credential'],
$_SESSION['registration_user_id']
);
// 获取公钥
$publicKey = $credential->getPublicKeyCredentialSource();
// 保存到数据库
$stmt = $this->db->prepare(
'INSERT INTO passkey_credentials (id, user_id, public_key, credential_id)
VALUES (?, ?, ?, ?)'
);
$stmt->execute([
bin2hex(random_bytes(16)),
$_SESSION['registration_user_id'],
$publicKey->getPublicKey(),
$credential->getId()
]);
echo json_encode(['success' => true]);
} catch (\Exception $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
}
}
}
// 认证流程
public function handleAuthentication() {
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['username'])) {
// 获取用户凭证
$stmt = $this->db->prepare(
'SELECT * FROM passkey_credentials
WHERE user_id = (SELECT id FROM users WHERE username = ?)'
);
$stmt->execute([$input['username']]);
$credentials = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($credentials) {
// 生成认证选项
$options = $this->server->generateAuthentication(
$this->getCredentialSources($credentials)
);
header('Content-Type: application/json');
echo json_encode($options);
}
}
}
// 验证认证响应
public function verifyAuthentication() {
$input = json_decode(file_get_contents('php://input'), true);
try {
$result = $this->server->loadAndCheckAssertionResponse(
$input['credential'],
$this->getCredentialSources()
);
// 认证成功
$_SESSION['user_id'] = $result->getUser()->getId();
echo json_encode(['success' => true]);
} catch (\Exception $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
}
}
// 获取凭证仓库
private function getCredentialRepository() {
return new class implements \Webauthn\PublicKeyCredentialSourceRepository {
private $db;
public function __construct() {
$this->db = new PDO('mysql:host=localhost;dbname=passkey_db', 'user', 'password');
}
public function findOneByCredentialId(string $publicKeyCredentialId):
?\Webauthn\PublicKeyCredentialSource {
// 从数据库查找凭证
$stmt = $this->db->prepare(
'SELECT * FROM passkey_credentials WHERE credential_id = ?'
);
$stmt->execute([$publicKeyCredentialId]);
$credential = $stmt->fetch(PDO::FETCH_ASSOC);
if ($credential) {
return \Webauthn\PublicKeyCredentialSource::createFromArray(
json_decode($credential['public_key'], true)
);
}
return null;
}
public function saveCredentialSource(
\Webauthn\PublicKeyCredentialSource $publicKeyCredentialSource
): void {
// 保存凭证
}
public function findAllByUserHandle(string $userHandle): array {
// 获取用户的所有凭证
$stmt = $this->db->prepare(
'SELECT * FROM passkey_credentials WHERE user_id = ?'
);
$stmt->execute([$userHandle]);
$credentials = $stmt->fetchAll(PDO::FETCH_ASSOC);
$sources = [];
foreach ($credentials as $credential) {
$sources[] = \Webauthn\PublicKeyCredentialSource::createFromArray(
json_decode($credential['public_key'], true)
);
}
return $sources;
}
};
}
}
路由配置
// routes.php
$router->post('/api/passkey/register-options', 'PasskeyController@handleRegistration');
$router->post('/api/passkey/register', 'PasskeyController@verifyRegistration');
$router->post('/api/passkey/verify-options', 'PasskeyController@handleAuthentication');
$router->post('/api/passkey/verify', 'PasskeyController@verifyAuthentication');
最佳实践建议
-
安全要求
- 使用HTTPS(必须)
- 有效且安全的域名
- 实施速率限制
- 使用CSRF保护
-
用户体验
- 提供降级方案(密码登录)
- 清晰的错误提示
- 支持多设备
-
配置要求
- 确保现代浏览器支持
- 正确配置CORS
- 处理移动端兼容性
-
测试建议
- 使用浏览器模拟器测试
- 测试不同认证器设备
- 确保错误处理完善
常见问题处理
// 错误处理示例
try {
// Passkey操作
} catch (\Webauthn\Exception\InvalidDataException $e) {
// 无效数据
http_response_code(400);
} catch (\Webauthn\Exception\ChallengeException $e) {
// 挑战验证失败
http_response_code(401);
} catch (\Webauthn\Exception\UserNotFoundException $e) {
// 用户不存在
http_response_code(404);
} catch (\Throwable $e) {
// 其他错误
error_log($e->getMessage());
http_response_code(500);
}
这是一个完整的Passkey实现方案,建议使用成熟库处理底层细节,并完善错误处理和日志记录。