无密码登录的安全问题及采用WebAuthn技术与实现分析

无密码登录的安全问题及采用WebAuthn技术与实现分析

使用用户名+FingerprintJS无密码登录和用户名+密码登录和用户名+WebAuthn的安全性比较

以下是对三种登录方式的安全性比较:用户名 + FingerprintJS(设备指纹)无密码登录用户名 + 密码登录用户名 + WebAuthn(Passkey / FIDO2)登录

三种方式都支持“用户名 + 某种凭证”的模式,并存时用户可选择(例如登录页面有三个按钮/选项)。比较维度以真实世界攻击场景为主(2025–2026 年常见威胁)。

维度用户名 + 密码登录用户名 + FingerprintJS 无密码登录用户名 + WebAuthn 无密码登录(Passkey)安全性排名(1最高)
凭证窃取风险高(密码可被 keylogger、phishing 页面窃取)中低(指纹数据可被 JS 窃取,但需先访问真实站点)极低(私钥永不离开设备,无法被窃取)WebAuthn > FingerprintJS > 密码
钓鱼(Phishing)抵抗力差(用户易输入密码到假网站)差(指纹在假网站也能采集,攻击者可重放或伪造)极强(RP ID / origin 绑定,假网站 challenge 无效)WebAuthn >> 其他两种
中间人攻击(MitM)抵抗差(密码明文传输易被截获,即使 HTTPS 后端弱)中(指纹数据传输,但无加密绑定)极强(公钥加密 + origin 校验)WebAuthn >> 其他
凭证重放攻击高(密码可无限重放)中(指纹可被重放,但可加 nonce / 时间戳缓解)极低(challenge 每次不同 + counter 防重放)WebAuthn > FingerprintJS > 密码
克隆 / 复制设备攻击高(密码知道即可在任意设备登录)中高(指纹变化大时失效,但高级攻击者可 spoof 指纹)低(私钥绑定硬件,克隆极难)WebAuthn > FingerprintJS > 密码
XSS 攻击下凭证泄露高(密码输入框易被窃取)高(localStorage 中的 device-secret 易读)低(私钥不暴露在 JS 可访问存储中)WebAuthn > 其他
肩窥 / 键盘记录高(用户输入密码)低(无需输入任何东西)低(只需 PIN/指纹验证)FingerprintJS ≈ WebAuthn > 密码
用户体验(摩擦)中(需记住密码)低(几乎无缝,但首次需密码绑定)低–中(首次注册稍麻烦,后续一键)FingerprintJS ≈ WebAuthn > 密码
隐私影响低(只存 hash)高(指纹可跨站追踪用户,GDPR/CCPA 风险大)中(不收集额外指纹,但设备信息有限泄露)密码 > WebAuthn > FingerprintJS
实现复杂度 & 维护成本中(需 FingerprintJS + 后端比对逻辑)高(需 webauthn4j / 类似库 + challenge 管理)密码 < FingerprintJS < WebAuthn
NIST / OWASP 推荐级别AAL1–AAL2AAL2(勉强)AAL3(最高,phishing-resistant MFA)WebAuthn >> 其他

总结对比(2026 年视角)

场景 / 威胁模型推荐方式为什么
高价值目标(银行、金融、企业内部系统)用户名 + WebAuthn唯一真正 phishing-resistant 的方案,NIST 推荐 AAL3,FIDO Alliance 标准
中价值目标(电商、一般 SaaS)WebAuthnFingerprintJS + 辅助FingerprintJS 作为补充(设备绑定),但单独用不推荐;WebAuthn 更安全
低价值 / 高便利需求(论坛、内容站)FingerprintJS 无密码密码成本低,用户摩擦小,但安全性明显弱于 WebAuthn
最差组合仅用户名 + FingerprintJS极易被高级钓鱼 + XSS 组合攻击,隐私问题严重,不建议作为唯一方式

采用密码登录和用户名+WebAuthn并存策略

WebAuthn 是一个浏览器原生 API,允许网站使用公钥凭证(public-key credentials) 来安全认证用户身份,支持无密码登录生物识别(指纹、面部)、设备 PIN硬件安全密钥(如 YubiKey),彻底取代传统密码 + 短信验证码的弱认证方式。

