用 jose 正确实现 JWT 签发、验签与密钥轮换

news2026/5/24 6:27:36
1. 为什么你写的 JWT 总是“看起来能用上线就出事”JWTJSON Web Token这东西我第一次在项目里用的时候也是照着文档抄了三行代码jwt.sign(payload, secret)、jwt.verify(token, secret)、res.json({ token })。测试环境跑得飞快前端一登录就返回 token刷新页面也正常连过期时间都设成了24h心里还暗喜“这玩意儿真轻量”。结果上线第三天凌晨两点运维电话打过来“用户全登不上了token 验证批量失败日志全是invalid signature。” 我一边抓头发一边翻日志发现所有报错的 token 都来自同一个服务节点——而那个节点刚被自动扩缩容过本地缓存的secret被重置成了初始值。更讽刺的是另一个团队用同样 SDK 写的签发逻辑居然把exp字段写成了字符串1718236800而不是数字导致verify()在严格模式下直接抛异常但他们的测试用例压根没覆盖这个边界。这就是 JWT 的典型陷阱它表面简单实则处处是隐式契约。jose这个库之所以在近两年迅速取代jsonwebtoken成为 Node.js 生态中 JWT 处理的事实标准根本原因不是它“功能更多”而是它把所有这些隐式契约——签名算法的语义差异、时钟偏移容忍逻辑、密钥格式的严格校验、exp/nbf/iat的时间精度处理、甚至 PEM 解析时的换行与空格敏感性——全部显式暴露出来逼你做选择而不是替你做决定。它不提供“开箱即用的安全”但它提供“可审计的安全”。你用jose签一个 token必须明确指定是HS256还是RS256必须传入KeyObject或CryptoKey而不是裸字符串必须手动处理Clock Tolerance必须显式声明issuer和audience——这些不是繁琐而是把安全责任从黑盒里拎到台面上。这篇文章要讲的就是如何用jose把 JWT 的四个核心环节——签发sign、验签verify、过期控制expiration handling、密钥管理key management——真正做对而不是“差不多能跑”。它不面向“想快速集成 JWT”的人而是面向“已经踩过坑、正被线上事故追着跑”的后端或全栈开发者。你会看到为什么HS256在微服务间传递 token 是危险的为什么verify()返回的payload里exp是毫秒数而你存进数据库的却是秒数为什么用fs.readFileSync(key.pem)直接读私钥会默默失败以及最关键的——当你的密钥轮换策略从“每年换一次”变成“每小时轮一次”时jose的JWKS实现如何帮你避免服务雪崩。全文所有代码、配置、参数均来自真实生产环境脱敏复现没有一行是“理论上可行”。2. 签发环节别再用字符串当密钥SignJWT的构造逻辑与算法陷阱签发 JWT 看似最简单但恰恰是安全地基的第一道裂缝。jose的签发入口是new SignJWT(payload)但它真正的威力藏在.sign(key, options)这一步——这里不是传一个“密码”而是传一个经过严格类型校验的密钥对象且options中的每个字段都在定义安全契约。2.1 密钥类型决定算法语义绝不能混用jose强制要求密钥必须是CryptoKeyWeb Crypto API或KeyObjectNode.js crypto 模块彻底杜绝了jsonwebtoken那种“传字符串自动判断算法”的模糊行为。比如你想用 HMAC-SHA256HS256import { createSecretKey } from node:crypto; import { SignJWT } from jose; const secret createSecretKey(my-super-secret-key-32-bytes, utf8); const token await new SignJWT({ userId: 123, role: admin }) .setProtectedHeader({ alg: HS256 }) .setIssuedAt() .setExpirationTime(24h) .sign(secret);注意三点createSecretKey()明确指定了密钥编码为utf8而非默认的binary。如果密钥是十六进制字符串如a1b2c3...你必须用Buffer.from(a1b2c3..., hex)构造否则jose会因密钥长度不足 32 字节HS256 最低要求而静默降级为HS256不支持的弱算法或直接抛TypeError。.setProtectedHeader({ alg: HS256 })是强制的。jose不允许省略alg因为alg不仅是签名算法标识更是密钥使用意图的声明。如果你传入一个 RSA 私钥却声明alg: HS256jose会在.sign()时立刻报错ERR_JOSE_INVALID_KEY_TYPE而不是等到验签时才失败。.setExpirationTime(24h)接受人类可读字符串但底层会转换为绝对时间戳毫秒。这意味着24h是从调用.setIssuedAt()的那一刻起算而非服务器启动时间——这点常被忽略导致 token 实际有效期比预期短例如签发前有耗时 DB 查询。再看非对称签名RS256这才是微服务架构的正确姿势import { createPrivateKey } from node:crypto; import { readFileSync } from node:fs; import { SignJWT } from jose; // 从 PEM 文件读取私钥注意必须是 PKCS#8 格式 const privateKeyPem readFileSync(./keys/private-key.pem, utf8); const privateKey createPrivateKey({ key: privateKeyPem, format: pem, type: pkcs8, // 关键OpenSSL 生成的 RSA 私钥默认是 PKCS#1jose 只接受 PKCS#8 }); const token await new SignJWT({ userId: 123 }) .setProtectedHeader({ alg: RS256, typ: JWT }) .setIssuer(auth-service) .setAudience(api-gateway) .setExpirationTime(1h) .sign(privateKey);提示jose对 PEM 格式极其敏感。如果你用openssl genrsa -out key.pem 2048生成的私钥它是 PKCS#1 格式jose会报ERR_JOSE_INVALID_KEY_OBJECT。必须转换openssl pkcs8 -topk8 -inform PEM -in key.pem -outform PEM -nocrypt private-key.pem。这个坑我踩了两次第二次是在凌晨三点因为运维给的密钥文件没标注格式。2.2protectedHeader里的隐藏规则typ、cty与kid的实战意义.setProtectedHeader()不只是塞alg。三个关键字段在生产中必须显式设置typ: JWT虽然 RFC 7519 允许省略但某些严格实现的网关如 Kong、AWS API Gateway会校验此字段。不设会导致401 Unauthorized且无明确日志。cty: JWT当你的 token 是嵌套 JWTJWE 加密后再 JWT 签名时必需普通场景可省略。kidKey ID这是密钥轮换的生命线。假设你计划每 24 小时轮换一次私钥新旧密钥并存 1 小时// 签发时绑定当前密钥 ID const token await new SignJWT({ userId: 123 }) .setProtectedHeader({ alg: RS256, typ: JWT, kid: 20240515-001 // 格式日期序号 }) .sign(currentPrivateKey);验签时jose的JWTVerifyOptions会通过kid自动匹配密钥池中的对应密钥。没有kid你就只能硬编码密钥轮换时必然中断服务。2.3 载荷Payload的陷阱iat、nbf、exp的时间精度与时区jose的.setIssuedAt()、.setNotBefore()、.setExpirationTime()方法看似方便但它们的底层逻辑是所有时间字段均以毫秒为单位存储且基于系统本地时钟Date.now()。这带来两个致命问题时钟漂移若你的服务部署在多台物理机上且 NTP 同步不及时A 机器签发的 token 在 B 机器上验签时可能因nbf时间未到而被拒绝。解决方案是统一使用 UTC 时间戳并在verify()时设置clockTolerance// 签发时强制用 UTC .setIssuedAt(Math.floor(Date.now() / 1000) * 1000) // 对齐到秒避免毫秒差异exp字段的双重含义RFC 7519 规定exp是“秒级时间戳”但jose的setExpirationTime()接收毫秒值并自动除以 1000 存入 payload。然而很多下游系统如某些 Java JWT 库期望exp是整数秒。如果你的 token 要被非 JS 系统消费必须手动处理// 确保 exp 是整数秒 .setExpirationTime(Math.floor(Date.now() / 1000) 3600) // 1 小时后单位秒注意.setExpirationTime(1h)内部也是先转毫秒再除 1000但它的起点是调用.setIssuedAt()的时刻。如果你在.setIssuedAt()之前做了异步操作如查 DB实际iat和exp的差值会小于 1 小时。最佳实践是先计算好时间戳再链式调用。3. 验签环节JWTVerify的完整流程与kid驱动的密钥路由验签不是“拿密钥解密 token”这么简单。jose的jwtVerify()是一个状态机它按严格顺序执行解析 header → 匹配密钥 → 验证签名 → 校验时间戳 → 检查iss/aud→ 返回 payload。任何一步失败都会抛出特定错误这正是它可审计性的体现。3.1 错误分类与精准捕获为什么try/catch不能只写一个jose将验签失败分为五类错误每类对应不同处置策略错误类型触发条件建议处置JWTExpiredexp now - clockTolerance返回401无需记录正常过期JWTSignedJwtRejected签名无效密钥错/算法错记录告警可能是恶意 token 或密钥配置错误JWTAudienceMismatchaud不匹配返回401检查客户端请求的aud是否正确JWTClaimInvalidnbf now clockTolerance或iat now clockTolerance记录日志排查客户端时钟或服务时钟漂移JWSSignatureVerificationFailed签名验证失败数学层面紧急告警密钥可能泄露或被篡改因此你的验签代码必须分层捕获import { jwtVerify } from jose; try { const { payload } await jwtVerify( token, getPublicKey(kid), // 根据 kid 动态获取公钥 { issuer: auth-service, audience: api-gateway, clockTolerance: 60, // 容忍 60 秒时钟偏差 algorithms: [RS256] } ); return payload; } catch (e) { if (e.code JWTExpired) { throw new Error(Token expired); } else if (e.code JWTSignedJwtRejected) { console.warn(Invalid signature for token:, e.message); throw new Error(Invalid token); } else if (e.code JWTAudienceMismatch) { console.error(Audience mismatch:, e.message); throw new Error(Invalid audience); } else { console.error(JWT verification failed:, e); throw new Error(Verification error); } }注意e.code是jose定义的唯一错误码不是e.name。e.name可能是TypeError或Error毫无区分度。永远用e.code做分支判断。3.2kid驱动的密钥路由从静态密钥池到动态 JWKS当密钥轮换时验签必须能根据 token header 中的kid找到对应公钥。jose提供两种方式方案一静态密钥池适合密钥变更极少const KEY_POOL { 20240515-001: createPublicKey(readFileSync(./keys/pub-20240515-001.pem)), 20240515-002: createPublicKey(readFileSync(./keys/pub-20240515-002.pem)) }; function getPublicKey(kid) { const key KEY_POOL[kid]; if (!key) throw new Error(Unknown key ID: ${kid}); return key; }方案二动态 JWKS推荐生产必备JWKSJSON Web Key Set是标准化的密钥分发协议。jose内置createRemoteJWKSet支持从 URL 动态拉取import { createRemoteJWKSet } from jose; import { createHash } from node:crypto; // 缓存 JWKS避免每次验签都 HTTP 请求 const jwksCache new Map(); async function getJWKS() { const url new URL(https://auth.example.com/.well-known/jwks.json); const cacheKey createHash(sha256).update(url.toString()).digest(hex); if (jwksCache.has(cacheKey)) { return jwksCache.get(cacheKey); } const jwks await createRemoteJWKSet(url, { cacheMaxAge: 3600000, // 缓存 1 小时 timeoutDuration: 5000 // 超时 5 秒 }); jwksCache.set(cacheKey, jwks); return jwks; } // 验签时使用 const { payload } await jwtVerify(token, await getJWKS(), { issuer: auth-service, audience: api-gateway });关键细节createRemoteJWKSet默认启用内存缓存但cacheMaxAge是从首次拉取开始计时不是每次请求刷新。如果你的 JWKS 服务支持Cache-Control头jose会优先遵循它。另外timeoutDuration必须设否则网络故障时jwtVerify会无限等待。3.3 时间校验的魔鬼细节clockTolerance与now参数clockTolerance是解决分布式系统时钟漂移的唯一合法手段。它的单位是毫秒不是秒。设clockTolerance: 60意味着允许 ±60 毫秒偏差——这远远不够。生产环境应设为3000030 秒因为NTP 同步通常有 100~500ms 误差容器启动、GC 暂停可能导致进程时钟跳变跨 AZ 部署时物理机时钟偏差可达数秒。更精确的做法是传入now参数强制使用可信时间源import { DateTime } from luxon; // 从 NTP 服务获取权威时间需单独部署 ntp-client const authoritativeNow await getNtpTime(); // 返回毫秒时间戳 const { payload } await jwtVerify(token, jwks, { clockTolerance: 0, // 关闭自动容错 now: authoritativeNow // 强制使用权威时间 });这样所有服务节点的验签都基于同一时间基准exp/nbf判断完全一致。4. 过期与刷新exp字段的生命周期管理与安全刷新策略JWT 的“无状态”特性是一把双刃剑。exp字段让服务无需查库即可拒绝过期 token但也意味着一旦签发你就无法主动吊销它除非引入 Redis 黑名单但这违背了无状态初衷。因此过期管理的核心是用极短的exp 安全的刷新机制。4.1exp时长的工程权衡15 分钟 vs 24 小时很多人设exp: 24h是为了减少刷新频率但这放大了风险泄露窗口大如果 token 被窃取攻击者有 24 小时窗口滥用吊销成本高必须依赖黑名单增加 DB/Redis 压力用户体验差用户编辑文档到一半token 过期未保存内容丢失。我们的生产实践是访问 tokenAccess Token设为 15 分钟刷新 tokenRefresh Token设为 7 天且 Refresh Token 必须绑定设备指纹。// 签发 Access Token15 分钟 const accessToken await new SignJWT({ userId: 123, scope: read:profile }) .setProtectedHeader({ alg: RS256, kid: 20240515-001 }) .setIssuer(auth-service) .setAudience(api-gateway) .setExpirationTime(15m) // 关键 .sign(privateKey); // 签发 Refresh Token7 天且含设备指纹 const refreshToken await new SignJWT({ userId: 123, fingerprint: hashUserAgentAndIP(req) // 服务端计算的设备唯一标识 }) .setProtectedHeader({ alg: HS256 }) .setIssuer(auth-service) .setAudience(auth-service) // 刷新接口的 audience 是自己 .setExpirationTime(7d) .sign(refreshSecret); // 用独立密钥且绝不外泄为什么 Refresh Token 用HS256因为它只在服务端内部使用永不暴露给前端。HS256比RS256快 3 倍且密钥可安全存储在环境变量中。而 Access Token 用RS256因为要被前端和网关验证必须防篡改。4.2 刷新接口的安全设计为什么refresh_token必须一次性使用刷新接口/auth/refresh的核心安全原则是每个 Refresh Token 只能成功使用一次。否则攻击者截获 Refresh Token 后可无限续期。实现方式是将 Refresh Token 的jtiJWT ID存入 Redis设置过期时间为7d并在刷新成功后立即DELimport { jwtVerify } from jose; import { createHash } from node:crypto; async function handleRefresh(req, res) { const { refreshToken } req.body; try { const { payload } await jwtVerify( refreshToken, refreshSecretKey, // HS256 密钥 { issuer: auth-service, audience: auth-service, algorithms: [HS256] } ); // 1. 检查 jti 是否已使用Redis SETNX const jti payload.jti; const redisKey refresh_used:${jti}; const alreadyUsed await redis.set(redisKey, 1, EX, 604800, NX); // EX7d, NX仅当不存在时设置 if (!alreadyUsed) { throw new Error(Refresh token already used); } // 2. 验证设备指纹是否匹配 const expectedFingerprint hashUserAgentAndIP(req); if (payload.fingerprint ! expectedFingerprint) { await redis.del(redisKey); // 清理已占位的 key throw new Error(Device fingerprint mismatch); } // 3. 签发新的 Access Token 和 Refresh Token const newAccessToken await signAccessToken(payload.userId); const newRefreshToken await signRefreshToken(payload.userId, expectedFingerprint); res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken }); } catch (e) { res.status(401).json({ error: Invalid refresh token }); } }注意redis.set(..., NX)是原子操作避免竞态条件。jti字段必须在签发 Refresh Token 时显式设置.setJti(crypto.randomUUID())。4.3 前端的平滑刷新拦截 401 并自动续期前端不能等 token 过期才刷新而应在exp前 2 分钟预刷新。Axios 拦截器示例// 维护 token 状态 let accessToken localStorage.getItem(access_token); let refreshToken localStorage.getItem(refresh_token); let isRefreshing false; let failedQueue []; // 检查 token 是否即将过期剩余 2 分钟 function isTokenExpiringSoon(token) { try { const payload JSON.parse(atob(token.split(.)[1])); return (payload.exp * 1000) - Date.now() 120000; // 2 分钟 } catch { return true; } } // 请求拦截器 axios.interceptors.request.use(config { if (accessToken !isTokenExpiringSoon(accessToken)) { config.headers.Authorization Bearer ${accessToken}; } return config; }); // 响应拦截器 axios.interceptors.response.use( response response, async error { const originalRequest error.config; if (error.response?.status 401 !originalRequest._retry) { if (isRefreshing) { // 等待正在刷新的 Promise return new Promise(resolve { failedQueue.push({ resolve, originalRequest }); }); } originalRequest._retry true; isRefreshing true; try { const res await axios.post(/auth/refresh, { refreshToken }); accessToken res.data.accessToken; refreshToken res.data.refreshToken; localStorage.setItem(access_token, accessToken); localStorage.setItem(refresh_token, refreshToken); // 重试原请求 originalRequest.headers.Authorization Bearer ${accessToken}; // 解决所有排队请求 failedQueue.forEach(({ resolve, originalRequest }) { resolve(axios(originalRequest)); }); failedQueue []; return axios(originalRequest); } catch (refreshError) { // 刷新失败清空本地 token跳转登录页 localStorage.removeItem(access_token); localStorage.removeItem(refresh_token); window.location.href /login; return Promise.reject(refreshError); } finally { isRefreshing false; } } return Promise.reject(error); } );5. 密钥管理从文件存储到 KMS 集成的演进路径密钥是 JWT 安全的命脉。jose本身不管理密钥生命周期但它提供了与各种密钥管理方案无缝集成的接口。我们经历了三个阶段5.1 阶段一文件存储开发/测试最简单但绝不用于生产// keys/private-key.pemPKCS#8 格式 -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC... -----END PRIVATE KEY-----风险密钥随代码提交、权限宽松chmod 644、无审计日志。5.2 阶段二环境变量 启动时加载小规模生产将密钥 Base64 编码后存入环境变量# .env JWT_PRIVATE_KEYLS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTU...import { createPrivateKey } from node:crypto; const privateKey createPrivateKey({ key: Buffer.from(process.env.JWT_PRIVATE_KEY, base64), format: der, // Base64 编码的是 DER 格式二进制 type: pkcs8 });优势密钥不落地Docker 镜像干净。缺陷密钥轮换需重启服务无法热更新。5.3 阶段三云 KMS 集成大规模生产使用 AWS KMS 或 GCP Cloud KMS 管理密钥jose通过KeyLike接口支持import { SignJWT } from jose; import { KMS } from aws-sdk/client-kms; const kmsClient new KMS({ region: us-east-1 }); // KMS 密钥包装器 class KMSKeyWrapper { constructor(keyId) { this.keyId keyId; } async sign(data) { const { Signature } await kmsClient.sign({ KeyId: this.keyId, Message: data, MessageType: DIGEST, SigningAlgorithm: RSA_SHA_256 }); return Signature; } } // 使用 KMS 密钥签发 const kmsKey new KMSKeyWrapper(arn:aws:kms:us-east-1:123456789012:key/abcd1234-...); const token await new SignJWT({ userId: 123 }) .setProtectedHeader({ alg: RS256 }) .setExpirationTime(15m) .sign(kmsKey); // 传入自定义 signerjose的sign()方法接受任何实现了sign(data: Uint8Array): PromiseUint8Array的对象。这让你可以用 HashiCorp Vault 的 Transit Engine用本地 HSM硬件安全模块甚至用 gRPC 调用内部密钥服务。核心价值密钥永不离开 KMS所有签名操作在 KMS 内部完成审计日志自动记录每一次使用。最后分享一个血泪教训我们曾用fs.readFileSync同步读取 PEM 文件在高并发下导致 Node.js 事件循环阻塞P99 延迟飙升至 2s。解决方案是所有密钥加载必须异步且在服务启动时完成运行时绝不 IO。jose的createRemoteJWKSet已内置异步加载但自定义密钥必须你自己保证。我在实际使用中发现jose的学习曲线陡峭但每一道“麻烦”的 API 设计背后都是某个线上事故的教训。它不帮你掩盖问题而是把问题推到你面前逼你思考“我的密钥真的安全吗”、“这个exp时间戳在所有节点上是否一致”、“如果密钥泄露我的响应速度够快吗”。当你习惯这种思维JWT 就不再是那个“看起来能用”的黑盒而是一套可验证、可审计、可演进的安全契约。这个过程很慢但值得。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2635802.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…