一个为 Typecho 博客系统提供企业级 Passkey(WebAuthn)登录功能的插件,使用生物识别(指纹、面容)或设备 PIN 快速安全登录。
Passkey管理页面,轻松管理已绑定的凭证,查看登录历史记录。
配置注入模式、RP 信息和注册选项
后台登录页面启用 Passkey 登录
移动端管理页面深度优化,轻松管理 Passkey 设置
| 功能项 | 描述 |
|---|---|
| Passkey 登录 | 使用生物识别(指纹、面容)或设备 PIN 快速登录 |
| 后台管理 | 在 Typecho 后台管理和绑定 Passkey |
| 登录记录 | 仪表盘查询近期 Passkey 登录历史,掌握账户安全状况 |
| 自动注入 | 自动在登录页面添加 Passkey 登录选项 |
| 手动模式 | 支持手动控制登录按钮的显示位置 |
| 多设备支持 | 可以绑定多个设备的 Passkey |
| 注册支持 | 允许新用户通过 Passkey 创建账户 |
| 功能项 | 描述 |
|---|---|
| 完整签名验证 | 服务器端实现 ES256/RS256 算法验证(PHP OpenSSL) |
| IEEE P1363 ↔ DER | 自动转换 WebAuthn 签名格式兼容 OpenSSL |
| 速率限制 | 基于 Session 的注册/登录频率限制,防暴力破解 |
| Challenge 验证 | 可配置超时时间(60-600秒),防重放攻击 |
| 签名计数器 | 检测克隆的认证器(Clone Detection) |
| Origin 验证 | 严格/宽松模式,防域名欺骗 |
| 数据长度限制 | 防止恶意超大数据 DoS 攻击 |
| 安全日志记录 | 完整记录验证失败事件,便于审计 |
| 功能项 | 描述 |
|---|---|
| 用户体验 | 优雅通知 |
| 响应式设计 | 适配 Passport 设计系统(无荧光、无圆角、无阴影),完全支持移动端 |
| 触摸友好 | 按钮和交互元素适合触摸操作,支持移动设备 |
| 版本控制 | 资源文件带版本号,避免缓存 |
| 完整卸载 | 移除插件时可选删除所有数据 |
| 功能项 | 描述 |
|---|---|
| 智能检测 | 自动识别浏览器类型和版本,支持 8+ 种设备检测方法 |
| Safari 适配 | Safari < 14 自动跳过不支持的选项,iOS 14.5+ 优化 |
| Firefox 增强 | 版本检查和友好错误提示,Firefox Android 92+ 支持 |
| 条件特性 | 动态调整 WebAuthn 选项,针对不同设备优化参数 |
PHP 扩展检查:
php -m | grep -E 'openssl|mbstring|json|session'
下载插件
cd /var/www/typecho/usr/plugins/
# 上传或克隆 Passkey 文件夹
设置权限
chmod -R 755 Passkey
chown -R www-data:www-data Passkey
目录结构确认
usr/plugins/Passkey/ ├── Plugin.php # 主插件类 ├── Action.php # API 处理类 ├── Panel.php # 管理面板 ├── WebAuthn.php # WebAuthn 验证类 ├── LICENSE └── assist/ ├── css/ │ └── style.css # 样式文件 └── js/ └── passkey.js # 核心 JavaScript
启用插件
配置插件
cd /var/www/typecho/usr/plugins/
git clone https://github.com/little-gt/PLUGION-Passkey/Passkey.git
chmod -R 755 Passkey
进入「控制台」→「插件」→「Passkey」→「设置」
插件会自动在 Typecho 登录页面注入 Passkey 登录按钮,无需修改任何代码。
实现方式:
header.php 中调用 $this->header() 时自动注入 CSS 和 JS 资源需要在主题登录页面中手动添加 Passkey 登录代码。
步骤:
themes/你的主题/login.php 或 page-login.php)<form>...</form></form> 后面添加以下代码:<!-- Passkey 登录 -->
<link rel="stylesheet" href="<?php echo $this->options->pluginUrl; ?>/Passkey/assist/css/style.css?v=1.0.6">
<script>var PASSKEY_ACTION_URL = "<?php echo $this->options->index; ?>/action/passkey";</script>
<script src="<?php echo $this->options->pluginUrl; ?>/Passkey/assist/js/passkey.js?v=1.0.6"></script>
<div id="passkey-login-container" style="margin-top: 20px;">
<div style="text-align: center; margin-bottom: 10px;">
<span style="color: #999;">或</span>
</div>
<button type="button" id="passkey-login-btn" class="btn primary" style="width: 100%;">
使用 Passkey 登录
</button>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var btn = document.getElementById('passkey-login-btn');
if (btn) {
btn.addEventListener('click', function() {
PasskeyManager.login();
});
}
});
</script>
RP 名称
RP ID
启用后,未登录用户可以在登录页面使用 Passkey 创建新账户(无需输入用户名密码)。
⚠️ 重要:此设置受 Typecho 全局注册设置控制
请先到「设置」→「基本」→「允许注册」中开启全局注册功能。
插件提供完整的安全配置系统,支持三种预设模式和自定义模式:
三种预设安全模式:
| 模式 | 适用场景 | 速率限制 | Challenge 超时 | Origin 验证 |
|---|---|---|---|---|
| 开发模式 | 开发/测试环境 | 50次/IP/小时 | 600秒 (10分钟) | 宽松模式 |
| 常规模式 | 个人博客/小型站点 | 10次/IP/小时 | 300秒 (5分钟) | 标准验证 |
| 严格模式 | 高安全需求场景 | 5次/IP/小时 | 180秒 (3分钟) | 严格匹配 |
可配置的安全参数(10+ 项):
| 配置项 | 范围 | 说明 |
|---|---|---|
| 速率限制 | ||
| maxAttemptsPerIP | 1-100 | 每小时每 IP 最大尝试次数 |
| maxAttemptsPerHour | 1-100 | 每小时每用户最大尝试次数 |
| 会话管理 | ||
| sessionTimeout | 60-600秒 | Challenge 超时时间,防重放攻击 |
| 数据长度限制 | ||
| maxChallengeLength | 256-2048 | Challenge 最大长度(字节) |
| maxClientDataLength | 2048-16384 | ClientDataJSON 最大长度 |
| maxAttestationLength | 16384-131072 | AttestationObject 最大长度 |
| maxAuthDataLength | 16384-131072 | AuthenticatorData 最大长度 |
| maxSignatureLength | 256-2048 | 签名最大长度 |
| maxPublicKeyLength | 2048-16384 | 公钥最大长度 |
| CBOR 安全 | ||
| maxCBORDepth | 5-20 | CBOR 解码最大深度(防递归攻击) |
| 验证策略 | ||
| originValidationMode | strict/standard/relaxed | Origin 验证模式 |
进入管理页面
添加新凭证
管理凭证
查看登录记录
访问登录页面
进行身份验证
自动登录
如果启用了 Passkey 注册功能:
触发注册流程
填写注册信息
创建凭证
插件实现了完整的服务器端 WebAuthn 签名验证,支持主流算法:
| 算法 | COSE ID | 说明 | 实现方式 |
|---|---|---|---|
| ES256 | -7 | ECDSA P-256 + SHA-256 | PHP OpenSSL + DER 转换 |
| RS256 | -257 | RSA PKCS#1 + SHA-256 | PHP OpenSSL |
关键技术点:
openssl_verify() 验证 SHA-256 签名// IEEE P1363 转 DER
private static function ieee1363ToDer($signature) {
$r = substr($signature, 0, 32);
$s = substr($signature, 32, 32);
// 编码为 DER INTEGER
$rDer = self::encodeDERInteger($r);
$sDer = self::encodeDERInteger($s);
// 构造 SEQUENCE
return "\x30" . chr(strlen($rDer . $sDer)) . $rDer . $sDer;
}
// 验证 ES256 签名
private static function verifyES256($data, $signature, $publicKey) {
// 自动检测并转换格式
if (strlen($signature) === 64) {
$signature = self::ieee1363ToDer($signature);
}
// 构造 PEM 格式公钥
$pem = self::buildECPublicKeyPEM($publicKey['x'], $publicKey['y']);
// OpenSSL 验证
return openssl_verify($data, $signature, $pem, OPENSSL_ALGO_SHA256) === 1;
}
插件自动创建 2 个数据表:
CREATE TABLE typecho_passkey_credentials (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
credential_id VARCHAR(512) NOT NULL, -- 优化为 512(utf8mb4 兼容)
public_key TEXT NOT NULL,
counter INT DEFAULT 0,
created_at INT NOT NULL,
last_used INT DEFAULT NULL,
UNIQUE KEY unique_credential (credential_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
字段说明:
id - 主键user_id - 关联的 Typecho 用户 IDcredential_id - WebAuthn 凭证唯一标识符(Base64 编码,最大 512 字符)public_key - COSE 格式公钥数据(Base64 编码)counter - 签名计数器(防重放攻击和克隆检测)created_at - 创建时间戳last_used - 最后使用时间戳CREATE TABLE typecho_passkey_login_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
credential_id INT NOT NULL,
challenge TEXT NOT NULL,
ip_address VARCHAR(45) NOT NULL,
user_agent TEXT,
login_time INT NOT NULL,
status VARCHAR(20) DEFAULT 'success',
INDEX idx_user_id (user_id),
INDEX idx_login_time (login_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
字段说明:
id - 主键user_id - 关联的 Typecho 用户 IDcredential_id - 使用的凭证 ID(外键关联 passkey_credentials.id)challenge - 本次登录使用的挑战值(用于审计)ip_address - 登录 IP 地址(支持 IPv4 和 IPv6)user_agent - 浏览器用户代理字符串login_time - 登录时间戳status - 登录状态(success/failed)通过 /action/passkey 访问,支持以下操作:
| 端点 | 方法 | 说明 | 登录要求 |
|---|---|---|---|
?do=register-options | GET/POST | 获取注册选项 | 否(支持新用户注册) |
?do=register-verify | POST | 验证注册凭证 | 否 |
?do=login-options | GET | 获取登录选项 | 否 |
?do=login-verify | POST | 验证登录凭证 | 否 |
?do=list | GET | 列出用户的凭证 | 是 |
?do=login-logs | GET | 获取登录历史记录 | 是 |
?do=delete | POST | 删除凭证 | 是 |
Endpoint: GET/POST /action/passkey?do=register-options
请求体(新用户注册时):
{
"username": "myusername",
"email": "user@example.com",
"screenName": "My Display Name"
}
响应:
{
"success": true,
"data": {
"challenge": "base64_encoded_challenge",
"rp": {
"name": "My Website",
"id": "example.com"
},
"user": {
"id": "base64_encoded_user_id",
"name": "username",
"displayName": "Display Name"
},
"pubKeyCredParams": [
{"type": "public-key", "alg": -7},
{"type": "public-key", "alg": -257}
],
"timeout": 60000,
"attestation": "none",
"authenticatorSelection": {
"authenticatorAttachment": "platform",
"requireResidentKey": false,
"userVerification": "preferred"
}
}
}
Endpoint: POST /action/passkey?do=register-verify
请求体:
{
"id": "credential_id",
"rawId": "credential_id",
"type": "public-key",
"response": {
"clientDataJSON": "base64_encoded_client_data",
"attestationObject": "base64_encoded_attestation"
}
}
响应:
{
"success": true,
"data": {
"message": "Passkey registered successfully",
"isNewUser": false
}
}
Endpoint: GET /action/passkey?do=login-options
响应:
{
"success": true,
"data": {
"challenge": "base64_encoded_challenge",
"timeout": 60000,
"rpId": "example.com",
"userVerification": "preferred"
}
}
Endpoint: POST /action/passkey?do=login-verify
请求体:
{
"id": "credential_id",
"rawId": "credential_id",
"type": "public-key",
"response": {
"authenticatorData": "base64_encoded_auth_data",
"clientDataJSON": "base64_encoded_client_data",
"signature": "base64_encoded_signature"
}
}
响应:
{
"success": true,
"data": {
"message": "登录成功",
"redirect": "https://example.com/admin/",
"user": {
"name": "username",
"screenName": "Display Name"
}
}
}
Endpoint: GET /action/passkey?do=list
响应:
{
"success": true,
"data": [
{
"id": 1,
"credential_id": "YWJjZGVm...",
"created_at": "2026-02-22 14:30:00"
}
]
}
Endpoint: GET /action/passkey?do=login-logs&limit=20
参数:
limit - 返回记录数(1-100,默认 10)响应:
{
"success": true,
"data": [
{
"id": 1,
"credential_id": "YWJjZGVm...",
"ip_address": "192.168.1.1",
"user_agent": "Chrome / Windows",
"login_time": "2026-02-22 14:30:00",
"status": "success"
}
]
}
Endpoint: POST /action/passkey?do=delete
请求体:
{
"id": 1
}
响应:
{
"success": true,
"data": {
"message": "凭证已删除"
}
}
{
"success": false,
"error": "错误信息",
"errorCode": "ERR_VALIDATION" // 仅在调试模式显示
}
错误代码:
| 错误代码 | 说明 |
|---|---|
ERR_VALIDATION | 输入验证错误 |
ERR_AUTH_FAILED | 认证失败 |
ERR_ORIGIN_MISMATCH | Origin 不匹配 |
ERR_CREDENTIAL_LENGTH | 凭证长度超限 |
ERR_DUPLICATE | 重复凭证 |
ERR_RATE_LIMIT | 速率限制 |
ERR_SESSION | 会话错误 |
ERR_NETWORK | 网络错误 |
ERR_UNKNOWN | 未知错误 |
🟢 开发模式(推荐:开发/测试环境)
适用场景:开发环境、测试环境 速率限制:50次/IP/小时 Challenge 超时:600秒(10分钟) Origin 验证:宽松模式(支持子域名和端口差异) 性能影响:极低
🟡 常规模式(推荐:个人博客)
适用场景:个人博客、小型站点(日均 PV < 1000) 速率限制:10次/IP/小时 Challenge 超时:300秒(5分钟) Origin 验证:标准模式(验证协议和主域名) 性能影响:低
🔴 严格模式(推荐:高安全需求)
适用场景:金融/支付相关、高价值内容管理 速率限制:5次/IP/小时 Challenge 超时:180秒(3分钟) Origin 验证:严格模式(完全匹配协议+域名+端口) 性能影响:中等
// 检查浏览器支持
if (PasskeyManager.isSupported()) {
console.log('浏览器支持 WebAuthn');
} else {
console.log('浏览器不支持,需要升级');
}
// 注册 Passkey(后台管理页面)
PasskeyManager.register()
.then(result => {
console.log('注册成功', result);
// result 包含服务器返回的数据
})
.catch(error => {
console.error('注册失败', error.message);
// 错误类型:NotAllowedError, InvalidStateError 等
});
// 使用 Passkey 登录(登录页面)
PasskeyManager.login()
.then(result => {
console.log('登录成功', result);
// 自动跳转到 result.redirect
window.location.href = result.redirect;
})
.catch(error => {
console.error('登录失败', error.message);
});
// 显示网页内通知
PasskeyManager.showNotification('操作成功', 'success');
PasskeyManager.showNotification('操作失败', 'error');
PasskeyManager.showNotification('提示信息', 'info');
// 样式类型
- success: 绿色,成功操作
- error: 红色,错误信息
- info: 蓝色,提示信息
- warning: 黄色,警告信息
// 特性
- 自动定位到页面顶部
- 5 秒后自动消失
- 支持多条通知队列
- 响应式设计,移动端友好
核心特性: 移动端兼容性优化
核心更新:
核心更新:
siteUrl 作为可信来源主要修复:
核心安全特性:
新增功能:
新增功能:
核心功能:
原因:
解决方案:
原因:
解决方案:
原因:
解决方案:
原因:
解决方案:
答案: 可以!
答案: 可以!
答案: 是的,更安全!
答案: 可以自由选择!
编辑 config.inc.php:
/** 开启调试模式 */
define('__TYPECHO_DEBUG__', true);
按 F12 打开开发者工具:
// 检查支持
console.log('WebAuthn 支持:', PasskeyManager.isSupported());
// 查看详细错误
PasskeyManager.login().catch(error => {
console.error('错误名称:', error.name);
console.error('错误信息:', error.message);
});
| 错误 | 说明 | 解决方案 |
|---|---|---|
NotAllowedError | 用户取消或超时 | 重新尝试,不要取消弹窗 |
InvalidStateError | 设备未注册 | 先在后台添加 Passkey |
NotSupportedError | 设备不支持 | 更换支持的设备或浏览器 |
SecurityError | 安全上下文错误 | 使用 HTTPS 或 localhost |
-- 查看数据表
SHOW TABLES LIKE '%passkey%';
-- 查看凭证数据
SELECT * FROM typecho_passkey_credentials;
-- 查看登录记录
SELECT * FROM typecho_passkey_login_logs ORDER BY login_time DESC LIMIT 10;
-- 检查表结构
DESC typecho_passkey_credentials;
DESC typecho_passkey_login_logs;
本插件遵循 MIT 许可证开源。
Made with ❤️ by GARFIELDTOM & little-AI