FIDO协议:为网站添加用户管理功能,支持FIDO2认证
FIDO协议基础
FIDO(Fast IDentity Online,快速在线身份认证)协议是一种开放标准,旨在实现更加安全、便捷的无密码身份验证机制,减少传统口令认证带来的风险,如密码泄露、重放攻击及钓鱼攻击等问题。该标准由FIDO联盟(FIDO Alliance) 推动,目前主要包括三种规范:FIDO UAF、FIDO U2F与FIDO2。FIDO UAF(Universal Authentication Framework)
(1)FIDO UAF(通用认证框架)
FIDO UAF支持用户通过设备上的本地生物识别技术(如指纹、人脸识别等)进行身份验证,而无需输入传统密码。在注册阶段,用户设备会创建一对密钥,并将私钥安全地保存于本地,仅将公钥提交到认证服务器。当用户再次登录时,设备利用私钥生成数字签名,服务器通过验证该签名来确认用户身份,从而实现无密码认证。
(2)FIDO U2F(通用第二因素)
FIDO U2F主要用于双因素认证(2FA)场景,通常与用户名及密码共同使用。在注册过程中,用户的安全密钥生成一对密钥,公钥存储于服务器端。当用户登录时,在输入密码后,系统会提示插入硬件密钥,密钥通过私钥对服务器提供的挑战值进行签名,从而完成身份验证。这种方式能够显著提升账号安全性。
(3)FIDO2(FIDO2 = WebAuthn+CTAP2)
FIDO2是该系列标准的最新版本,由WebAuthn与CTAP2两部分组成,可同时支持无密码登录与双因素认证。
WebAuthn是由W3C与FIDO联盟联合制定的基于公钥加密的Web认证标准,使网站能够调用系统或外部认证设备(如指纹识别器、USB安全密钥)作为登录凭证。
CTAP2则定义了浏览器/操作系统与认证器(例如指纹模块、YubiKey等硬件密钥)之间的通信方式。借助FIDO2,用户可在手机、电脑等多平台上使用安全设备进行登录,从而摆脱对传统密码的依赖。
FIDO2工作流程
FIDO2的整个身份认证流程分为注册阶段和登录阶段两部分。以下是其详细机制说明。
(1)注册阶段:当用户首次访问网站时,需要完成FIDO2凭证注册,过程如下。
a.用户发起注册请求:用户在网站注册界面(/register)输入用户名和密码等基础信息,并请求启用FIDO2认证。前端通过表单提交用户名和密码到后端。
b.服务器生成挑战值:网站服务器接收注册请求后,首先验证用户名是否已存在。若不存在,使用generate_password_hash()对密码进行加密存储。然后在/fido_register路由中,调用server.register_begin(user_entity)创建一个随机的挑战(challenge),确保认证过程的唯一性与安全性。服务器将挑战值和用户信息(用户ID、用户名、显示名称)、RP信息(应用名称和ID)、支持的公钥算法等打包成注册选项,并将其存储到会话中。
c.调用WebAuthn API:前端页面接收服务器返回的注册选项后,将Base64编码的challenge和user.id转换为ArrayBuffer格式,然后调用navigator.credentials.create()方法,并将服务器生成的挑战传递给浏览器。
d.生成密钥对:用户的认证设备(如指纹传感器、手机、硬件密钥等)接收该请求,并提示用户进行身份验证(如指纹或PIN码)。验证通过后,设备会生成独立的公私钥对。私钥:保存在本地设备,绝不会上传。公钥:将返回服务器用于后续验证。设备生成的凭证包含rawId(凭证ID)、response.clientDataJSON(客户端数据)和response.attestationObject(证明对象)。
e.服务器存储凭证:设备把公钥及相关元信息(如凭证ID、算法类型等)传回服务器。前端将凭证数据(Base64编码)发送到/fido_complete_register路由,服务器调用server.register_complete()验证凭证数据的有效性。验证成功后,服务器提取凭证ID和公钥,使用CBOR编码公钥,并将其与用户账户关联保存到users[username][“credentials”]中。
(2)登录阶段:当用户下次访问该网站时,可以直接通过FIDO2设备登录,过程如下。
a.发起登录请求:用户在/login页面输入用户名和密码,并请求使用FIDO2登录。前端通过表单提交用户名和密码到后端。
b.生成挑战信息:服务器接收登录请求后,首先使用check_password_hash()验证用户名和密码是否正确。验证成功后,将用户名存储到会话中。然后在 /fido_auth路由中,服务器为该会话生成新的挑战,从用户账户中读取已保存的凭证ID,调用server.authenticate_begin([credential_descriptor])生成认证选项,并将与用户关联的公钥元数据(凭证ID、challenge、rpId等)发送到客户端。
c.调用WebAuthn API:前端调用navigator.credentials.get()方法,将Base64编码的challenge和allowCredentials中的凭证ID转换为ArrayBuffer格式,将挑战值传递给认证设备。
d.签名生成:认证设备提示用户再次进行身份验证(如指纹识别),验证通过后,使用私钥对挑战进行签名,并返回签名数据。设备返回的断言包含rawId(凭证ID)、response.clientDataJSON(客户端数据)、response.authenticatorData(认证器数据)和response.signature(签名)。
e.验证签名:前端将签名数据(Base64编码)发送到/fido_login路由。服务器接收签名结果后,从会话中获取之前存储的认证状态,从用户账户中读取已保存的公钥,调用server.authenticate_complete()通过已保存的公钥进行验证,确认签名真实有效。若验证成功,即表明用户确实持有对应的私钥,登录被批准。服务器在会话中设置authenticated标志为True。
这一流程实现了真正的“无密码安全登录”,同时兼顾了跨平台兼容性和强身份保障,使FIDO2成为现代网络认证体系的核心标准之一。
项目实现
项目设计结构图如下:

1.首先配置环境,确保浏览器和设备支持FIDO2(WebAuthn)认证功能
pip install -r requirements.txt



2.编写程序
(1)首先在app.py中导入实现FIDO2认证所需的所有模块
1 2 3 4 5 6 7 8
| from flask import Flask, render_template, request, jsonify, session, redirect, url_for from werkzeug.security import generate_password_hash, check_password_hash from fido2.server import Fido2Server from fido2.webauthn import PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, PublicKeyCredentialDescriptor from fido2.utils import websafe_encode, websafe_decode import os import json import base64
|
模块说明:
Flask相关:用于构建We 应用和路由处理
werkzeug.security:用于密码加密和验证
fido2.server.Fido2Server:实现WebAuthn协议的FIDO2认证服务,用于生成和验证FIDO2注册和认证请求
fido2.webauthn:包含FIDO2凭证相关的数据结构
base64:用于编码/解码FIDO2数据
(2)初始化Flask应用和FIDO2服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import fido2.features fido2.features.webauthn_json_mapping.enabled = True
app = Flask(\__name_\_) app.secret_key = os.urandom(24)
rp = PublicKeyCredentialRpEntity(id="localhost", name="Example App")
def verify_origin(origin): return origin in \["http://localhost:5000", "https://localhost:5000"\]
server = Fido2Server(rp, attestation="none", verify_origin=verify_origin)
users = {}
|
配置说明:
RP Entity:代表依赖方(应用程序),用于标识应用
origin 验证函数:确保FIDO2请求来自正确的源
users字典:存储用户信息,包括密码哈希和FIDO2凭证
(3)用户注册界面
后端路由 (/register):
1 2 3 4 5 6 7 8 9 10 11
| @app.route("/register", methods=["GET", "POST"]) def register(): if request.method == "POST": username = request.form["username"] password = request.form["password"] if username in users: return "User already exists", 400 hashed_password = generate_password_hash(password) users[username] = {"password": hashed_password, "credentials": None} return redirect(url_for("fido_setup", username=username)) return render_template("register.html")
|
前端界面 (register.html):
用户输入用户名和密码
前端验证两次密码输入是否一致
提交表单后,后端存储加密的密码,并重定向到FIDO2设置页面
(4)FIDO2设置页面
后端路由(/fido_setup):
1 2 3 4 5
| @app.route("/fido_setup/<username>", methods=["GET"]) def fido_setup(username): if username not in users: return "User not found", 404 return render_template("fido_setup.html", username=username)
|
前端交互 (fido_setup.html):
显示FIDO2设置步骤指引
用户点击”开始FIDO2注册”按钮
前端向/fido_register发送请求获取注册选项
(5)生成FIDO2注册选项
后端路由(/fido_register):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @app.route("/fido_register", methods=["POST"]) def fido_register(): username = request.form.get("username") if not username or username not in users: return jsonify({"error": "User not found"}), 404
try: user_entity = PublicKeyCredentialUserEntity( id=username.encode(), name=username, display_name=username ) registration_data, state = server.register_begin(user_entity) session["fido_registration_state"] = state pk = registration_data.public_key challenge = base64.b64encode(pk.challenge).decode() user_id = base64.b64encode(pk.user.id).decode() response_data = { "publicKey": { "challenge": challenge, "rp": {"name": pk.rp.name, "id": pk.rp.id}, "user": { "id": user_id, "name": pk.user.name, "displayName": pk.user.display_name }, "pubKeyCredParams": [ {"type": "public-key", "alg": param.alg} for param in pk.pub_key_cred_params ], "timeout": 60000, "attestation": "direct" } } return jsonify(response_data) except Exception as e: return jsonify({"error": str(e)}), 400
|
前端处理 (fido_setup.html):
接收注册选项,将 Base64 编码的数据转换为ArrayBuffer
调用navigator.credentials.create()创建FIDO2凭证
用户使用生物识别或硬件密钥完成认证
(6)完成FIDO2注册
后端路由 (/fido_complete_register):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| @app.route("/fido_complete_register", methods=["POST"]) def fido_complete_register(): data = request.get_json() username = data.get("username") if not username or username not in users: return jsonify({"error": "User not found"}), 404 try: state = session.pop("fido_registration_state", None) if not state: return jsonify({"error": "Registration state not found"}), 400 from fido2.webauthn import CollectedClientData, AttestationObject client_data_bytes = base64.b64decode(data["response"]["clientDataJSON"]) attestation_object_bytes = base64.b64decode(data["response"]["attestationObject"]) client_data = CollectedClientData(client_data_bytes) attestation_object = AttestationObject(attestation_object_bytes) auth_data = server.register_complete(state, client_data, attestation_object) from fido2 import cbor cred_id = base64.b64encode(auth_data.credential_data.credential_id).decode() public_key = auth_data.credential_data.public_key public_key_bytes = cbor.encode(public_key) users[username]["credentials"] = json.dumps({ "credential_id": cred_id, "public_key": base64.b64encode(public_key_bytes).decode() }) return jsonify({"status": "registered"}) except Exception as e: return jsonify({"error": str(e)}), 400
|
(7)用户登录
后端路由(/login):
1 2 3 4 5 6 7 8 9 10
| @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": username = request.form["username"] password = request.form["password"] if username not in users or not check_password_hash(users[username]["password"], password): return "Invalid username or password", 400 session["username"] = username return redirect(url_for("fido_auth")) return render_template("login.html")
|
前端交互 (login.html):
用户输入用户名和密码
前端提交表单到后端验证
验证成功后,自动请求FIDO2认证选项
(8)FIDO2认证
后端路由 (/fido_auth):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| @app.route("/fido_auth", methods=["GET"]) def fido_auth(): username = session.get("username") if not username or username not in users: return "Unauthorized", 401 if users[username].get("credentials") is None: return "User has no FIDO2 credentials", 400 try: cred_data = json.loads(users[username]["credentials"]) credential_id = base64.b64decode(cred_data["credential_id"]) credential_descriptor = PublicKeyCredentialDescriptor( type="public-key", id=credential_id ) authentication_data, state = server.authenticate_begin([credential_descriptor]) session["fido_authentication_state"] = state pk = authentication_data.public_key challenge = base64.b64encode(pk.challenge).decode() cred_id_encoded = base64.b64encode(credential_id).decode() response_data = { "publicKey": { "challenge": challenge, "timeout": 60000, "rpId": pk.rp_id, "userVerification": "preferred", "allowCredentials": [ {"type": "public-key", "id": cred_id_encoded} ] } } return jsonify(response_data) except Exception as e: return jsonify({"error": str(e)}), 400
|
(9)完成FIDO2登录
后端路由 (/fido_login):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| @app.route("/fido_login", methods=["POST"]) def fido_login(): username = session.get("username") if not username or username not in users: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() try: state = session.pop("fido_authentication_state", None) if not state: return jsonify({"error": "Authentication state not found"}), 400 cred_data = json.loads(users[username]["credentials"]) stored_cred_id = base64.b64decode(cred_data["credential_id"]) from fido2 import cbor public_key_bytes = base64.b64decode(cred_data["public_key"]) public_key = cbor.decode(public_key_bytes) from fido2.webauthn import CollectedClientData, AuthenticatorData, AttestedCredentialData, Aaguid credential_id = base64.b64decode(data["id"]) client_data_bytes = base64.b64decode(data["response"]["clientDataJSON"]) authenticator_data_bytes = base64.b64decode(data["response"]["authenticatorData"]) signature = base64.b64decode(data["response"]["signature"]) client_data = CollectedClientData(client_data_bytes) authenticator_data = AuthenticatorData(authenticator_data_bytes) attested_cred_data = AttestedCredentialData.create( aaguid=Aaguid.NONE, credential_id=stored_cred_id, public_key=public_key ) server.authenticate_complete( state, [attested_cred_data], credential_id, client_data, authenticator_data, signature ) session["authenticated"] = True return jsonify({"status": "authenticated"}) except Exception as e: return jsonify({"error": str(e)}), 400
|
(10)仪表板和登出
后端路由:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @app.route("/", methods=["GET"]) def index(): if session.get("authenticated"): return redirect(url_for("dashboard")) return render_template("index.html")
@app.route("/dashboard", methods=["GET"]) def dashboard(): if not session.get("authenticated"): return redirect(url_for("login")) username = session.get("username") return render_template("dashboard.html", username=username)
@app.route("/logout", methods=["GET"]) def logout(): session.clear() return redirect(url_for("index"))
|
3.最后运行本项目,执行python app.py

