十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

pyjwt实战全解:JWT签发、校验、算法选型与安全避坑

pyjwt实战全解:JWT签发、校验、算法选型与安全避坑 1. 项目概述与核心需求解析1.1 JWT到底解决什么问题JWTJSON Web Token本身不是一个库而是一种开放标准RFC 7519用来在各方之间安全地传递声明claims。它最常见的场景就是身份认证用户登录成功后服务端签发一个 token 给客户端客户端后续请求带上这个 token服务端验签通过后就知道“你是谁”。整个过程不需要在服务端存储 session天然适合分布式部署和跨域场景这也是它比传统 session 方案更受欢迎的根本原因。pyjwt 是 Python 生态里最主流的 JWT 编解码库没有之一。它把复杂的签名、校验、过期判断全部封装成一行函数调用让开发者不需要深究加密原理也能安全地使用 JWT。但有句说句pyjwt 的易用性某种程度上也成了双刃剑——因为 API 太简单很多人抄了文档示例就上线把过期时间忘了配或者选了个不合适的签名算法导致线上出安全事故。这篇博客我会把 pyjwt 的完整用法、底层逻辑和安全陷阱从头到尾捋一遍。1.2 为什么选 pyjwt 而不是别的库Python 生态里 JWT 库其实不止 pyjwt 一个还有 python-jose、authlib、itsdangerous 等等。我为什么一直推荐 pyjwt三个原因。第一它轻量且专注。pyjwt 只做 JWT 的编码、解码、验签这三件事没有多余的功能负担。而 python-jose 这类库还集成了 JWE加密、JWK密钥管理对大多数项目来说属于过度设计而且依赖链更长。第二它是社区维护最活跃的库之一对最新的 PyJWT 版本跟进快安全补丁发布及时。第三它的 API 设计稳定从 1.x 到 2.x 的迁移路径清晰老项目升级成本低。我见过有些团队自己造轮子写 JWT 编解码用 base64 把 JSON 编码一下拼个签名就完事结果签名算法实现出错或者 payload 里混入了敏感信息。JWT 本身的设计就考虑了防篡改但前提是你用了正确的签名算法这条容错率很低自己实现几乎必然踩坑。所以我的建议是老老实实用 pyjwt它的封装没有黑魔法文档也清楚出事了好排查。1.3 适用场景与读者画像这篇内容适合谁看第一类是刚接触 JWT 的 Python 后端开发者需要快速在 FastAPI、Flask、Django 里接入 token 认证第二类是前端小伙伴后面要写 SPA 项目需要理解 token 在后端是怎么签发、怎么校验的第三类是已经用上 pyjwt 但想排查线上坑的人比如 token 突然失效、过期时间不生效、算法报错等。不管你是哪种情况核心要搞清楚的几个问题都是一样的pyjwt 怎么签发 token 和校验 tokenHS256 和 RS256 到底选哪个区别是什么token 过期时间怎么设置续签方案怎么设计网上常说的 JWT 漏洞具体指哪些、怎么避免下面我按照“先会用、再懂原理、最后避坑”的顺序逐个拆开讲。2. 环境准备与 pyjwt 基础用法2.1 安装与版本选择pyjwt 的安装非常简单一行命令pip install pyjwt这里有个很容易忽略的细节如果你需要用到 RSA 非对称加密算法RS256 等必须额外安装 cryptography 依赖pyjwt 默认不会帮你装。建议直接从第一步就装上pip install pyjwt[crypto]这个写法会拉取 pyjwt 加上 cryptography 库。如果你的项目里本来就用到了 requests、httpx 之类的 HTTP 客户端它们也可能间接依赖 cryptography所以很多时候你已经装了只是没意识到。但我建议不要赌这个显式声明依赖是工程素养。检查一下版本python -c import jwt; print(jwt.__version__)目前 pyjwt 2.x 是主流版本2.8 以上的版本对 Python 3.8 到 3.12 支持都很好。如果你看到网上老教程里写的是import jwt不用怀疑pyjwt 的模块名就叫jwt不是pyjwt这个命名坑过不少人。如果你装的是 PyJWT 1.x语法上有一些差异比如jwt.decode的默认行为建议升级到 2.x别在旧版本上纠结。2.2 编码生成你的第一个 token直接从代码入手。生成一个最简单的 HS256 签名的 JWTimport jwt import time # 定义密钥生产环境中必须从配置中心或环境变量读取 secret_key your-secret-key-change-me # 构造 payload也就是 token 里携带的信息 payload { user_id: 1024, username: zhangsan, role: admin, # exp 是 reserved claimpyjwt 会自动校验 exp: int(time.time()) 3600 } # 签发 token token jwt.encode(payload, secret_key, algorithmHS256) print(token)就这么几行一个 JWT 就出来了。拆开看它的结构打印出来是长这样的字符串eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMDI0LCJ1c2VybmFtZSI6InpoYW5nc2FuIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzEwMDAwMDAwfQ.xxxxxxxx.号把 token 分成三段Header、Payload、Signature。第一段 Header 是{alg:HS256,typ:JWT}第二段 Payload 就是你的user_id、username、exp这些声明第三段是签名。签名的计算过程是把前两段分别做 Base64URL 编码再用.连接然后用 HMAC-SHA256 加上secret_key对这段字符串做哈希结果再做 Base64URL 编码。整个过程的意图很明确——保证 token 内容在传输过程中不被篡改只要有人改了 Payload 里的任何字段验签就会失败因为你的密钥他不知道。2.3 解码与校验验签、过期、防御有编码就有解码。pyjwt 的解码函数比编码要复杂一点因为它承担了校验职责import jwt token 上面生成的token secret_key your-secret-key-change-me try: decoded jwt.decode( token, secret_key, algorithms[HS256] ) print(decoded) except jwt.ExpiredSignatureError: print(token已过期) except jwt.InvalidTokenError as e: print(f无效token: {e})decode方法做了三件事验签、校验过期时间、返回 Payload 内容。任何一个环节出错都会抛异常。这里我要敲黑板强调一个关键点algorithms[HS256]这个参数必须显式指定。在 pyjwt 2.x 里它已经是必填参数了1.x 时代不传会默认用 token 头里的算法这导致过一个著名的安全漏洞——攻击者把 token 的alg改成none直接绕过验签。pyjwt 在新版本里已经禁用了none算法但你仍然不能省略algorithms参数的指定这是安全底线别偷懒。2.4 Payload 中的注册声明Registered ClaimsJWT 标准定义了几个保留的声明名称pyjwt 对其中一部分会自动校验。我日常最常用的三个expExpiration Time过期时间必须是 UNIX 时间戳int 或 float。pyjwt 会自动检查当前时间是否超过这个值超了就抛ExpiredSignatureError。iatIssued At签发时间表示 token 是什么时候生成的。subSubject主题一般放用户 ID 或唯一的用户标识。还有一个nbfNot Before也很实用表示在这个时间之前 token 不可用。它的语义和exp正好相反通常用来做“延迟生效”的场景比较少用但知道没坏处。注意exp是 any 类型的话你传int(time.time()) 3600就行不需要传 datetime 对象。有些老教程教人传 datetime也能用但要小心时区问题——pyjwt 会把它转成可比较的时间对象传 int 最省心。另外如果你的 token 要跨系统传递接收方不一定用的是 pyjwt那时候时间戳格式的兼容性最好所以我建议一律用 int 时间戳。3. 核心细节解析算法选型与安全边界3.1 HS256、RS256、ES256到底怎么选这是我在技术社群里被问到最多的问题也是 JWT 里最容易踩坑的设计决策。三种算法的区别用一个表格说清楚算法类型密钥形式签名方验签方适用场景HS256对称加密同一个密钥双方共用双方共用前后端单一服务、无第三方RS256非对称加密私钥签名公钥验签拥有私钥的服务拥有公钥的任何服务微服务集群、开放 APIES256非对称加密椭圆曲线私钥签名公钥验签拥有私钥的服务拥有公钥的任何服务对 token 长度有要求的场景HS256 的优点是简单一个密钥走天下适合你的服务是单体的、token 只在你的服务和客户端之间流转的场合。但它的安全性完全系于那把密钥密钥一旦泄露攻击者可以随意伪造 token而且因为是对称的任何拥有密钥的节点都同时具备签名和验签能力风险面比较大。RS256 的优点是私钥永远只保存在认证服务一个地方其他业务服务拿到公钥只能验签、不能签出新 token。这样即使某个业务服务被攻破攻击者也没法伪造 token 去访问别的服务。微服务架构里我强烈推荐 RS256。代价是生成密钥对麻烦一点、验签性能比 HS256 差一些但现在的机器性能完全扛得住。ES256 和 RS256 的原理相同只是用了椭圆曲线算法签名短、速度快。唯一的问题是生态兼容性在某些语言里没有 RSA 那么成熟非必要不选它。我个人的实践经验是单体应用用 HS256 省事微服务或 API 要开放给第三方用 RS256不要混用更不要在同一个项目里支持两种算法。3.2 密钥管理比选算法更重要的事算法选得再好密钥泄露等于白搭。密钥管理的几条原则我踩过坑之后总结的密钥长度不是越长越好而是要够且随机。HS256 的密钥至少有 32 字节256 位的随机性我用类似secrets.token_urlsafe(64)这样的方式生成而不是随便敲一段英文字符。密钥不能硬编码在代码里。这是很多新手项目犯的错代码 push 到 GitHub 仓库就等于把认证系统的钥匙公开了。应该放到环境变量、配置中心或密钥管理服务Vault、云厂商 KMS部署时注入。密钥要支持轮换。你可以维护一个kidKey ID字段签发 token 时把当前密钥的版本 ID 放进 Header验签时根据kid找到对应密钥去验。这样等到切换新密钥时旧 token 还能在老密钥有效期内正常使用等所有客户端都换上新 token 后再下线旧密钥整个过程客户端无感知。网上流传一个说法叫“密钥轮换是 JWT 设计上最难的部分”我认为一半对一半不对。难确实难但 pyjwt 对kid有成熟的实现思路关键是你要在系统设计阶段就考虑这个能力而不是等出事了再补。3.3 网上流传的 JWT 漏洞哪些是真的“JWT 漏洞总结”在热搜词里出现不是偶然因为这类漏洞确实发生过。最有名的几个我都过一遍第一是算法混淆攻击。攻击者把 RS256 的 token 改为 HS256然后用公钥作为 HMAC 的密钥去签名如果服务端验签时没有固定algorithms参数、而是读取 token 头里的alg来决定算法就会被骗过去。pyjwt 2.x 强制要求显式传algorithms就是为了堵这个洞。第二是none算法绕过。把 Header 的alg改成none再删掉签名某些早期版本的库会直接放过。这个在现代版本的 pyjwt 里已经魔高一尺道高一丈了直接抛异常。第三是密钥暴力破解。HS256 的密钥如果太弱比如secret这种攻击者在拿到一个有效 token 后可离线用字典爆破密钥。网上有些开源工具专门干这个。防法是密钥用 32 字节以上的随机数没有捷径。第四是 payload 不加密带来的信息泄露。很多人把手机号、邮箱、密码放在 payload 里忘了 JWT 的 Payload 只是 Base64URL 编码根本不是加密任何人拿到 token 都能解码看到内容。真要放敏感信息必须用 JWE加密 JWT或者干脆别放我通常只放user_id和role所有需要敏感信息的接口服务端再查库。3.4 常见安全配置实测对照表配置项推荐做法不推荐做法风险说明签名算法显式指定 algorithms[HS256]不传或信任 token 头算法混淆攻击过期时间显式设置 exp不设 exptoken 永久有效payload 内容只放必要信息放密码、手机号等信息泄露不是加密密钥强度32字节以上随机值短字符串、常见单词离线爆破时间容忍度不设置 leeway 或尽量小设置过大 window过期 token 滥用这张表对应到代码上就是一个安全的 decode 调用长这样decoded jwt.decode( token, public_key, algorithms[RS256], leeway10, # 只允许 10 秒的时钟偏移 options{ require: [exp], # 强制要求 token 必须带 exp verify_aud: False, # 如果启用了 aud 校验则置 True } )这个写法把所有安全选项都摆在明面上配合注释可读性很高后面接手的同事一眼能看懂你的安全边界在哪里。4. 实操完整用户认证流程与 token 续签实现4.1 登录接口怎么签发 token拿 FastAPI 举个例子这是现在 Python 后端最主流的框架之一。登录成功后生成 token 的流程from fastapi import FastAPI, Depends, HTTPException, Header import jwt import time app FastAPI() SECRET_KEY change-me-32-bytes-minimum TOKEN_EXPIRE_SECONDS 2 * 60 * 60 # 2小时 def create_access_token(user_id: int, role: str) - str: payload { sub: str(user_id), role: role, iat: int(time.time()), exp: int(time.time()) TOKEN_EXPIRE_SECONDS, } return jwt.encode(payload, SECRET_KEY, algorithmHS256) app.post(/login) def login(username: str, password: str): # 这里省略查库校验密码的步骤 if username ! admin or password ! 123456: raise HTTPException(status_code401, detail用户名或密码错误) token create_access_token(user_id1, roleadmin) return {access_token: token, token_type: bearer, expires_in: TOKEN_EXPIRE_SECONDS}几个细节说明一下sub我放字符串类型的 user_id这是 JWT 标准推荐的做法因为sub的定义就是 string。如果直接放 intjwt.decode之后拿到的是 string需要自己转回 int容易造成前后端判断1 1翻车。expires_in字段一并返回给前端这样前端可以提前知道 token 什么时候失效提前做续签而不是等到接口 401 了才抱佛脚。密码校验的逻辑不管怎么实现记住一点永远不要在 token 里带密码相关的任何信息哪怕是一段 hash 都不行。4.2 请求校验逻辑从 Header 取 token 到验签客户端每次请求带着Authorization: Bearer token来服务端在依赖项里做统一校验。FastAPI 的写法from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials security HTTPBearer() def get_current_user(credentials: HTTPAuthorizationCredentials Depends(security)): token credentials.credentials try: payload jwt.decode(token, SECRET_KEY, algorithms[HS256]) user_id int(payload[sub]) role payload.get(role, user) except jwt.ExpiredSignatureError: raise HTTPException(status_code401, detailToken 已过期) except jwt.InvalidTokenError as e: raise HTTPException(status_code401, detailf无效 Token: {e}) # 返回当前用户信息后续接口通过 Depends(get_current_user) 获取 return {user_id: user_id, role: role} app.get(/me) def read_me(current_user: dict Depends(get_current_user)): return current_user到这里一个完整的登录鉴权闭环已经跑通了。但只做到这一步还不足以应对生产环境因为 token 过期这件事处理不好用户体验会很差。下一个问题就来了用户用着用着 token 过期了怎么办4.3 token 续签的两种主流方案实战对比token 过期是 JWT 绕不开的坎。你想让 token 短期有效降低泄露风险但用户不可能每隔两小时重新登录一次。业界主流有两种方案我两个都实现过各有取舍。第一种方案叫“双 token 模式”签发一个短期 access token比如 2 小时有效再签一个长期 refresh token比如 7 天有效。access token 用完就换refresh token 只在 access token 过期后用来换新的 access token。def create_access_token(user_id: int, role: str) - str: payload { sub: str(user_id), role: role, type: access, exp: int(time.time()) 2 * 60 * 60 } return jwt.encode(payload, SECRET_KEY, algorithmHS256) def create_refresh_token(user_id: int) - str: payload { sub: str(user_id), type: refresh, exp: int(time.time()) 7 * 24 * 60 * 60 } return jwt.encode(payload, SECRET_KEY, algorithmHS256) app.post(/refresh) def refresh_token(refresh_token: str): try: payload jwt.decode(refresh_token, SECRET_KEY, algorithms[HS256]) if payload.get(type) ! refresh: raise HTTPException(status_code401, detailToken 类型错误) user_id int(payload[sub]) # 可选校验 refresh token 是否在服务端白名单中 except jwt.ExpiredSignatureError: raise HTTPException(status_code401, detailRefresh Token 已过期) new_access create_access_token(user_id, roleuser) return {access_token: new_access}这种方案的核心是 refresh token 只负责换新 token不访问业务接口且有效期长所以它的保管要求更高。如果 refresh token 泄露攻击者能持续续签。对抗方式是服务端维护一个 refresh token 的版本编号或者黑名单用户改密码后所有 refresh token 立即失效。第二种方案叫“滑动过期时间”不设 dead 过期时间而是每次请求都检查 token 是否到了续签窗口比如剩下 1/3 时间如果到了就签发一个新 token 返回给前端。好处是无感续签坏处是每次请求都在写新 token而且 token 实际上会一直在滑动除非长时间不活跃才真正过期。我个人的偏好是如果是内部管理系统、用户活跃度不高滑动过期方案足够如果是 C 端产品、用户量很大双 token 方案更稳妥因为可以把 refresh token 做成可撤销的。不管哪种方案都要给前端一个明确的“401 后去哪里换 token”的约定这是 SPA 项目里最容易乱的地方。4.4 与 SPA 项目联调时的几个细节热搜词里有“SPA项目开发之jwt验证码实现”这个组合确实很典型。SPA单页应用用 JWT 认证有几个细节值得单独说一下。第一个细节是 token 存放位置。LocalStorage 还是 HttpOnly Cookie我见过太多教程直接让你把 token 存 localStorage但这样一旦被 XSS 拿到token 就被偷走。更稳妥的做法是把 access token 放到内存里刷新页面就没了refresh token 放在 HttpOnly Cookie 里后端通过SameSite和Secure属性控制它的发送范围。这个方案的体验接近传统 session但安全性高很多。第二个细节是 axios 或 fetch 的统一拦截。请求拦截器里加上Authorization: Bearer token响应拦截器里捕获 401自动调用 refresh 接口换新 token然后重放原请求。这个逻辑用不到 pyjwt但它是 JWT 实际能落地的前置条件。第三个细节是验证码。登录接口如果暴露在公网建议配合图形验证码或行为验证防止暴力破解。JWT 本身不提供防爆破能力它只负责签发和验证身份别指望加个 token 就万事大吉。5. 避免踩坑我遇到的 6 个杂症这部分全是实战里别人问过、或者我自己写过的问题整理成速查表可以直接抄作业。症状原因解决方案token 一解码就报DecodeErrortoken 在传输过程中被截断或改动检查前端是否完整传递了 token注意模板引擎会不会截掉.号设置了exp但一直报已过期服务器时间不准或跨时区NTP 同步服务器时间确认传入的是时间戳而不是格式化字符串decode后找不到user_id编码时user_id是 int解码后sub是 string一律用sub放字符串 ID解码后统一转换升级到 pyjwt 2.x 后报algorithms缺失2.x 强制要求显式声明算法decode 调用加algorithms[HS256]token 能验签成功但接口拿不到用户信息Payload 里没放足够的信息服务端没查库登录时把 user_id 放进去中间件里查库补全用户信息有些 token 能用有些不能用签发和验签用的密钥不一致检查是否有多套环境变量确保密钥唯一且一致5.1 定时任务场景的 token 失效问题一个容易被忽略的场景如果你的系统里有一个定时任务或者消息队列消费者它需要调用业务接口但业务接口要求带 token。这种服务间调用如果也用短期 token每跑一次任务都要去登录一次非常难受。解法有两种一是服务间调用用独立的 service account 签发长期 token权限范围控制在最小二是直接走内网做服务发现微服务内部接口不做 JWT 校验只做 mTLS 或 IP 白名单。我个人更倾向于第二种毕竟 JWT 是给“人和服务”的认证用的纯服务间通信有比 JWT 更合适的方案。5.2 单点登录SSO场景下的 RS256 实践如果你的公司有多个系统希望一套账号体系打通各个系统JWT RS256 是很经典的组合。认证中心用私钥签发 token各业务系统拿公钥验签不需要每个系统都连数据库查用户表。实操步骤我走了一遍先用 openssl 生成密钥对openssl genrsa -out private.pem 2048 openssl rsa -in private.pem -pubout -out public.pem认证中心签名with open(private.pem, rb) as f: private_key f.read() token jwt.encode( { sub: user123, aud: business-system-a, exp: int(time.time()) 3600 }, private_key, algorithmRS256 )业务系统验签with open(public.pem, rb) as f: public_key f.read() try: payload jwt.decode( token, public_key, algorithms[RS256], audiencebusiness-system-a ) except jwt.InvalidAudienceError: print(token 不是给本系统的)这里audAudience声明很重要它表示 token 是发给哪个接收方的。如果你有多个业务系统共享同一套公钥体系必须用aud来隔离否则 A 系统的 token 能被 B 系统直接使用这就出了越权漏洞。同一套密钥签发、不同aud接收方互相校验是 SSO 场景的标准姿势。5.3 日志与排错技巧最后说一个提升修复效率的小技巧打印 JWT 报错时不要只打印异常文本把异常类型也带上。因为InvalidTokenError是各种 JWT 异常的基类ExpiredSignatureError、DecodeError、InvalidSignatureError都是它的子类。只捕获基类会丢失具体原因排查起来云里雾里。我在线上环境通常会这样做import logging logger logging.getLogger(__name__) def jwt_decode_safe(token: str, key: str) - dict | None: try: return jwt.decode(token, key, algorithms[HS256]) except jwt.ExpiredSignatureError: logger.warning(jwt expired: token%s..., token[:20]) raise HTTPException(status_code401, detailtoken已过期) except jwt.InvalidSignatureError: logger.warning(jwt signature invalid, need to check key rotation) raise HTTPException(status_code401, detail签名校验失败) except jwt.DecodeError: logger.error(jwt decode failed, malformed token) raise HTTPException(status_code400, detailtoken格式错误) except jwt.InvalidTokenError as e: logger.error(jwt invalid: %s, e) raise HTTPException(status_code401, detail无效token)日志里 token 只打前 20 个字符防止完整 token 泄露到日志系统。这个习惯救过我一次当时线上有个用户反复报“token 无效”我一看日志发现是某个客户端把 token 放在 url 参数里传过来被网关截断了。5.4 本地调试的一些建议本地开发调试 JWT 时打印完整的 token 没关系但要注意两点一是 token 里不要放真实生产数据随便用假的 user_id二是如果你在用 Jupyter Notebook 之类的工具调试注意不要把自己的密钥留在 notebook 里并 commit 到仓库。更推荐的做法是写一个小脚本把 encode 和 decode 都封装好用命令行传入密钥和 payload 来调试。或者直接用在线工具调试 token 的 payload 内容——但要小心别把生产环境的 token 贴到未知网站JWT 的 payload 谁都能解出来。要调试就自己本地写脚本解不要嫌麻烦。6. 从 pyjwt 到完整认证体系还可以扩展的方向6.1 第三方库与框架的集成pyjwt 只是一个底层的编解码工具实际项目中还需要配合其他组件才能组成完整的认证体系。我自己常用的是这几个组合FastAPI python-multipart处理表单登录FastAPI passlib/bcrypt密码哈希校验FastAPI redis维护 refresh token 黑名单、登录状态实时查询Django djangorestframework-simplejwtDRF 生态里已经把 JWT 和用户模型绑得很深封装度更高如果你用的是 Django我不太建议直接用 pyjwt 手写因为 Django 的认证框架高度依赖 session/user modelsimplejwt 这个库做得更贴合。但如果你只是在某个模块里用 JWT 做临时凭证比如邮箱验证链接、分享链接pyjwt 依然是最快的那把刀。6.2 一个我实际落地过的完整折中方案我上一个项目就是标准的 FastAPI pyjwt 组合。最终方案是这样的access token 有效期 30 分钟存前端内存refresh token有效期 7 天存 HttpOnly Cookierefresh token 的id字段存 Redis 一份每次刷新都校验并更新。用户改密码会把所有 refresh token 从 Redis 删掉这就是“可撤销”的 JWT。整个链路走下来我的体会是JWT 并不是“免服务端存储”的银弹而是把存储的责任从 session 转发到了客户端。它解决的痛点是分布式场景下的共享状态问题代价是你必须多花心思在过期、撤销、密钥轮换这些以前由 session 容器帮你管的事情上。用好 pyjwt 的关键不在于多花哨的写法而在于每个决策点上都清楚自己在保什么、舍什么。最后分享一个我在第一版代码里踩过的坑我把 access token 的过期时间设成了一个星期想着用户少登录一次是一次。结果产品上线第一天就有用户投诉“为什么我的账号一直不掉线别人用我电脑登录的都没退”。这就是 token 有效期太长的连锁反应——安全体验是双向的。后来切成双 token 模式refresh token 定期要求用户确认身份反而被吐槽的次数少了。安全不是越严越好是刚刚好。
返回列表