当前位置: 首页 > news >正文

【微服务】SpringBoot 整合轻量级安全框架JWE 项目实战详解

目录

一、前言

二、JWE 与JWT 介绍

2.1 什么是 JWE

2.2 JWE 与 JWT 的关系

2.3 JWE 主要特点

2.4 JWE 数据结构

2.5 JWE 中常用的加密算法

密钥加密算法 (alg)

内容加密算法 (enc)

2.6 JWE 对比JWT优势

2.6.1 JWT(通常指JWS)局限性

2.6.2 JWE(JSON Web Encryption)优势

2.6.3 JWE与JWT 最佳实践探索

2.7 JWE 优缺点

三、基于JWT 加解密项目整合

3.1 JWT 基本使用

3.1.1 导入基本的依赖

3.1.2 添加一个JWT工具类

3.1.3 提供一个自定义controller

3.2 效果测试

四、基于JWE 加解密项目整合

4.1 导入基本依赖

4.2 提供JWE加解密工具类

4.3 提供JWE加解密测试用例

4.4 整合SpringBoot 常用的优化思路

五、写在最后


一、前言

这些年随着互联网技术的飞速发展,人们对于互联网产品在使用过程中的安全性也提出了更高的要求。在微服务开发过程中,几乎所有的需要上线的项目都会在安全方面进行基本的架构和设计,比如大家熟悉的登录认证,仅这一项就有种类繁多的操作方式,比如账户密码登录,手机验证码登录,有的甚至还需要结合三方认证服务进行安全校验,无一不说明对安全的重视程度。在springboot项目中,对安全的防护首先就是如何保护服务端的接口,经过多年的发展,也形成了比较多的解决思路,也产生了比较多的安全认证框架。本文以java生态下使用比较广泛并且比较轻量级的安全框架JWE为例进行详细的说明。

二、JWE 与JWT 介绍

2.1 什么是 JWE

JWE(JSON Web Encryption)是一种基于JSON的数据加密标准,属于JOSE(Javascript Object Signing and Encryption)套件的一部分。它定义了如何对JSON数据进行加密和解密,确保数据的机密性和完整性。官网:https://www.jwt.io/

2.2 JWE 与 JWT 的关系

说到JWE,很难不将其与JWT进行对比,具体来说,两者主要有下面的区别:

  • JWT (JSON Web Token): 主要用于身份验证和信息交换,可以签名但不加密

  • JWE (JSON Web Encryption): 对JWT或其他数据进行加密,保护数据机密性

  • 常见组合: JWT + JWE = 安全的加密令牌

JWT简称 JSON Web Token,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输,在数据传输过程中还可以完成数据加密,签名等相关处理。

而JSON Web 加密 (jwe) 是 rfc 7516 定义的标准,它使用基于 json 的数据结构表示加密内容。它允许您加密任意有效负载以确保机密性和完整性(如果需要)。此加密内容可以包括任何类型的数据,例如敏感的用户信息、安全令牌甚至文件。

2.3 JWE 主要特点

jwe 广泛用于 web 应用程序和 api,以安全地传输敏感数据,例如令牌、用户信息和财务详细信息。它确保信息即使被拦截也无法被未经授权的实体读取。加密的有效负载只能由拥有正确解密密钥的预期接收者解密和使用。

其主要特点如下:

  • 保密性:jwe 的首要目标是确保内容的机密性。

  • 完整性:保证数据在传输过程中不被篡改。

  • 互操作性:jwe 与不同的加密算法和环境兼容。

  • 紧凑性:jwe 提供了一种紧凑的表示形式,易于通过 http 传输。

2.4 JWE 数据结构

为了深入学习和掌握JWE,结合下面给出JWE的完整数据结构和图示进行理解,具体来说,一个JWE Token由5个部分组成,用点号分隔,如下:

Base64Url(Header).Base64Url(EncryptedKey).Base64Url(IV).Base64Url(Ciphertext).Base64Url(Tag)

下面对参数的完整结构做补充说明

1)JWE Header