前往http://localhost:5000/register注册新用户
前往http://localhost:5000/login进行登录
4.以下是界面操作展示
(1)网站界面

(2)创建新用户



(3)用户登录


(4)登录界面

四、总结实现原理
用户注册:在/register页面,用户提供用户名和密码进行注册。注册成功后,系统将用户信息(用户名和加密后的密码)存储到内存中,并将用户重定向到 /fido_setup 页面进行FIDO2凭证绑定。
FIDO2设置:在/fido_setup路由,用户点击”开始FIDO2注册”按钮后,前端向 /fido_register路由发送请求。后端生成注册选项(包含challenge、用户信息、RP信息等),并将其转换为JSON格式返回给前端。前端通过 navigator.credentials.create()调用用户的FIDO2设备(如生物识别或硬件密钥)创建凭证。生成的凭证数据(包含clientDataJSON和attestationObject)通过 /fido_complete_register路由发送回服务器,服务器验证并保存凭证信息(credential_id和public_key)到用户账户中。
用户登录:用户在/login页面输入用户名和密码。后端验证用户名和密码是否正确。若正确,系统将用户名存储到会话中,并将用户重定向到FIDO2认证流程。
FIDO2验证:登录表单提交后,前端向/fido_auth路由请求认证选项。后端根据存储的凭证信息生成认证选项(包含challenge和allowCredentials),返回给前端。前端调用navigator.credentials.get()完成FIDO2设备验证。验证成功后,生成的凭证数据(包含clientDataJSON、authenticatorData和signature)通过/fido_login路由发送到服务器进行最终验证。服务器验证签名和数据的完整性,若通过,则在会话中设置authenticated标志,用户便可访问/dashboard仪表盘。