无密码登录的安全问题及采用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

PyTorch 2.6最新镜像:支持Python 3.13开箱即用

PyTorch 2.6最新镜像:支持Python 3.13开箱即用 你是不是也遇到过这样的情况:想写一篇关于PyTorch 2.6的深度评测文章,结果发现本地环境已经被各种项目“污染”得乱七八糟?不同版本的Python、混杂的依赖包、残留的缓存文件……这些都会严重影响测试结果的可复现性。作为技术作家,我们最怕的就是——今天跑通了,明天就报错;在这台机器上没问题,在另一台却处处是坑。 别担心,现在有一个简单又干净的解决方案:使用预置了PyTorch 2.6 + Python 3.13的纯净镜像环境。这个镜像不仅帮你省去繁琐的环境配置过程,还能确保你在完全一致、无干扰的系统中进行测试和写作,真正做到“一次运行,处处可信”。 本文将带你从零开始,一步步利用ZEEKLOG算力平台提供的最新镜像资源,快速搭建一个专为PyTorch 2.6评测设计的标准开发环境。无论你是刚接触AI开发的小白,还是需要稳定测试环境的技术写作者,都能轻松上手。我们会讲清楚这个镜像到底解决了什么问题、怎么一键部署、如何验证核心功能(比如torch.compile在Python

By Ne0inhk
在线浏览“秀人网合集”的新思路:30 行 Python 把封面图链接秒变本地可点图库

在线浏览“秀人网合集”的新思路:30 行 Python 把封面图链接秒变本地可点图库

用 30 行 Python 把秀人网公开合集“搬”进本地数据库 “秀人网”近日上线的新主题合集页采用前端渲染,数据通过 /api/v2/theme/list 接口一次性返回 JSON,无需模拟点击“加载更多”。接口无登录限制,但带 5 秒滑动窗口的 IP 频次校验:单 IP >30 次/分即返回 429。本文示范如何遵守 robots 协议、放缓速率,仅采集“公开可见”字段,并给出断点续抓、User-Agent 随机化、异常重试等常用技巧。 核心思路三步走: 分析列表接口:在浏览器 DevTools 里筛选 XHR,发现真实请求 URL

By Ne0inhk

Ubuntu玩转Python:从配置到实战全指南

好的,这是一份在 Ubuntu 环境下使用 Python 的完整指南: 在 Ubuntu 环境下玩转 Python:从环境配置到实战开发全指南 Ubuntu 是开发者喜爱的 Linux 发行版之一,与 Python 结合能提供强大且稳定的开发环境。本指南将带你完成从环境配置到实战开发的完整流程。 一、环境配置 1. 检查系统自带 Python * Ubuntu 通常预装了 Python。 * 查看输出,确认版本(如 Python 3.10.12)。python 命令可能指向 Python 2,建议始终使用 python3 和 pip3。 2. 安装 Python 开发工具包 3. 4. 使用虚拟环境(强烈推荐)

By Ne0inhk

股票分析:Python 爬取同花顺股票数据(技术指标提取)

Python 爬取同花顺股票数据及技术指标提取详解(2026 年视角) 在 2026 年,使用 Python 爬取股票数据已成为量化分析、AI 预测和个人投资工具的标配。同花顺(iFinD)作为国内主流金融平台,提供丰富的股票行情、历史 K 线和技术指标数据。但直接爬取其官网网页可能面临反爬机制、数据延迟或法律风险(需遵守平台条款,避免商业滥用)。推荐使用开源库如 Akshare 或 Tushare,这些库本质上是封装好的爬虫接口,支持同花顺等数据源,免费且高效。 本教程基于 2026 年最新实践: * 首选库:Akshare(免费开源,支持实时/历史数据,数据来源包括同花顺、东方财富等)。 * 备选:Tushare(需注册 Token,免费版有限额,付费版更稳定)。 * 技术指标提取:使用 pandas_ta

By Ne0inhk