它属于 FIDO2 框架的核心组成部分(FIDO2 = WebAuthn + CTAP2)。

为什么需要 WebAuthn?(解决传统密码的痛点)

传统用户名 + 密码存在以下致命问题:

  • 易被钓鱼(phishing):用户在假网站输入密码
  • 易被泄露:数据库被黑、keylogger、肩窥
  • 易被重放:密码一旦泄露可在任意设备无限使用
  • 用户体验差:需要记住复杂密码、经常重置

WebAuthn 通过公钥密码学 + 设备绑定 解决这些:

  • 私钥永不离开用户设备(手机、电脑、硬件密钥),服务器只存公钥
  • origin / RP ID 绑定:假网站无法使用你在真实站点注册的凭证(强防钓鱼)
  • challenge + 签名:每次认证用随机挑战,防重放
  • counter 机制:防止同一凭证被克隆使用
  • 支持生物识别(指纹/面部)或PIN,无需输入密码

必要环境

环境开发阶段可接受值生产环境必须值备注
协议http://localhosthttps://几乎所有浏览器强制要求
域名(RP ID)localhost你的真实域名(example.com)不能是 IP、127.0.0.1、子域名(除非明确配置)
端口任意(5500、3000、8080 等)443(标准 HTTPS)或自定义但需证书端口不影响,但必须 HTTPS

流程图

在这里插入图片描述

详细版流程图(完整前后端逻辑验证)

这是一个详细的序列图,包含所有技术细节:challenge 生成、浏览器 API 调用、后端具体验证点(challenge 匹配、签名验证、counter 检查等)。强调 WebAuthn 阶段的前后端交互和安全校验。假设使用 webauthn4j 后端库。

在这里插入图片描述

代码实现

这是一个前后端分离的完整示例,使用 Spring Boot 作为后端,实现 WebAuthn 的注册和认证流程,前端使用你提供的单文件 HTML 风格(但调整为与后端 API 配合)。

webauthn-demo/ ├── backend/ # Spring Boot 项目 │ ├── src/main/java/... │ ├── pom.xml │ └── ... └── frontend/ └── index.html # 单文件前端(或 index.html) 

后端:Spring Boot + webauthn4j

pom.xml(核心依赖)

XML

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>webauthn-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>webauthn-demo</name> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.4</version> <relativePath/> </parent> <properties> <java.version>21</java.version> <webauthn4j.version>0.28.0.RELEASE</webauthn4j.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.webauthn4j</groupId> <artifactId>webauthn4j-core</artifactId> <version>${webauthn4j.version}</version> </dependency> <dependency> <groupId>com.webauthn4j</groupId> <artifactId>webauthn4j-util</artifactId> <version>${webauthn4j.version}</version> </dependency> <!-- 用于临时存储 challenge(生产环境建议用 Redis) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> 

核心配置类:WebAuthnConfig.java

Java

