JWT身份验证相关安全问题

前言:工作中需要基于框架开发一个贴近实际的应用,找到一款比较合适的cms框架,其中正好用到的就是jwt做身份信息验证,也记录一下学习jwt相关的安全问题过程。

 

JWT介绍

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT组成

JWT可分为三部分,分别为头部(header)、载荷(payload)、签名(signature),简单介绍一下每个部分的作用。

整体组成如下,以“.”分隔为三头部、载荷、签名部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT加解密网站:JSON Web Tokens - jwt.io

头部(header)

作用:声明类型、加密算法等

原始格式:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

解base64:

{
"alg": "HS256",
"typ": "JWT"
}

typ:声明算法类型,这里是JWT,

alg:声明加密算法,这里是HS256,为对称算法(前后端使用同一密钥进行加密,并非能够解密),常用的还有RS256和ES256两个非对称算法(签名时使用私钥,验证时使用公钥)。

载荷(payload)

作用:存储有效数据

原始格式:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

解base64:

{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}

载荷部分默认字段:

iss (issuer):JWT的发行者
exp (expiration time):过期时间
sub (subject):JWT面向的主题
aud (audience):JWT的用户
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):JWT唯一标识

注:用户可根据需求自定义字段

签名(signature)

作用:签名部分,服务端校验此字段来验证载荷(payload)字段是否合法

原始格式:SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

该字段加密方式如下:

signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)

这里的HMACSHA256就是在header中alg字段指定的HS256加密算法,而RS256和ES256则是服务端使用私钥加密,好处是可以将验证委托给其他应用,只要散发自己的公钥即可。

JWT用法

JWT被用于身份验证中,作用类似于session,但相比于session这种方式各有优劣,下面简述一下JWT的使用流程

一、用户登录,输入登录所需的信息,后端验证后,返回jwt格式的token

二、用户携带token访问需要身份验证的资源或接口

三、服务端验证jwt格式token中的signature(使用相应加密算法重新加密token中的header和payload,验证是否相等)

四、验证成功,允许访问资源

登录获取token代码示例(lin-cms-springboot)

首先入口是login方法

/**
* 用户登陆
*/
@PostMapping("/login")
public Tokens login(@RequestBody @Validated LoginDTO validator, @RequestHeader(value = "Tag", required = false) String tag) {
UserDO user = userService.getUserByUsername(validator.getUsername());
if (user == null) {
throw new NotFoundException(10021);
}
boolean valid = userIdentityService.verifyUsernamePassword(
user.getId(),
user.getUsername(),
validator.getPassword());
if (!valid) {
throw new ParameterException(10031);
}
return jwt.generateTokens(user.getId());
}

判断用户名和密码正确后,将user.getId(即用户的ID)传入generateTokens方法

public Tokens generateTokens(long identity) {
String access = this.generateToken("access", identity, "lin", this.accessExpire);
String refresh = this.generateToken("refresh", identity, "lin", this.refreshExpire);
return new Tokens(access, refresh);
}

根据固定的字段和传入的用户ID,调用generateToken方法获取token(这里调用了两次,分别生成两个token,access_token和refresh_token,后面会讲)

public String generateToken(String tokenType, long identity, String scope, long expire) {
Date expireDate = DateUtil.getDurationDate(expire);
return this.builder.withClaim("type", tokenType).withClaim("identity", identity).withClaim("scope", scope).withExpiresAt(expireDate).sign(this.algorithm);
}

——————以下调用是com.auth0.jwt第三方库中的内容——————

这里反复调用了withClaim方法

public JWTCreator.Builder withClaim(String name, Long value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withClaim(String name, String value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
...
重载的所有withClaim方法具体内容都一样

因为这个方法返回的还是this,所以可以直接循环调用,是为了生成并绑定不同字段的值。

接着调用了assertNonNull方法、addClaim方法

private void assertNonNull(String name) {
if (name == null) {
throw new IllegalArgumentException("The Custom Claim's name can't be null.");
}
}
​
private void addClaim(String name, Object value) {
if (value == null) {
this.payloadClaims.remove(name);
} else {
this.payloadClaims.put(name, value);
}
}

assertNonNull方法就是判空处理,addClaim方法就将键值对put到payloadClaims这个map对象中,也就是最终生成的payload字段。

这里执行完成后,接着还跳回generateToken方法,因为调用完withClaim方法后,就会调用withExpiresAt(expireDate).sign(this.algorithm),

withExpiresAt方法,就是添加exp字段

public JWTCreator.Builder withExpiresAt(Date expiresAt) {
this.addClaim("exp", expiresAt);
return this;
}

接着进入最关键的sign方法

public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCreationException {
if (algorithm == null) {
throw new IllegalArgumentException("The Algorithm cannot be null.");
} else {
this.headerClaims.put("alg", algorithm.getName());
if (!this.headerClaims.containsKey("typ")) {
this.headerClaims.put("typ", "JWT");
}
​
String signingKeyId = algorithm.getSigningKeyId();
if (signingKeyId != null) {
this.withKeyId(signingKeyId);
}
​
return (new JWTCreator(algorithm, this.headerClaims, this.payloadClaims)).sign();
}
}

headerClaims方法就是向header字段中添加typ和alg的值,重点在最终return的地方,接着跟入new JWTCreator(algorithm, this.headerClaims, this.payloadClaims)

JWTCreator类的实例化,其中传入的参数分别是加密算法对象、header、payload

private JWTCreator(Algorithm algorithm, Map<String, Object> headerClaims, Map<String, Object> payloadClaims) throws JWTCreationException {
this.algorithm = algorithm;
​
try {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(ClaimsHolder.class, new PayloadSerializer());
mapper.registerModule(module);
mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
this.headerJson = mapper.writeValueAsString(headerClaims);
this.payloadJson = mapper.writeValueAsString(new ClaimsHolder(payloadClaims));
} catch (JsonProcessingException var6) {
throw new JWTCreationException("Some of the Claims couldn't be converted to a valid JSON format.", var6);
}
}

简单看一下上面的逻辑,就是对算法、header、payload进行绑定,然后接着往下走,跳回上一步的sign方法,在对JWTCreator实例化后,紧接着又调用了sign方法,不过这个sign方法没有传入参数,也就是下面这个方法

private String sign() throws SignatureGenerationException {
String header = Base64.encodeBase64URLSafeString(this.headerJson.getBytes(StandardCharsets.UTF_8));
String payload = Base64.encodeBase64URLSafeString(this.payloadJson.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = this.algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8));
String signature = Base64.encodeBase64URLSafeString(signatureBytes);
return String.format("%s.%s.%s", header, payload, signature);
}

方法中对header和payload的生成就是简单的对json数据进行base64编码,最终生成signature字段的操作为this.algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8)),接着跟入sign方法

public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException {
try {
return this.crypto.createSignatureFor(this.getDescription(), this.secret, headerBytes, payloadBytes);
} catch (InvalidKeyException | NoSuchAlgorithmException var4) {
throw new SignatureGenerationException(this, var4);
}
}

进入到createSignatureFor方法,传入了加密算法、密钥、header和payload

byte[] createSignatureFor(String algorithm, byte[] secretBytes, byte[] headerBytes, byte[] payloadBytes) throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(secretBytes, algorithm));
mac.update(headerBytes);
mac.update((byte)46);
return mac.doFinal(payloadBytes);
}

就不继续往里跟了(里面最后是调javax.crypto.Mac类中的方法进行的加密,调来调去太乱了),大致原理我们已经搞清楚了,差不多就是根据header和payload字段,然后用secret当salt做一次hash,最后再base64编码传出来,就是我们最终的token。

以上就是从登录到获取token的大致过程。

通过jwt进行身份校验代码示例

这里是拦截器中的一个方法,就不一步一步的跟了,

public boolean handleLogin(HttpServletRequest request, HttpServletResponse response, MetaInfo meta) {
//获取请求头中的Authorization头
String tokenStr = verifyHeader(request, response);
Map<String, Claim> claims;
try {
//在这里做的校验
claims = jwt.decodeAccessToken(tokenStr);
} catch (TokenExpiredException e) {
throw new io.github.talelin.autoconfigure.exception.TokenExpiredException(10051);
} catch (AlgorithmMismatchException | SignatureVerificationException | JWTDecodeException | InvalidClaimException e) {
throw new TokenInvalidException(10041);
}
return getClaim(claims);
}

最终验证的地方在com.auth0.jwt.algorithms.HMACAlgorithm#verify方法中

public void verify(DecodedJWT jwt) throws SignatureVerificationException {
byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature());
​
try {
//然后调用verifySignatureFor方法校验
boolean valid = this.crypto.verifySignatureFor(this.getDescription(), this.secret, jwt.getHeader(), jwt.getPayload(), signatureBytes);
if (!valid) {
throw new SignatureVerificationException(this);
}
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException var4) {
throw new SignatureVerificationException(this, var4);
}
}

跟入

boolean verifySignatureFor(String algorithm, byte[] secretBytes, String header, String payload, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException {
return this.verifySignatureFor(algorithm, secretBytes, header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8), signatureBytes);
}
​
boolean verifySignatureFor(String algorithm, byte[] secretBytes, byte[] headerBytes, byte[] payloadBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException {
return MessageDigest.isEqual(this.createSignatureFor(algorithm, secretBytes, headerBytes, payloadBytes), signatureBytes);
}

跟入

byte[] createSignatureFor(String algorithm, byte[] secretBytes, byte[] headerBytes, byte[] payloadBytes) throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(secretBytes, algorithm));
mac.update(headerBytes);
mac.update((byte)46);
return mac.doFinal(payloadBytes);
}

可以看到,这里的createSignatureFor方法,那么最终实现的原理也就是根据传入的header和payload,再调用创建签名方法根据secret密钥创建签名,如果最终创建出来的签名和你传入的jwttoken中的Signature字段值相等,则判定为真。

JWT安全问题

secret硬编码

将加密使用的secret密钥硬编码在框架中,如果开发者不注意的话,使用默认密钥,没有进行修改,那么只要获取到密钥,就可以伪造token。

知道密钥后,那么可以通过该网站https://jwt.io或者编写脚本伪造token

例如该cms:

1659882676_62efccb4e7be5d7abeaa3.png!small?1659882677806

通过在线网站生成token:

1659882686_62efccbe2184e186d88e3.png!small?1659882686985

那么我们直接使用这个token即可访问网站中需要身份验证的接口资源。

1659882697_62efccc9a29fc0fb41501.png!small?1659882698632

后端未校验Signature字段或

攻击方法:可任意修改或者直接删除

原理:后端未对Signature字段进行校验,就取payload中的数据进行后续操作

alg=none签名绕过漏洞(CVE-2015-2951)

攻击方法:将header中的alg的键值改为none,然后将Signature删除即可

原理:后端未对传入的header中的alg字段进行校验,直接使用其中指定的加密算法对Signature

针对以上两种安全问题,如果没有原始jwt_token,可以使用如下脚本生成token:

使用之前需要先使用pip安装PyJWT,而不是JWT,直接python37 -m pip install PyJWT即可

import jwt
payload = {
"identity": 1,
"scope": "lin",
"type": "access",
"exp": 1659797574
}
print(jwt.encode(payload,None,algorithm="none"))

Secret爆破

上面我们知道硬编码的话我们可以任意伪造token,其实原理就是知道secret密钥,除了开源CMS的这种泄露,或者其他系统备份文件、日志之类的泄露密钥,我们还可以通过爆破的方法获取密钥(如果不是弱密钥,难度非常大)

可以使用这个工具:https://github.com/ticarpi/jwt_tool

修改非对称密码算法为对称密码算法(CVE-2016-10555)

这个漏洞只针对使用非对称加密算法(RS256)做校验的系统,当后端使用RS256加密时,使用私钥加密,而校验时用到的是公私钥对中的公钥做校验,而公钥是公开的,我们很容易获取。

那么当我们修改RS256为HS256时,后端会以为使用的是HS256对称加密做校验,即使用公钥当作HS256校验时的secret来进行加密对比是否相等,把公钥当成secret来使用,也就相当于泄露了secret,所以我们就可以使用公钥来伪造token。

伪造密钥(CVE-2018-0114)

我理解这个漏洞和上面的漏洞比较像,上一个漏洞是后台新人了我们提供的header中的算法,这个是使用了JWS,里面也是存储的公钥,那么我们自己生成公私钥对,然后使用私钥生成token,再将自己生成的公钥放到JWS中,让后台使用这个公钥解密,这样就可以巧妙地绕过后台的验证。

网络安全学习资源分享:

给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

因篇幅有限,仅展示部分资料,朋友们如果有需要全套《网络安全入门+进阶学习资源包》,需要点击下方链接即可前往获取

CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享(安全链接,放心点击)

同时每个成长路线对应的板块都有配套的视频提供: 

大厂面试题

视频配套资料&国内外网安书籍、文档

当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料

所有资料共282G,朋友们如果有需要全套《网络安全入门+进阶学习资源包》,可以扫描下方二维码或链接免费领取~ 

 读者福利 | CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享(安全链接,放心点击)

特别声明:

此教程为纯技术分享!本教程的目的决不是为那些怀有不良动机的人提供及技术支持!也不承担因为技术被滥用所产生的连带责任!本教程的目的在于最大限度地唤醒大家对网络安全的重视,并采取相应的安全措施,从而减少由网络安全而带来的经济损失。

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

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

相关文章

如何遍历并处理不平衡的Python数据集

目录 一、引言 二、不平衡数据集的概念与影响 三、处理不平衡数据集的策略 重采样策略 集成学习方法 代价敏感学习 一分类方法 四、Python工具与库 五、案例分析与代码实现 案例一&#xff1a;使用imbalanced-learn库进行上采样 案例二&#xff1a;使用scikit-learn…

基于L1范数惩罚的稀疏正则化最小二乘心电信号降噪方法(Matlab R2021B)

L1范数正则化方法与Tikhonov正则化方法的最大差异在于采用L1范数正则化通常会得到一个稀疏向量&#xff0c;它的非零系数相对较少&#xff0c;而Tikhonov正则化方法的解通常具有所有的非零系数。即&#xff1a;L2范数正则化方法的解通常是非稀疏的&#xff0c;并且解的结果在一…

CentOS 8 (stream) 迁移到 其他开源linux almalinux

Centos8 迁移到 almalinux 只需要下载迁移脚本即可 #wget -c https://raw.githubusercontent.com/AlmaLinux/almalinux-deploy/master/almalinux-deploy.sh #chmod x almalinux-deploy.sh //对于centos Stream迁移需要加上降级参数 almalinux-deploy.sh --downgrade

vue 笔记02

目录 01 事件修饰符 02 按键修饰符 03 v-bind属性 04 vue-axios的基本使用 05 vue的生命周期 06 vue生命周期涉及到的其他的知识点 01 事件修饰符 vue的事件修饰符 事件名称.修饰符1.修饰符2...事件驱动函数 stop 阻止冒泡修饰符 prevent 阻止默认行为 once 当前事件只触…

Shell的条件语句

一 条件测试 1.1文件测试与整数测试 1.2字符串测试与逻辑测试 二 If语句 2.1If单分支语句 2.2If双分支语句 2.3If多分支语句 三 Case分支语句 四 总结 一 条件测试操作 1.1文件测试 要使shell脚本程序具备一定的&#xff02;智能&#xff02;&#xff0c;面临的第一个问…

【信息学奥赛】字典的键和值对换

【信息学奥赛】字典的键和值对换 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 输入一个字典&#xff0c;要求将该字典的键和值对换。&#xff08;注意&#xff0c;字典中有键的值是重复的&#xff09; 输入&#xff1a; 一行&#xff0…

信息系统项目管理师0137:输出(8项目整合管理—8.9结束项目或阶段—8.9.3输出)

点击查看专栏目录 文章目录 8.9.3 输出8.9.3 输出 项目文件(更新)可在结束项目或阶段更新所有项目文件,并标记为最终版本。特别值得注意的是,经验教训登记册的最终版本要包含阶段或项目收尾的最终信息。最终版本的经验教训登记册可包含:效益管理、项目评估的准确性、项目和…

PostgreSQL教程

引言 PostgreSQL&#xff0c;简称Postgres&#xff0c;是一个强大、开源的对象关系型数据库管理系统&#xff08;ORDBMS&#xff09;。它因其高性能、稳定性和灵活性而广受欢迎。本文将详细介绍PostgreSQL&#xff0c;从安装、基本操作到高级功能&#xff0c;帮助你全面掌握这…

修改vuetify3的开关组件v-switch在inset模式下的大小

<v-switchv-model"model":label"Switch: ${model.toString()}"hide-detailsinset></v-switch>使用方式1&#xff1a;本页面使用 本页面中使用&#xff0c;必须要含有lang“scss” scoped&#xff0c;才会生效 <style lang"scss"…

基础漏洞系列——CSRF跨站请求伪造

简介&#xff1a; 跨站请求伪造&#xff08;英语&#xff1a;Cross-site request forgery&#xff09;&#xff0c;也被称为 one-click attack或者 session riding&#xff0c;通常缩写为 CSRF或者 XSRF&#xff0c; 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操…

【会议征稿,JPCS出版】2024年航空航天与力学国际学术会议(ICAM 2024)

2024年航空航天与力学国际学术会议&#xff08;ICAM 2024&#xff09;将于2024年7月12-14日在中国沈阳举办。会议由东北大学机械工程与自动化学院主办&#xff0c;吉林大学机械与航空航天工程学院承办&#xff0c;大连理工大学、沈阳航空航天大学、沈阳建筑大学、沈阳工业大学、…

react面试题----1(基础和生命周期)

react生命周期面试题 1.react 生命周期函数 初始化阶段: getDefaultProps:获取实例的默认属性getInitialState:获取每个实例的初始化状态componentWillMount:组件即将被装载、渲染到页面上render:组件在这里生成虚拟的 DOM 节点componentDidMount:组件真正在被装载之后运行…

适合源代码的加密系统

在信息化高度发展的今天&#xff0c;源代码的安全问题日益受到重视。源代码是企业或组织的核心资产&#xff0c;一旦泄露或被篡改&#xff0c;将会对其造成巨大的经济损失和声誉风险。因此&#xff0c;如何有效防止源代码的泄露和算改成为了企业和组织急需解决的问题。沙盒技术…

unity 安卓层面的简单热更办法

咱们前段时间不是在写个app吗&#xff0c;就暂时不想搞ios端&#xff0c;但是要实现热更的话&#xff0c;咋办呢&#xff1f;按道理说如果只是安卓端热更的话&#xff0c;其实很简单&#xff0c;因为安卓是支持你程序编译的&#xff0c;所以你可以下载程序&#xff0c;进行安装…

代码随想录算法训练营第四十三天 | 343. 整数拆分、96.不同的二叉搜索树

343. 整数拆分 代码随想录 视频讲解&#xff1a;动态规划&#xff0c;本题关键在于理解递推公式&#xff01;| LeetCode&#xff1a;343. 整数拆分_哔哩哔哩_bilibili 解题思路 1. dp[i]对i进行拆分&#xff0c;得到的最大的乘积为dp[i] 2.递推公式 一个是j * (i - j) 直接相…

Sylvester矩阵、子结式、辗转相除法的三者关系(第三部分)

2.执行辗转相除法第二步 F 7 Q 7 , 6 F 6 F 4 deg ⁡ ( F 7 ) 7 deg ⁡ ( F 6 ) 6 deg ⁡ ( F 4 ) 4 F_{7} Q_{7,6} \times F_{6} F_{4}\ \ \ \ \ \ \ \ \ \ \deg\left( F_{7} \right) 7\ \ \ \ \ \ \deg\left( F_{6} \right) 6\ \ \ \ \ \ \deg\left( F_{4} \right)…

【Rust】——使用消息在线程之间传递数据

&#x1f4bb;博主现有专栏&#xff1a; C51单片机&#xff08;STC89C516&#xff09;&#xff0c;c语言&#xff0c;c&#xff0c;离散数学&#xff0c;算法设计与分析&#xff0c;数据结构&#xff0c;Python&#xff0c;Java基础&#xff0c;MySQL&#xff0c;linux&#xf…

非计算机科班如何顺利转入计算机领域:经验分享与建议

近年来&#xff0c;计算机领域的发展日新月异&#xff0c;吸引了越来越多非计算机科班的人士希望跳槽转入这个行业。对于这些人来说&#xff0c;如何顺利转码成为一名程序员&#xff0c;成为了一个备受关注的话题。在本文中&#xff0c;我将分享一些关于非计算机科班如何顺利转…

架构(十七)翻译监控

一、引言 作者最近做的一个功能是需要监控一个翻译转换&#xff0c;根据国家和语言进行分组&#xff0c;然后定时把监控情况放到ck里面。为什么是分组和定时监控呢&#xff1f;因为调用比较高的系统的qps在单机一万多&#xff0c;70台机器&#xff0c;可怕的高频调用注定他不能…

探秘GPT-4o:从版本对比到技术能力的全面评价

随着人工智能技术的不断发展&#xff0c;自然语言处理领域的突破性技术——GPT&#xff08;Generative Pre-trained Transformer&#xff09;系列模型也在不断演进。最新一代的GPT-4o横空出世&#xff0c;引起了广泛的关注和讨论。在本文中&#xff0c;我们将对GPT-4o进行全面评…