该参数体主要包含加密算法和参数:

{"alg": "RSA-OAEP-256",     // 密钥加密算法"enc": "A128GCM",          // 内容加密算法"typ": "JWT",              // 类型"cty": "JWT"               // 内容类型
}

2)Encrypted Key

使用Header中指定的算法加密的对称密钥

3)Initialization Vector (IV)

加密算法的初始化向量

4)Ciphertext

实际加密的数据

5)Authentication Tag

完整性验证标签

2.5 JWE 中常用的加密算法

在使用JWE对数据进行加密时,其SDK提供了丰富的加密算法可供开发者选择,下面列举几种JWE常用的加密算法:

密钥加密算法 (alg)

  • RSA系列: RSA-OAEP, RSA-OAEP-256, RSA1_5

  • AES密钥包装: A128KW, A192KW, A256KW

  • 直接加密: dir

  • 密钥协商: ECDH-ES, ECDH-ES+A128KW

内容加密算法 (enc)

  • AES GCM: A128GCM, A192GCM, A256GCM

  • AES CBC: A128CBC-HS256, A192CBC-HS384, A256CBC-HS512

2.6 JWE 对比JWT优势

为什么要选择JWE呢?JWE 和 JWT 并不是相互替代的关系,而是解决不同问题的互补技术。从使用经验来说,更多的还是考虑到安全问题,简单来说:

  • JWT(JSON Web Token) 的核心是签名,确保数据不被篡改,但数据本身是明文的。

  • JWE(JSON Web Encryption) 的核心是加密,确保数据的机密性,内容对未经授权的一方完全不可读。

因此,讨论 JWE 的优势,实际上可以说是在讨论“加密的令牌”相对于“签名的令牌”在特定场景下的优势。

2.6.1 JWT(通常指JWS)局限性

通常所说的 JWT,绝大多数情况下指的是经过签名(JWS,JSON Web Signature)的令牌。它的结构是 Header.Payload.Signature

Payload 是 Base64Url 编码的,不是加密的。任何人都可以轻松地将 Payload 部分解码,看到里面的所有声明(Claims),如 email, userId, role 等。Payload 的基本结构如下:

// Header
{"alg": "HS256","typ": "JWT"
}
// Payload (明文,仅Base64编码)
{"sub": "1234567890","name": "John Doe","admin": true,"iat": 1516239022
}

基于上面的分析不难发现,JWT(JWS)的主要问题如下:

  1. 敏感信息泄露:如果令牌中包含邮箱、手机号、权限等敏感信息,这些信息在传输和存储过程中都是暴露的。如果令牌被中间人截获或在客户端(如浏览器 LocalStorage)被恶意脚本读取,敏感信息就泄露了。

  2. 不适合在不可信方传递:JWT 经常被发给客户端(如浏览器、移动App)。客户端是一个不可完全信任的环境。你不应该让客户端看到任何它不该看的信息。

2.6.2 JWE(JSON Web Encryption)优势

JWE 的结构是 Header.EncryptedKey.IV.Ciphertext.AuthenticationTag,它将整个有效载荷(Payload)进行了加密。这样做的优势如下:

  1. 机密性

    1. 这是最核心的优势。 JWE 确保了令牌内容只有持有正确解密密钥的接收方才能读取。

    2. 解决了敏感信息泄露问题:即使令牌被截获,攻击者也无法得知里面的内容。这对于遵守数据隐私法规(如GDPR、HIPAA)至关重要。

  2. 同时提供机密性和完整性

    1. JWE 不仅加密数据,还通过 Authentication Tag 提供了完整性保护,确保加密后的数据在传输过程中没有被篡改。它同时做到了 JWS(完整性)和加密(机密性)的工作。

  3. 适用于包含敏感数据的场景

    1. 身份信息:如完整的用户档案、地址、社保号等。

    2. 授权码:在 OAuth 2.0 流程中,授权码本身就是一个敏感凭证,使用 JWE 格式传输更为安全。

    3. 客户端凭证:当后端服务需要向客户端传递一些它需要使用但不应理解的“黑盒”数据时。

    4. 服务间通信:在微服务架构中,如果一个令牌需要穿过多个中间服务才能到达目标服务,而你又不想让中间服务看到令牌内容,JWE 是理想选择。

  4. 灵活性

    1. JWE 支持多种加密算法(如 RSA-OAEP, A128GCM等),允许开发者根据安全需求和性能考虑进行选择。

通过下面这张表的对比能看的更清楚:

特性JWT(通常指 JWS)JWE
核心目标数据完整性 和 认证数据机密性 和 完整性
Payload 状态明文,仅 Base64Url 编码加密的密文
可读性任何人可解码并读取 Payload只有持有密钥的接收方可解密并读取
主要风险敏感信息泄露密钥管理复杂(如果密钥泄露,则安全性丧失)
典型结构Header.Payload.SignatureHeader.EncryptedKey.IV.Ciphertext.AuthenticationTag
适用场景会话管理、无状态认证、公开信息传递传输敏感数据、保护授权码、服务间安全通信

2.6.3 JWE与JWT 最佳实践探索

事实上,JWE 和 JWS(JWT)可以被结合使用,以实现最佳的安全效果。这被称为“签名然后加密”或“加密然后签名”模式。一个常见的模式是:

  1. 内部创建一个 JWS:首先,认证服务器创建一个包含用户声明的 JWS,以确保数据的完整性。

  2. 对外发布一个 JWE:然后,将这个 JWS 整个作为 payload,用 JWE 进行加密。

  3. 结果:客户端得到的是一个 JWE。它自己无法解密,当它把这个 JWE 发给资源服务器时,资源服务器先解密,得到里面的 JWS,然后再验证 JWS 的签名。

这种方式既保证了令牌内容不被客户端窥探(机密性),又保证了令牌是由可信的认证服务器颁发的(完整性和认证)。

小结:

JWE 相对于 JWT优势在于提供了更强大的安全性:

  • 如果你需要在不安全的信道(如互联网)上传输令牌,或者需要将令牌存储在不可信的客户端,并且令牌内包含了任何敏感信息时,就应该优先考虑使用 JWE。

  • 如果你的令牌中只包含一些不敏感的且可以公开的信息,比如用户ID,并且安全依赖于签名验证和短暂的过期时间,那么使用JWT就够了。

选择哪一个,完全取决于你的应用场景和对数据机密性的要求。在安全要求极高的系统中,“JWS嵌套在JWE内”是黄金标准。

2.7 JWE 优缺点

尽管JWE能够带来更多的安全性,但是实际选择的时候也需要进行取舍,下面列举了JWE的优缺点,方便技术选型的使用做完参考。

1)优点

  • 保密性:提供端到端加密,确保数据隐私。

  • 互操作性:跨不同系统和平台兼容。

  • 完整性和安全性:确保数据免遭篡改。

  • 支持多个收件人:允许使用不同的密钥将数据加密到多个收件人。

2)缺点

  • 复杂性高:加密和解密的过程可能很复杂且容易出错。

  • 性能开销大:加密/解密过程会增加计算开销,选择的加密算法越复杂,加解密时开销越高。

  • 更大的有效负载大小:由于加密元数据,jwe 有效负载比纯数据或 jwt 更大。

三、基于JWT 加解密项目整合

为了在后续使用JWE过程中有更深的理解,并与JWT形成比较清晰的理解和对比,首先通过一个JWT的案例来重温一下JWT的基本功能使用。

3.1 JWT 基本使用

3.1.1 导入基本的依赖

在pom文件中导入基本的JWT依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version>
</dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version>
</dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency>

3.1.2 添加一个JWT工具类

自定义一个JWT工具类,参考实际项目中的一个实际业务场景,分别提供生成token,解析token,验证token的有效性这几个方法,参考下面的代码:

package com.congge.jwt;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;@Component
public class JwtUtil {// 密钥private final String SECRET_KEY = "mySecretKey123456789012345678901234567890";// Token有效期(7天)private final long EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 7;/*** 生成Token*/public String generateToken(String username) {Map<String, Object> claims = new HashMap<>();return createToken(claims, username);}private String createToken(Map<String, Object> claims, String subject) {return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())).setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)).signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();}/*** 从Token中提取用户名*/public String extractUsername(String token) {return extractAllClaims(token).getSubject();}/*** 提取Claims*/private Claims extractAllClaims(String token) {return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();}/*** 验证Token是否有效*/public Boolean validateToken(String token) {try {return !isTokenExpired(token);} catch (Exception e) {return false;}}/*** 检查Token是否过期*/private Boolean isTokenExpired(String token) {return extractAllClaims(token).getExpiration().before(new Date());}/*** 从请求头中提取Token*/public String extractTokenFromHeader(String authorizationHeader) {if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {return authorizationHeader.substring(7);}return null;}
}

3.1.3 提供一个自定义controller

为了方便测试看效果,提供2个测试的接口,参考下面的代码

package com.congge.web;import com.congge.entity.User;
import com.congge.jwt.JwtUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;import java.util.HashMap;
import java.util.Map;@RestController
@RequestMapping("/auth")
public class AuthController {@Autowiredprivate JwtUtil jwtUtil;// 模拟用户数据private Map<String, String> userDatabase = new HashMap<>();public AuthController() {// 初始化模拟用户userDatabase.put("admin", "admin123");userDatabase.put("user", "user123");}@PostMapping("/login")public ResponseEntity<?> login(@RequestBody User loginUser) {String username = loginUser.getUsername();String password = loginUser.getPassword();// 验证用户名和密码if (!userDatabase.containsKey(username) || !userDatabase.get(username).equals(password)) {Map<String, String> response = new HashMap<>();response.put("error", "用户名或密码错误");return ResponseEntity.badRequest().body(response);}// 生成TokenString token = jwtUtil.generateToken(username);Map<String, String> response = new HashMap<>();response.put("token", token);response.put("username", username);response.put("message", "登录成功");return ResponseEntity.ok(response);}@Resourceprivate HttpServletRequest request;@GetMapping("/verify")public ResponseEntity<?> verifyToken() {String authHeader = request.getHeader("Authorization");String token = jwtUtil.extractTokenFromHeader(authHeader);if (token == null) {return ResponseEntity.badRequest().body("缺少Token");}if (!jwtUtil.validateToken(token)) {return ResponseEntity.badRequest().body("Token无效或已过期");}String username = jwtUtil.extractUsername(token);Map<String, String> response = new HashMap<>();response.put("username", username);response.put("message", "Token验证成功");response.put("status", "valid");return ResponseEntity.ok(response);}
}

3.2 效果测试

启动工程后,分别测试一下2个接口。

1)模拟登录,生成token

在接口工具中,调用下第一个生成token接口

2)验证token是否有效

在接口工具中,验证token是否有效

四、基于JWE 加解密项目整合

接下来通过实际案例演示在springboot项目中如何整合JWE使用。

4.1 导入基本依赖

在pom文件中导入下面的JWE依赖

<!--jwe 的依赖-->
<dependency><groupId>com.nimbusds</groupId><artifactId>nimbus-jose-jwt</artifactId><version>9.37.3</version>
</dependency>

4.2 提供JWE加解密工具类

自定义给一个JWE的工具类,在工具类中提供常用的加解密方法,仍然以上述的关于token的生成和解析为业务背景,提供token的创建,token解析,token校验是否有效几个方法,参考下面的代码

package com.congge.jwe;import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import com.nimbusds.jwt.EncryptedJWT;
import com.nimbusds.jwt.JWTClaimsSet;import java.text.ParseException;
import java.util.Date;
import java.util.Map;public class JweUtil {private final RSAKey rsaKey;private final JWEAlgorithm algorithm;private final EncryptionMethod encryptionMethod;public JweUtil() throws JOSEException {// 生成RSA密钥对this.rsaKey = new RSAKeyGenerator(2048).keyID("123").generate();this.algorithm = JWEAlgorithm.RSA_OAEP_256;this.encryptionMethod = EncryptionMethod.A128GCM;}public JweUtil(RSAKey rsaKey) {this.rsaKey = rsaKey;this.algorithm = JWEAlgorithm.RSA_OAEP_256;this.encryptionMethod = EncryptionMethod.A128GCM;}/*** 创建JWE Token*/public String createJweToken(String subject, Map<String, Object> claims,long expirationMinutes) throws JOSEException {// 创建JWT声明JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder().subject(subject).issueTime(new Date()).expirationTime(new Date(System.currentTimeMillis() + expirationMinutes * 60 * 1000));// 添加自定义声明if (claims != null) {claims.forEach(claimsBuilder::claim);}JWTClaimsSet jwtClaims = claimsBuilder.build();// 创建JWE头JWEHeader header = new JWEHeader.Builder(algorithm, encryptionMethod).contentType("JWT") // 表明加密的内容是JWT.keyID(rsaKey.getKeyID()).build();// 创建加密的JWTEncryptedJWT encryptedJWT = new EncryptedJWT(header, jwtClaims);// 使用RSA公钥加密RSAEncrypter encrypter = new RSAEncrypter(rsaKey.toRSAPublicKey());encryptedJWT.encrypt(encrypter);return encryptedJWT.serialize();}/*** 解析JWE Token*/public JWTClaimsSet parseJweToken(String jweToken)throws ParseException, JOSEException {// 解析JWE TokenEncryptedJWT encryptedJWT = EncryptedJWT.parse(jweToken);// 使用RSA私钥解密RSADecrypter decrypter = new RSADecrypter(rsaKey.toRSAPrivateKey());encryptedJWT.decrypt(decrypter);return encryptedJWT.getJWTClaimsSet();}/*** 验证JWE Token是否有效*/public boolean validateJweToken(String jweToken) {try {JWTClaimsSet claims = parseJweToken(jweToken);return claims.getExpirationTime().after(new Date());} catch (Exception e) {return false;}}/*** 刷新JWE Token*/public String refreshJweToken(String jweToken, long newExpirationMinutes)throws ParseException, JOSEException {JWTClaimsSet oldClaims = parseJweToken(jweToken);// 创建新的声明集,保留除时间外的所有声明JWTClaimsSet.Builder newClaimsBuilder = new JWTClaimsSet.Builder().subject(oldClaims.getSubject()).issueTime(new Date()).expirationTime(new Date(System.currentTimeMillis() + newExpirationMinutes * 60 * 1000));// 复制自定义声明oldClaims.getClaims().forEach((key, value) -> {if (!"iat".equals(key) && !"exp".equals(key) && !"nbf".equals(key)) {newClaimsBuilder.claim(key, value);}});return createJweToken(oldClaims.getSubject(),newClaimsBuilder.build().getClaims(), newExpirationMinutes);}public RSAKey getRsaKey() {return rsaKey;}
}

通过创建token的方法不难看出,对请求的header以及claim进行了加密,这样的话,万一说token被泄漏了,有人拿到了,仍然无法直接进行破解,因为私钥存储在服务端,理论上讲,只要私钥没有泄漏,加密算法没有泄漏,这个token就是安全的。

4.3 提供JWE加解密测试用例

为了验证效果,提供一个测试类(也可以通过编写接口进行验证),对上述的工具类中的核心方法进行效果验证,参考下面的代码

package com.congge.jwe;import com.nimbusds.jwt.JWTClaimsSet;import java.util.Map;public class JweExampleTest {public static void main(String[] args) {try {// 1. 创建JWE工具实例JweUtil jweUtil = new JweUtil();// 2. 准备声明数据Map<String, Object> claims = Map.of("userId", 12345,"username", "john_may","roles", new String[]{"USER", "ADMIN"},"email", "john_may@example.com");// 3. 创建JWE TokenString jweToken = jweUtil.createJweToken("john_may", claims, 60); // 60分钟过期System.out.println("JWE Token: " + jweToken);// 4. 解析和验证JWE TokenJWTClaimsSet parsedClaims = jweUtil.parseJweToken(jweToken);System.out.println("Subject: " + parsedClaims.getSubject());System.out.println("Username: " + parsedClaims.getClaim("username"));System.out.println("Roles:" + parsedClaims.getClaim("roles"));// 5. 验证Token有效性boolean isValid = jweUtil.validateJweToken(jweToken);System.out.println("Token is valid: " + isValid);// 6. 刷新TokenString refreshedToken = jweUtil.refreshJweToken(jweToken, 120);System.out.println("Refreshed Token: " + refreshedToken);} catch (Exception e) {e.printStackTrace();}}
}

运行上面的程序,观察控制台的输出效果,可以看到,通过这种方式得到的加密字符串更加复杂,其结构与上述我们分解其内部结构一致。

4.4 整合SpringBoot 常用的优化思路

通常来说,针对JWE或者JWT这种类型的token认证,行业中比较通用的做法可以参考下面的完整思路:

  • 编写JWE(JWT)工具类,结合实际业务需求,比如密钥,claims承载的信息,安全等级等信息编写相应的工具方法,至少包括:token生成,token解密得到claims的信息,token验证是否有效,token刷新;

  • 编写全局的过滤器(拦截器),如果有gateway这样的网关也可以,在前端进入服务端接口之前,先在拦截器(过滤器/网关)中进行拦截,验证token的有效性,token有效才能走到真正的接口进行逻辑处理;

  • 程序应该有针对token过期时间的自动续约(自动刷新token)的处理,对于那些在页面上一直在操作的行为,程序需要对这类token进行自动的续约,确保比较好的用户体验;

五、写在最后

本文通过实际的案例演示了如何使用JWT与JWE的完整过程,在如今的互联网时代背景下,人们对于安全的诉求越来越高,选择安全度更高的技术一直是所有的应用开发者在持续追求的,希望对看到的同学有用哦,本篇到此结束,感谢观看。

http://www.dtcms.com/a/515456.html

相关文章:

  • 一个完整的AI项目从需求分析到部署的全流程详解
  • UE5 材质-14:减法subtract节点适用于向量与标量,数学 if 组件,由已遮罩材质结合自发光参数,周期性改变蒙版的大小,实现溶解效果
  • 构建AI智能体:七十一、模型评估指南:准确率、精确率、F1分数与ROC/AUC的深度解析
  • 基于脚手架微服务的视频点播系统-客户端业务逻辑处理部分(二)
  • 电商网站开发 文献综述百度网址大全 旧版本
  • 网站平台建设保密协议新网域名续费
  • 机器学习之生成对抗网络(GAN)
  • 零基础-动手学深度学习-13.11. 全卷积网络
  • JMeter测试关系数据库: JDBC连接
  • Linux(五):进程优先级
  • 【算法专题训练】26、队列的应用-广度优先搜索
  • 可靠性SLA:服务稳定性的量化承诺
  • 收集飞花令碎片——C语言内存函数
  • c语言-字符串
  • 红帽Linux -章8 监控与管理进程
  • 企业网站规范简述seo的优化流程
  • LLaMA Factory进行微调训练的时候,有哪些已经注册的数据集呢?
  • 【人工智能系列:走近人工智能03】概念篇:人工智能中的数据、模型与算法
  • 江苏品牌网站设计如何做旅游休闲网站
  • 个人Z-Library镜像技术实现:从爬虫到部署
  • MySQL 索引深度指南:原理 · 实践 · 运维(适配 MySQL 8.4 LTS)
  • SVG修饰属性
  • Labelme格式转yolo格式
  • react的生命周期
  • 保险行业网站模板东莞阳光网站投诉平台
  • Mychem在Ubuntu 24.04 平台上的编译与配置
  • 自定义部署Chrony同步时间
  • 力扣热题100道之73矩阵置零
  • 概述网站建设的流程网站模板之家
  • AI智能体编程的挑战有哪些?