package com.example.webauthndemo.config; import com.webauthn4j.WebAuthnManager; import com.webauthn4j.converter.jackson.ObjectConverter; import com.webauthn4j.credential.CredentialRecord; import com.webauthn4j.data.RelyingPartyIdentity; import com.webauthn4j.server.ServerProperty; import com.webauthn4j.validator.WebAuthnValidator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Set; @Configuration public class WebAuthnConfig { @Bean public RelyingPartyIdentity relyingPartyIdentity() { return RelyingPartyIdentity.builder() .id("localhost") // 开发时用 localhost,生产换成你的域名 .name("WebAuthn Demo") .build(); } @Bean public WebAuthnManager webAuthnManager() { return new WebAuthnManager(); } @Bean public WebAuthnValidator webAuthnValidator() { return new WebAuthnValidator(); } @Bean public ObjectConverter objectConverter() { return new ObjectConverter(); } } 

简易内存存储 CredentialRepository(开发测试用)

Java

package com.example.webauthndemo.repository; import com.webauthn4j.credential.CredentialRecord; import com.webauthn4j.data.ByteArray; import com.webauthn4j.data.PublicKeyCredentialDescriptor; import com.webauthn4j.data.PublicKeyCredentialType; import com.webauthn4j.server.ServerProperty; import org.springframework.stereotype.Component; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @Component public class InMemoryCredentialRepository { // userHandle → List<CredentialRecord> private final Map<ByteArray, List<CredentialRecord>> credentials = new ConcurrentHashMap<>(); public void save(CredentialRecord credentialRecord) { ByteArray userHandle = credentialRecord.getUserHandle(); credentials.computeIfAbsent(userHandle, k -> new ArrayList<>()).add(credentialRecord); } public List<CredentialRecord> findByUserHandle(ByteArray userHandle) { return credentials.getOrDefault(userHandle, Collections.emptyList()); } public Optional<CredentialRecord> findByCredentialId(ByteArray credentialId) { return credentials.values().stream() .flatMap(List::stream) .filter(c -> c.getCredentialId().equals(credentialId)) .findFirst(); } public Set<PublicKeyCredentialDescriptor> getCredentialDescriptors(ByteArray userHandle) { List<CredentialRecord> records = findByUserHandle(userHandle); Set<PublicKeyCredentialDescriptor> descriptors = new HashSet<>(); for (CredentialRecord record : records) { descriptors.add(new PublicKeyCredentialDescriptor( PublicKeyCredentialType.PUBLIC_KEY, record.getCredentialId(), record.getTransports() )); } return descriptors; } } 

Controller:WebAuthnController.java

Java

package com.example.webauthndemo.controller; import com.example.webauthndemo.repository.InMemoryCredentialRepository; import com.webauthn4j.WebAuthnManager; import com.webauthn4j.converter.jackson.ObjectConverter; import com.webauthn4j.credential.CredentialRecord; import com.webauthn4j.data.*; import com.webauthn4j.data.client.challenge.DefaultChallenge; import com.webauthn4j.server.ServerProperty; import com.webauthn4j.validator.WebAuthnValidator; import com.webauthn4j.validator.attestation.statement.COSEAlgorithmIdentifier; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.*; @RestController @RequestMapping("/api/webauthn") @RequiredArgsConstructor public class WebAuthnController { private final InMemoryCredentialRepository credentialRepository; private final WebAuthnManager webAuthnManager; private final WebAuthnValidator webAuthnValidator; private final ObjectConverter objectConverter; private static final String RP_ID = "localhost"; private static final String ORIGIN = "http://localhost:5500"; // 改成你前端实际端口 // 1. 开始注册 @PostMapping("/register/start") public ResponseEntity<Map<String, Object>> startRegistration() { Challenge challenge = new DefaultChallenge(); PublicKeyCredentialCreationOptions options = new PublicKeyCredentialCreationOptions( new RelyingPartyIdentity(RP_ID, "WebAuthn Demo"), new UserIdentity( new ByteArray(UUID.randomUUID().toString().getBytes()), "testuser", "测试用户" ), challenge, List.of( new PublicKeyCredentialParameters( PublicKeyCredentialType.PUBLIC_KEY, COSEAlgorithmIdentifier.ES256 ) ), null, new AuthenticatorSelectionCriteria( null, true, // residentKey UserVerificationRequirement.PREFERRED ), null, null, null ); // 生产环境应该把 challenge 存到 session/redis,关联用户 // 这里简化,假定单用户 Map<String, Object> json = objectConverter.getJsonConverter().writeValueAsMap(options); return ResponseEntity.ok(json); } // 2. 完成注册 @PostMapping("/register/finish") public ResponseEntity<String> finishRegistration(@RequestBody Map<String, Object> request) { try { // 实际项目中应该从 session 取 challenge,这里简化 PublicKeyCredential<AuthenticatorAttestationResponse, CollectedClientData> pkc = objectConverter.getJsonConverter().readValue(request, PublicKeyCredential.class); // 这里需要你自己实现验证逻辑(challenge、origin、rpId 等) // 简化示例: CredentialRecord credentialRecord = webAuthnManager.parseRegistrationResponse(pkc); credentialRepository.save(credentialRecord); return ResponseEntity.ok("注册成功"); } catch (Exception e) { return ResponseEntity.badRequest().body("注册失败: " + e.getMessage()); } } // 3. 开始认证 @PostMapping("/authenticate/start") public ResponseEntity<Map<String, Object>> startAuthentication() { Challenge challenge = new DefaultChallenge(); PublicKeyCredentialRequestOptions options = new PublicKeyCredentialRequestOptions( challenge, 60000L, RP_ID, null, // allowCredentials 可以从数据库查 UserVerificationRequirement.PREFERRED, null ); Map<String, Object> json = objectConverter.getJsonConverter().writeValueAsMap(options); return ResponseEntity.ok(json); } // 4. 完成认证 @PostMapping("/authenticate/finish") public ResponseEntity<String> finishAuthentication(@RequestBody Map<String, Object> request) { try { PublicKeyCredential<AuthenticatorAssertionResponse, CollectedClientData> pkc = objectConverter.getJsonConverter().readValue(request, PublicKeyCredential.class); // 验证逻辑(实际要查数据库中的公钥) // 这里简化演示 return ResponseEntity.ok("认证成功"); } catch (Exception e) { return ResponseEntity.badRequest().body("认证失败: " + e.getMessage()); } } } 

Application.java

Java

@SpringBootApplication public class WebauthnDemoApplication { public static void main(String[] args) { SpringApplication.run(WebauthnDemoApplication.class, args); } } 

前端:index.html(单文件,连接后端)

HTML

<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>WebAuthn 前端 - Spring Boot Demo</title> <style> body { font-family: system-ui, sans-serif; padding: 20px; max-width: 800px; margin: auto; line-height: 1.6; } button { margin: 10px 0; padding: 12px 24px; font-size: 16px; cursor: pointer; background: #007bff; color: white; border: none; border-radius: 6px; } button:hover { background: #0056b3; } pre { background: #f8f9fa; padding: 16px; border-radius: 6px; overflow: auto; max-height: 500px; font-size: 14px; } #status { margin: 20px 0; font-weight: bold; font-size: 1.1em; color: #333; } .info { color: #555; margin-bottom: 20px; } </style> </head> <body> <h1>WebAuthn 测试(连接 Spring Boot 后端)</h1> <p>请确保后端运行在 http://localhost:8080,前端通过 http://localhost:5500 访问</p> <button>开始注册 Passkey</button> <button>开始无密码登录</button> <div>等待操作...</div> <pre></pre> <script type="module"> const status = document.getElementById('status'); const output = document.getElementById('output'); const API_BASE = 'http://localhost:8080/api/webauthn'; async function fetchJson(url, method = 'POST', body = null) { const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : null }); if (!res.ok) throw new Error(await res.text()); return res.json(); } async function register() { status.textContent = "正在请求注册选项..."; try { const optionsJSON = await fetchJson(`${API_BASE}/register/start`); const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJSON); status.textContent = "请按照浏览器提示创建 Passkey"; const credential = await navigator.credentials.create({ publicKey: options }); status.textContent = "正在提交注册结果..."; const responseJSON = credential.toJSON(); await fetchJson(`${API_BASE}/register/finish`, 'POST', responseJSON); status.textContent = "注册成功!"; output.textContent = JSON.stringify(responseJSON, null, 2); } catch (err) { status.textContent = "注册失败"; output.textContent = `${err.name}\n${err.message}`; console.error(err); } } async function login() { status.textContent = "正在请求认证选项..."; try { const optionsJSON = await fetchJson(`${API_BASE}/authenticate/start`); const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJSON); status.textContent = "请按照浏览器提示验证身份"; const assertion = await navigator.credentials.get({ publicKey: options }); status.textContent = "正在提交认证结果..."; const responseJSON = assertion.toJSON(); await fetchJson(`${API_BASE}/authenticate/finish`, 'POST', responseJSON); status.textContent = "登录成功!"; output.textContent = JSON.stringify(responseJSON, null, 2); } catch (err) { status.textContent = "登录失败"; output.textContent = `${err.name}\n${err.message}`; console.error(err); } } document.getElementById('registerStartBtn').addEventListener('click', register); document.getElementById('loginStartBtn').addEventListener('click', login); </script> </body> </html> 

运行步骤

  1. 把前端 index.html 放在 VS Code,用 Live Server 打开(端口 5500)
  2. 浏览器访问 http://localhost:5500/index.html
  3. 点击按钮 → 应该能正常完成 WebAuthn 流程(注册后凭证保存在内存中,认证时能验证)

启动 Spring Boot 后端(端口 8080)Bash

mvn spring-boot:run 

注意事项(重要!)

  • 当前后端实现非常简化(内存存储、单用户、无 session 管理 challenge、无完整验证逻辑)
  • 生产环境必须:
    • 使用 Redis/JWT/session 存储 challenge
    • 完整验证 origin、rpId、challenge
    • 用户体系(关联 userHandle)
    • HTTPS
    • 域名改为真实域名(RP ID 必须匹配)

后端什么时候知道登录是没问题的?

在 WebAuthn(包括你项目中使用的 webauthn4j 库)的完整认证流程里,后端(Relying Party,简称 RP / 服务器)在 finishAuthentication / finishAuthenticationResponse 这一步验证通过后,才真正知道“这次登录是没问题的”。

下面我用前后端分离的视角,详细说明后端什么时候、如何确定登录成功(结合你之前提供的代码骨架)。

认证流程回顾(重点标注后端确认成功的时机)

  1. 前端发起登录→ 调用/authenticate/start
    • 后端生成随机 challenge + options
    • 返回给前端(PublicKeyCredentialRequestOptionsJSON)
  2. 前端调用 navigator.credentials.get()
    • 浏览器弹出 PIN/指纹/面部/密码提示
    • 用户验证成功后,生成签名(assertion)
    • 前端拿到 PublicKeyCredential(包含 authenticatorData、signature、userHandle 等)
    • 前端把 .toJSON() 结果 POST 到 /authenticate/finish
  3. 后端收到 /authenticate/finish 请求(这是关键时刻)
    • 收到前端传来的 JSON(assertion response)
    • 后端执行核心验证(webauthn4j 帮你完成大部分)
      • challenge 是否匹配(防重放)
      • origin / RP ID 是否正确(防钓鱼)
      • authenticatorData 中的标志位是否符合要求(UP、UV)
      • 签名验证:用之前注册时存储的公钥验证 signature 是否有效
      • counter(签名计数器)是否递增(防克隆/重放)
      • userHandle 是否匹配预期用户(尤其是 discoverable credential)
    • 如果以上全部通过 → webauthn4j 的验证方法会返回成功(或不抛异常)
    • 此时后端才知道:这次认证是合法的,用户身份可信
    • 后端可以签发会话(JWT、cookie、session ID 等),完成登录

在代码中的体现(基于你之前的 Spring Boot 示例)

在 /authenticate/finish 接口里:

Java

@PostMapping("/authenticate/finish") public ResponseEntity<String> finishAuthentication(@RequestBody Map<String, Object> request) { try { // 1. 把前端 JSON 解析成 PublicKeyCredential PublicKeyCredential<AuthenticatorAssertionResponse, CollectedClientData> pkc = objectConverter.getJsonConverter().readValue(request, PublicKeyCredential.class); // 2. 核心验证(这里是后端真正确认“登录没问题”的地方) // 实际项目中需要传入正确的 ServerProperty(包含 challenge、origin、rpId) // 以及从数据库查到的对应 CredentialRecord(公钥、counter 等) AuthenticationData authenticationData = webAuthnManager.parseAuthenticationResponse( pkc, serverProperty, // challenge、origin、rpId credentialRecord, // 从 DB 查到的已注册凭证 null, // 扩展可选 false // 允许未验证用户句柄等 ); // 如果上面没抛异常 → 验证成功 // 更新 counter(防克隆重放) credentialRecord.setCounter(authenticationData.getAuthenticatorData().getSignCount()); // 保存更新后的 credential(如果用 DB) credentialRepository.save(credentialRecord); // ★★★ 这里后端确认登录成功 ★★★ // 可以生成 JWT 或设置 session String jwt = generateJwtForUser(credentialRecord.getUserHandle()); return ResponseEntity.ok("登录成功,token: " + jwt); } catch (DataConversionException | VerificationException e) { // 签名无效、challenge 不匹配、counter 异常、origin 不对 等 return ResponseEntity.badRequest().body("认证失败: " + e.getMessage()); } catch (Exception e) { return ResponseEntity.internalServerError().body("服务器错误"); } } 

关键结论

  • 后端不是在收到前端请求时就认为登录成功
  • 而是在 finishAuthentication 接口里,完成所有密码学验证(尤其是签名验证 + counter 检查)后,才确认“登录没问题”
  • 如果任何一项校验失败(签名不对、counter 没增长、origin 被篡改等),webauthn4j 会抛异常,后端返回失败,前端看到“登录失败”
  • 只有验证全部通过,后端才会签发会话 token 或设置登录状态

实际项目中常见的改进点

为什么重要怎么做(简要)
challenge 存储防重放攻击用 Redis / session / DB 临时存(TTL 2min)
ServerProperty包含当前 challenge、origin、rpId在 finish 时从 session 取,校验一致
查 Credential必须用注册时存的公钥验证签名根据 credentialId 或 userHandle 从 DB 查
更新 counter防止同一凭证被克隆使用验证成功后必须更新并持久化
签发 token完成登录状态返回 JWT / 设置 httpOnly cookie

如果你现在的代码里 /authenticate/finish 只是简单 return “认证成功”(没做完整验证),那其实后端还没真正确认安全,很容易被伪造请求绕过。

前后端交互数据的时候可以伪造吗?

可以伪造“数据包”发送给后端,但你无法伪造“有效的签名数据”。

在 WebAuthn 的流程中,前后端交互的数据确实可以被截获、查看,甚至有人可以手动模拟一个 POST 请求发送给 Spring Boot。但由于密码学签名的存在,后端能瞬间识别出这些数据是“真货”还是“伪造品”。

我们可以从以下三个层面拆解为什么“伪造”无法得逞:


1. 数据包的结构:看得见,改不动

当指纹识别成功后,前端发给后端的数据主要包含三部分:

  1. ClientDataJSON:包含 Challenge 和当前的域名(Origin)。
  2. AuthenticatorData:包含设备状态和签名计数器。
  3. Signature(签名):这是最关键的部分。

为什么不能伪造: 签名是硬件使用私钥对前两部分内容的哈希值进行加密的结果。如果你篡改了 ClientDataJSON 里的任何一个字节(比如想把域名从 fake.com 改成 real.com),后端使用公钥解密签名时,计算出的哈希值就对不上了。


2. Challenge(挑战值):无法“预制”数据包

黑客可能会想:我先录制一段你登录成功的完整数据包,下次直接发给后端。

  • 后端的防御: 后端在每次登录前给出的 Challenge 都是随机且唯一的(通常是 32 位以上的随机字节)。
  • 结果: 就像银行柜台问你“今天的暗号是什么?”,你必须用私钥对“今天的暗号”签名。黑客拿着“昨天的暗号”签名数据过来,后端会发现 Challenge 不匹配,直接丢弃。

3. 浏览器的“强制诚信”:拦截域名伪造

这是 WebAuthn 最特殊的地方。在前后端交互中,Origin(域名)不是由前端 JavaScript 定义的,而是由浏览器内核强制写入的。

  • 伪造场景: 黑客写了一个恶意脚本,试图在数据包里把 Origin 字段填成你的官网域名。
  • 浏览器的拦截: 浏览器在调用底层硬件 API 时,会核实当前真实的访问地址。如果黑客在 evil.com 上运行,浏览器传给硬件进行签名的域名永远只能是 evil.com
  • 后端的最后把关: 当后端收到数据,发现签名里锁定的域名是 evil.com 而不是自己的域名,校验失败。

4. 模拟请求(脚本攻击)为什么行不通?

如果黑客跳过前端页面,直接用 PostmanPython 脚本往你的 Spring Boot 接口发一段伪造的 JSON:

  1. 没有私钥: 脚本无法生成合法的 Signature
  2. 验签失败: Spring Boot 使用 java-webauthn-server 库调用 rp.finishAuthentication。该方法内部会执行以下逻辑:
    • 检查 Challenge 是否存在于 Session 中。
    • 使用数据库里的公钥对 Signature 进行 RSA/EC 验签。
    • 检查 Origin 是否合法。

只要其中一项不符,后端就不会给这个请求签发 JWT 或 Session。


总结:黑客唯一能做的

在前后端交互中,黑客唯一能做的就是**“观察”**。他能看到你发了什么,但他改不了,也无法在没有你设备硬件的情况下生成一份新的、能通过后端校验的数据。

这就好比黑客截获了你的一张“亲笔签名信”:

  • 他可以复印这张信(重放攻击),但因为信上的日期(Challenge)不对,收信人不认。
  • 他想改信的内容(篡改数据),但由于他没法模仿你的笔迹(私钥签名),收信人一眼就能看出签名被破坏了。

Read more

webdav-server 终极指南:轻量级WebDAV服务器完整教程

在现代数字化办公环境中,文件共享和远程访问已成为日常工作的重要需求。webdav-server作为一个轻量级WebDAV服务器实现,提供了简单而强大的文件共享解决方案。本文将为您全面解析webdav-server的核心功能、部署方法和实战应用技巧。 【免费下载链接】webdavSimple Go WebDAV server. 项目地址: https://gitcode.com/gh_mirrors/we/webdav 为什么选择webdav-server?核心价值解析 webdav-server是一个基于Go语言开发的独立WebDAV服务器,具有以下核心优势: 🚀 轻量高效:单二进制文件部署,资源占用极低 🔒 安全可靠:支持TLS加密传输和多种认证方式 📁 跨平台兼容:支持Windows、Linux、macOS等主流操作系统 👥 权限精细控制:可配置用户级权限和目录访问规则 与传统的FTP或Samba共享相比,WebDAV协议提供了更丰富的文件操作功能和更好的集成性,特别适合需要Web界面访问或与办公软件集成的场景。 3步快速部署webdav-server 步

WebPlotDigitizer:智能图表数据提取工具提升科研效率指南

WebPlotDigitizer:智能图表数据提取工具提升科研效率指南 【免费下载链接】WebPlotDigitizerWebPlotDigitizer: 一个基于 Web 的工具,用于从图形图像中提取数值数据,支持 XY、极地、三角图和地图。 项目地址: https://gitcode.com/gh_mirrors/we/WebPlotDigitizer 技术原理:智能数据解析的双层级架构 WebPlotDigitizer作为一款专业的图表数据提取工具,其核心优势在于创新性的双层级处理架构。这种架构将复杂的图像识别任务分解为"智能识别层"与"数据校准层",实现了从像素到数据的精准转换。 智能识别层:视觉语义理解的核心 智能识别层通过计算机视觉技术实现图表内容的深度理解。不同于传统的像素分析方法,该层能够识别图表的语义结构,包括坐标轴类型、数据系列分布和标签信息。这一过程主要由javascript/services/ai.js模块驱动,通过多维度特征提取实现图表类型的自动分类。 系统首先进行图像预处理,包括噪声过滤和对比度增强,为后续分析奠定基础。接着通过边缘检测算法识别

【Flask+VUE】flask+vue开发web网页系统(详细安装使用范例)

【Flask+VUE】flask+vue开发web网页系统(详细安装使用范例)

【Flask_VUE】flask+vue开发web网页系统(详细安装使用范例) * ✅ 一、项目结构规划 * ✅ 二、后端:Flask 搭建 API 服务 * 1. 安装 Flask 并创建后端项目 * 2. 编写 `app.py` * 3. 运行后端服务 * ✅ 三、前端:Vue 搭建用户界面 * 1. 创建 Vue 项目(需要 Node.js 和 npm) * 2. 安装 Axios(用于 HTTP 请求) * 3. 创建 API 服务文件 * 4. 创建用户列表组件(`src/views/

极客大挑战2025-web复现

极客大挑战2025-web复现

题解 1.one_last_image(php文件上传/ 短标签利用) 进来以后发现是个文件上传的题,然后就试着传一个php文件上去 发现里面给出了uploads的路径,访问。如果是空的php进去会发现什么都没有,为了绕过对常见的php标签以及命令执行函数的限制,我们用短标签。 <?=`env`; 或 <?=('sys'.'tem')('env'); 然后顺着操作即可。然后其他人说在phpinfo里面可以找到, 2.Vibe SEO(站点地图的使用/未关闭文件与文件描述符的读取) 看到这个题还是很蒙的,因为界面里什么都没有。然后了解了一下才知道站点地图是什么。 站点地图(sitemap.xml)是一个XML格式的文件,它列出了网站中所有重要的网页URL,并可以附带每个URL的额外信息(例如最后更新时间、更新频率、相对重要性等),主要作用是帮助搜索引擎更高效、全面地抓取和索引网站内容。 以下是它的核心要点:核心作用引导搜索引擎爬虫: