跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Java大前端java

无密码登录安全对比与 WebAuthn 技术实战

无密码登录相比传统密码方案在防钓鱼和凭证窃取上优势明显,WebAuthn 凭借公钥加密与设备绑定成为首选。通过对比三种登录方式的安全性,结合 Spring Boot 与 webauthn4j 库,详解从注册到认证的后端验证逻辑,重点剖析 Challenge 机制、签名校验及 Counter 防重放策略,并提供前后端交互的完整代码示例与生产环境落地建议。

蓝绿部署发布于 2026/4/7更新于 2026/9/559 浏览
无密码登录安全对比与 WebAuthn 技术实战

三种登录方式的安全性横向对比

在构建现代认证系统时,我们常面临选择:用户名 + 密码、用户名 + FingerprintJS(设备指纹) 以及 用户名 + WebAuthn(Passkey / FIDO2)。这三种模式虽然都基于'凭证'验证,但在真实攻击场景下的表现差异巨大。

下表以当前主流威胁模型为基准,从多个维度进行对比:

维度用户名 + 密码登录用户名 + FingerprintJS 无密码登录用户名 + WebAuthn 无密码登录(Passkey)
凭证窃取风险高(易受键盘记录器、钓鱼页面窃取)中低(需先访问站点,但 JS 可被窃取)极低(私钥永不离开设备,无法被窃取)
钓鱼(Phishing)抵抗力差(用户易向假站输入密码)差(指纹数据可在假站采集并重放)极强(RP ID / origin 绑定,假站 Challenge 无效)
中间人攻击(MitM)抵抗差(HTTPS 下仍可能因后端弱配置泄露)中(数据传输但未加密绑定)极强(公钥加密 + origin 校验)
凭证重放攻击高(密码可无限次使用)中(可加 nonce/时间戳缓解)极低(Challenge 每次不同 + Counter 防重放)
克隆 / 复制设备攻击高(知道密码即可在任何设备登录)中高(高级攻击者可伪造指纹)低(私钥绑定硬件,克隆极难)
XSS 攻击下凭证泄露高(密码输入框易被脚本读取)高(localStorage 中的 device-secret 易读)低(私钥不暴露在 JS 可访问存储中)
肩窥 / 键盘记录高(用户需手动输入)低(无需输入)低(仅需 PIN/生物识别验证)
用户体验(摩擦)中(需记忆复杂密码)低(几乎无缝,首次需绑定)低–中(首次注册稍繁琐,后续一键)
隐私影响低(仅存 Hash)高(指纹可跨站追踪,GDPR/CCPA 风险大)中(不收集额外指纹,设备信息有限泄露)
NIST / OWASP 推荐级别AAL1–AAL2AAL2(勉强)AAL3(最高,抗钓鱼 MFA)
场景化建议
  • 高价值目标(银行、金融、企业内部系统):必须采用 WebAuthn。这是唯一真正抗钓鱼的方案,符合 NIST AAL3 标准。
  • 中价值目标(电商、一般 SaaS):优先 WebAuthn,FingerprintJS 仅作为辅助风控手段,不建议单独作为唯一认证方式。
  • 低价值 / 高便利需求(论坛、内容站):可考虑 FingerprintJS 无密码 或传统 密码,成本较低但安全性明显弱于 WebAuthn。

为什么需要 WebAuthn?

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

作为 FIDO2 框架的核心组成部分(FIDO2 = WebAuthn + CTAP2),它通过以下机制解决传统密码痛点:

  • 私钥永不离开用户设备:服务器只存储公钥,即使数据库泄露也无法还原私钥。
  • Origin / RP ID 绑定:假网站无法使用你在真实站点注册的凭证,天然防钓鱼。
  • Challenge + 签名:每次认证使用随机挑战值,防止重放攻击。
  • Counter 机制:防止同一凭证被克隆使用。

前置环境要求

开发阶段和生产环境的配置有显著区别,务必注意:

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

WebAuthn 认证流程图

下图展示了包含所有技术细节的序列图,涵盖 Challenge 生成、浏览器 API 调用及后端具体验证点(Challenge 匹配、签名验证、Counter 检查等)。假设后端使用 webauthn4j 库。

详细前后端交互逻辑

代码实现参考

下面给出一个前后端分离的参考实现,后端使用 Spring Boot,前端为单文件 HTML 风格。项目结构如下:

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

后端:Spring Boot + webauthn4j

首先配置核心依赖 pom.xml。这里使用了 webauthn4j-core 和 webauthn4j-util,并预留了 Redis 依赖用于生产环境存储 Challenge。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>webauthn-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.4</version>
    </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>
        <!-- 生产环境建议使用 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>
    </dependencies>
</project>
核心配置类

我们需要定义 Relying Party Identity 和 Validator。注意开发环境 RP ID 设为 localhost,生产环境需替换为真实域名。

package com.example.webauthndemo.config;

import com.webauthn4j.WebAuthnManager;
import com.webauthn4j.converter.jackson.ObjectConverter;
import com.webauthn4j.data.RelyingPartyIdentity;
import com.webauthn4j.validator.WebAuthnValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@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();
    }
}
凭证存储仓库

开发测试阶段可用内存存储,但生产环境必须持久化到数据库。这里演示了一个简单的 InMemoryCredentialRepository。

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 org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class InMemoryCredentialRepository {
    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 实现

Controller 负责处理注册和认证的请求分发。注意 startRegistration 和 startAuthentication 生成的 Challenge 在生产环境中应存入 Session 或 Redis,以便后续验证。

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.validator.WebAuthnValidator;
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, UserVerificationRequirement.PREFERRED),
                null, null, null
        );
        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 {
            PublicKeyCredential<AuthenticatorAttestationResponse, CollectedClientData> pkc = 
                    objectConverter.getJsonConverter().readValue(request, PublicKeyCredential.class);
            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, 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);
            // 实际项目中需在此处执行完整的 ServerProperty 校验
            // 包括 Challenge 匹配、Origin 校验、Counter 检查等
            return ResponseEntity.ok("认证成功");
        } catch (Exception e) {
            return ResponseEntity.badRequest().body("认证失败:" + e.getMessage());
        }
    }
}

前端:index.html

前端主要调用 navigator.credentials.create 和 navigator.credentials.get。确保后端运行在 http://localhost:8080,前端通过 Live Server 运行在 http://localhost:5500。

<!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; }
    </style>
</head>
<body>
    <h1>WebAuthn 测试(连接 Spring Boot 后端)</h1>
    <p>请确保后端运行在 http://localhost:8080,前端通过 http://localhost:5500 访问</p>
    <button id="registerStartBtn">开始注册 Passkey</button>
    <button id="loginStartBtn">开始无密码登录</button>
    <div id="status">等待操作...</div>
    <pre id="output"></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. 启动 Spring Boot 后端(端口 8080):
    mvn spring-boot:run
    
  2. 将前端 index.html 放在 VS Code,用 Live Server 打开(端口 5500)。
  3. 浏览器访问 http://localhost:5500/index.html,点击按钮即可完成流程。
生产环境落地注意点

当前示例为了演示简化了逻辑(内存存储、单用户、无 session 管理 Challenge)。实际落地时必须完善以下几点:

  • Challenge 存储:生产环境必须使用 Redis/JWT/Session 存储 Challenge,设置 TTL(如 2 分钟),防止重放。
  • 完整验证逻辑:在 /authenticate/finish 接口中,必须传入正确的 ServerProperty(包含 Challenge、Origin、RP ID),并从数据库查到的 CredentialRecord 中提取公钥进行验签。
  • Counter 更新:验证成功后必须更新并持久化 Counter,防止凭证克隆。
  • 域名一致性:RP ID 必须与真实域名匹配,且前端 Origin 必须与后端校验一致。

后端何时确认登录成功?

在 WebAuthn 流程中,后端(Relying Party)只有在完成所有密码学验证后,才真正知道'这次登录是没问题的'。

关键验证时机
  1. 前端发起登录 → 调用 /authenticate/start
    • 后端生成随机 Challenge + Options,返回给前端。
  2. 前端调用 navigator.credentials.get()
    • 浏览器弹出 PIN/指纹/面部提示。
    • 用户验证成功后,生成签名(Assertion)。
    • 前端将 .toJSON() 结果 POST 到 /authenticate/finish。
  3. 后端收到 /authenticate/finish 请求(关键时刻)
    • 后端执行核心验证(webauthn4j 库协助完成):
      • Challenge 是否匹配:防止重放。
      • Origin / RP ID 是否正确:防止钓鱼。
      • AuthenticatorData 标志位:检查 UP(User Present)、UV(User Verified)是否符合预期。
      • 签名验证:用注册时存储的公钥验证 Signature 是否有效。
      • Counter 检查:防止同一凭证被克隆使用。
    • 如果以上全部通过,webauthn4j 不会抛异常。
    • 此时后端才知道:这次认证是合法的,用户身份可信。
    • 后端可以签发会话(JWT、Cookie、Session ID 等),完成登录。
代码层面的体现

在 /authenticate/finish 接口里,真正的验证逻辑如下:

@PostMapping("/authenticate/finish")
public ResponseEntity<String> finishAuthentication(@RequestBody Map<String, Object> request) {
    try {
        // 1. 解析前端传来的 JSON
        PublicKeyCredential<AuthenticatorAssertionResponse, CollectedClientData> pkc = 
                objectConverter.getJsonConverter().readValue(request, PublicKeyCredential.class);

        // 2. 核心验证(这里是后端真正确认'登录没问题'的地方)
        // 实际项目中需要传入正确的 ServerProperty(包含 challenge、origin、rpId)
        // 以及从数据库查到的对应 CredentialRecord(公钥、counter 等)
        AuthenticationData authenticationData = webAuthnManager.parseAuthenticationResponse(
                pkc, serverProperty, credentialRecord, null, false
        );

        // 如果上面没抛异常 → 验证成功
        // 更新 counter(防克隆重放)
        credentialRecord.setCounter(authenticationData.getAuthenticatorData().getSignCount());
        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 检查)后,才确认'登录没问题'。如果任何一项校验失败,webauthn4j 会抛异常,后端返回失败。

前后端交互数据可以被伪造吗?

黑客可以伪造'数据包'发送给后端,但无法伪造'有效的签名数据'。

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

前端发给后端的数据主要包含三部分:ClientDataJSON、AuthenticatorData、Signature。签名是硬件使用私钥对前两部分内容的哈希值加密的结果。如果你篡改了 ClientDataJSON 里的任何一个字节,后端使用公钥解密签名时,计算出的哈希值就对不上了。

2. Challenge:无法'预制'数据包

后端在每次登录前给出的 Challenge 都是随机且唯一的。黑客拿着'昨天的暗号'签名数据过来,后端会发现 Challenge 不匹配,直接丢弃。

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

Origin 不是由前端 JavaScript 定义的,而是由浏览器内核强制写入的。如果黑客在 evil.com 上运行,浏览器传给硬件进行签名的域名永远只能是 evil.com。后端收到数据发现签名里锁定的域名不是自己的域名,校验失败。

4. 模拟请求为什么行不通?

如果黑客跳过前端页面,直接用 Postman 发一段伪造的 JSON:

  • 没有私钥:脚本无法生成合法的 Signature。
  • 验签失败:后端内部会检查 Challenge 是否存在于 Session 中,并使用数据库里的公钥对 Signature 进行 RSA/EC 验签。
  • 只要其中一项不符,后端就不会给这个请求签发 JWT 或 Session。

总结:黑客唯一能做的就是'观察'。他能看到你发了什么,但他改不了,也无法在没有你设备硬件的情况下生成一份新的、能通过后端校验的数据。这就好比黑客截获了你的一张'亲笔签名信',他可以复印这张信(重放攻击),但因为信上的日期(Challenge)不对,收信人不认;他想改信的内容(篡改数据),但由于他没法模仿你的笔迹(私钥签名),收信人一眼就能看出签名被破坏了。

目录

  1. 三种登录方式的安全性横向对比
  2. 场景化建议
  3. 为什么需要 WebAuthn?
  4. 前置环境要求
  5. 代码实现参考
  6. 后端:Spring Boot + webauthn4j
  7. 核心配置类
  8. 凭证存储仓库
  9. Controller 实现
  10. 前端:index.html
  11. 运行步骤
  12. 生产环境落地注意点
  13. 后端何时确认登录成功?
  14. 关键验证时机
  15. 代码层面的体现
  16. 前后端交互数据可以被伪造吗?
  17. 1. 数据包结构:看得见,改不动
  18. 2. Challenge:无法“预制”数据包
  19. 3. 浏览器的“强制诚信”:拦截域名伪造
  20. 4. 模拟请求为什么行不通?

更多推荐文章

查看全部
  • 堆数据结构与字符串处理算法详解
  • 自然语言处理在法律领域的应用与实战
  • Faster Whisper v1.7 本地部署教程:支持 AMD 显卡与日文转中文
  • 论文阅读:FR-LLM 多任务大模型用于联合故障诊断与 RUL 预测
  • C语言结构体、共用体与链表基础
  • STL map/multimap 深度解析:接口使用与核心特性
  • OpenClaw 配置本地 Ollama 模型指南:Auth 设置与离线部署
  • ZeroClaw Gateway + LM Studio + Reflex 本地 AI 管理面板搭建
  • ControlNet-sd21 完全攻略:AI 绘画精准控制指南
  • 开源 AI 编程工具对比:OpenCode 与 GitHub Copilot 选型指南
  • Python 学习路径:从基础语法到机器学习实战指南
  • Windows 系统永久配置 Git 使用 SSH 连接 GitHub 教程
  • C++ Qt 摄像头视频采集实战:V4L2 与多线程
  • AI 在前后端开发中的差异化落地:从 MVP 到高并发
  • Pandas 数据可视化基础绘图教程
  • 知网 AIGC 检测原理与降低 AI 疑似度策略
  • IndexTTS-2-LLM WebUI 界面定制化修改步骤详解
  • CCF-GESP 2025 年 12 月四级 C++ 真题:优先购买
  • 利用AI提升鸿蒙开发效率:Rules与智能实践
  • OpenClaw 接入飞书机器人与 Kimi2.5 配置实战

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online