【Java项目安全基石】登录认证实战:Session/Token/JWT用户校验机制深度解析


目录

1.前言

2.正文

2.1Cookie—Session机制

2.1.1核心原理图解:

2.1.2四步核心流程:

2.1.3存储架构对比

2.1.4集群部署方案(Spring Session + Redis)

2.2Token令牌

2.2.1核心原理图解:

2.2.2四步核心流程:

2.2.3安全架构设计

2.3JWT令牌验证

2.3.1核心原理图解:

2.3.2JWT结构

2.3.3安全风险与解决方案

2.3.4签名算法对比

2.4三种方案对比

2.4.1核心机制对比表

2.4.2安全性与控制力对比

2.4.3性能与扩展性对比

2.4.4开发复杂度对比

2.4.5典型应用场景推荐

3.小结


1.前言

登录认证是系统安全的门户,而会话的持续管理策略直接影响开发效率与系统健壮性。许多开发者在实践中常陷入困惑:

  • 为何Session在集群部署时突然失效?

  • Token与JWT看似相似,核心差异究竟在哪?

  • 如何避免常见的安全陷阱?

本文针对主流场景,从底层原理剖析Session、Token、JWT三大用户校验方案,结合Java代码实现与安全规范,详解其工作机制、适用边界及落地要点。无论您是构建传统Web应用还是前后端分离项目,均可获得可直接复用的实践方案。


插播一条消息~

🔍 十年经验淬炼 · 系统化AI学习平台推荐

系统化学习AI平台https://www.captainbed.cn/scy/

  • 📚 完整知识体系:从数学基础 → 工业级项目(人脸识别/自动驾驶/GANs),内容由浅入深
  • 💻 实战为王:每小节配套可运行代码案例(提供完整源码)
  • 🎯 零基础友好:用生活案例讲解算法,无需担心数学/编程基础

🚀 特别适合

  • 想系统补强AI知识的开发者
  • 转型人工智能领域的从业者
  • 需要项目经验的学生

2.正文

在正式讲解常见的登录验证方式,先看看无验证的登陆流程是怎样的。

核心逻辑:

致命缺陷:

1.零身份验证

  • 攻击者输入任意有效用户名(无需密码)即可登录他人账户。
  • 示例:输入 admin 直接获取管理员权限。

2.会话劫持风险

  • 未登录用户访问 /profile 接口导致空指针异常(无用户信息)。
  • 若会话ID被窃取(如XSS攻击),攻击者可直接复用会话。

3.越权操作

  • 用户A登录后,修改URL参数即可操作用户B的数据(如 /deleteUser?id=2)。

2.1Cookie—Session机制

2.1.1核心原理图解:

2.1.2四步核心流程:

  1. 会话创建阶段

    • 用户提交有效凭证(用户名+密码)

    • 服务端验证通过后:

      // Java Servlet示例
      HttpSession session = request.getSession(true); // 创建新会话
      session.setAttribute("user", userObject); // 存储用户对象
      session.setMaxInactiveInterval(30*60); // 设置30分钟超时
    • 生成唯一Session ID(如JSESSIONID)

  2. Cookie传递阶段

    • 服务端响应头包含:

      HTTP/1.1 200 OK
      Set-Cookie: JSESSIONID=5A8C3D9F1E7B2; Path=/; HttpOnly; Secure; SameSite=Lax
    • 关键属性:

      • HttpOnly:阻止JavaScript访问

      • Secure:仅HTTPS传输

      • SameSite:防御CSRF攻击

  3. 会话保持阶段

    • 客户端后续请求自动携带Cookie:

      GET /profile HTTP/1.1
      Cookie: JSESSIONID=5A8C3D9F1E7B2
    • 服务端校验流程:

      public boolean checkSession(HttpServletRequest request) {HttpSession session = request.getSession(false); // 不创建新会话if(session == null) {return false; // 会话不存在}User user = (User)session.getAttribute("user");return user != null; // 用户对象存在
      }
  4. 会话销毁阶段

    • 主动注销:

      session.invalidate(); // 立即销毁会话
    • 超时销毁(web.xml配置):

      <session-config><session-timeout>30</session-timeout> <!-- 单位:分钟 -->
      </session-config>

2.1.3存储架构对比

存储方式实现方案优点缺点
内存存储Web容器默认(Tomcat等)零配置、响应快单点故障、集群失效
Redis存储Spring Session + Redis分布式支持、高性能需额外中间件
数据库存储自定义Session表持久化可靠、数据完整性能低、需清理机制
文件存储序列化到文件系统简单易实现I/O瓶颈、扩展性差

2.1.4集群部署方案(Spring Session + Redis)

  1. 依赖配置

    <dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
    </dependency>
  2. 配置类

    @EnableRedisHttpSession 
    public class SessionConfig {@Beanpublic LettuceConnectionFactory connectionFactory() {return new LettuceConnectionFactory(); }
    }
  3. 会话存取原理


Cookie-Session机制在传统Web应用中保持不可替代地位,通过严格的会话管理策略和集群扩展方案,可构建安全可靠的用户认证体系。 

2.2Token令牌

2.2.1核心原理图解:

2.2.2四步核心流程:

1. Token生成阶段

// 生成强随机Token(示例)
public String generateToken() {// 使用SecureRandom保证加密强度SecureRandom random = new SecureRandom();byte[] bytes = new byte[32];random.nextBytes(bytes);return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}// 存储关联关系(Redis示例)
public void storeToken(String token, User user) {// 设置Token有效期(如2小时)redisTemplate.opsForValue().set("AUTH_TOKEN:" + token, user.getId(),2, TimeUnit.HOURS);
}

2. Token传递方式

传递方式

实现示例

适用场景

Header传递

Authorization: Bearer xyz

前后端分离项目(主流)

URL参数

/api/data?token=xyz

临时调试(不安全)

POST Body

{ "token": "xyz" }

特殊接口场景

Cookie存储

Set-Cookie: token=xyz

兼容传统Web应用

3. 服务端验证流程

// Token验证拦截器
public class TokenInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {// 1. 从Header获取TokenString token = request.getHeader("Authorization");if(token == null || !token.startsWith("Bearer ")) {response.setStatus(401);return false;}token = token.substring(7);// 2. 查询Redis验证String userId = redisTemplate.opsForValue().get("AUTH_TOKEN:" + token);if(userId == null) {response.setStatus(401);return false;}// 3. 加载用户数据User user = userService.findById(userId);if(user == null) {response.setStatus(401);return false;}// 4. 设置安全上下文SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()));return true;}
}

4. Token注销机制

// 主动注销
@PostMapping("/logout")
public ResponseEntity logout(@RequestHeader("Authorization") String token) {token = token.replace("Bearer ", "");redisTemplate.delete("AUTH_TOKEN:" + token);return ResponseEntity.ok().build();
}// 自动过期(依赖Redis TTL)
// 可通过定时任务清理过期Token

2.2.3安全架构设计

1. 防御令牌劫持

攻击类型防御措施实现方案
XSS攻击HttpOnly Cookie存储Set-Cookie: token=xyz; HttpOnly
中间人攻击强制HTTPS传输服务端校验请求协议
CSRF攻击校验Origin头+CORS策略response.setHeader("Access-Control-Allow-Origin", "trusted.com")

2. 令牌绑定策略

// 设备指纹绑定
public String generateDeviceFingerprint(HttpServletRequest req) {String ip = req.getRemoteAddr();String userAgent = req.getHeader("User-Agent");return DigestUtils.sha256Hex(ip + userAgent);
}// 存储时绑定
redisTemplate.opsForValue().set("AUTH_TOKEN:" + token, user.getId() + "|" + deviceFingerprint, 2, TimeUnit.HOURS
);// 验证时检查
String[] parts = storedValue.split("\\|");
if(!parts[1].equals(currentDeviceFingerprint)) {// 异常设备访问,强制注销redisTemplate.delete("AUTH_TOKEN:" + token);return false;
}

Token机制为现代分布式架构提供了灵活的身份验证方案。通过严格的密钥管理、传输加密和存储安全措施,可构建高性能、可扩展的认证体系,特别适合API驱动的前后端分离应用。

2.3JWT令牌验证

2.3.1核心原理图解:


2.3.2JWT结构

JWT由三部分组成,以点分隔:Header.Payload.Signature

1. Header(头部)

{"alg": "HS256",   // 签名算法(HS256/RSA等)"typ": "JWT"      // 令牌类型
}
  • Base64Url编码后形成第一部分

2. Payload(载荷)

{"sub": "1234567890",      // 标准声明(subject)"name": "John Doe",       // 自定义声明"iat": 1516239022,        // 签发时间(issued at)"exp": 1516239322         // 过期时间(expiration)
}

标准声明字段:

字段全称说明
issIssuer签发者
subSubject主题(用户ID)
audAudience接收方
expExpiration Time过期时间(时间戳)
nbfNot Before生效时间(时间戳)
iatIssued At签发时间(时间戳)
jtiJWT ID唯一标识(防重放)

3. Signature(签名)

// 伪代码示例
signature = HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secretKey
)
  • 防止数据篡改的核心保障

  • 算法可选:HS256(对称)/ RS256(非对称)


代码实现:

1. 生成JWT

// 依赖
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtime 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtime 'io.jsonwebtoken:jjwt-jackson:0.11.5'// 生成代码
String secretKey = "your-256-bit-secret"; // 实际应使用安全随机生成String jwt = Jwts.builder().setSubject("user123")                 // 用户标识.claim("name", "John Doe")             // 自定义声明.claim("role", "ADMIN").setIssuedAt(new Date())               // 签发时间.setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1小时过期.signWith(SignatureAlgorithm.HS256, secretKey) // 签名算法.compact();

2. 验证JWT

public boolean validateToken(String jwt) {try {Claims claims = Jwts.parserBuilder().setSigningKey(secretKey)   // 设置密钥.build().parseClaimsJws(jwt)        // 解析并验证签名.getBody();// 手动校验过期时间(库自动校验exp,此处演示逻辑)Date expiration = claims.getExpiration();if(expiration.before(new Date())) {throw new ExpiredJwtException(null, claims, "Token expired");}// 获取用户信息String username = claims.getSubject();String role = claims.get("role", String.class);return true;} catch (JwtException e) {// 处理各种异常:签名无效/过期/格式错误等return false;}
}

2.3.3安全风险与解决方案

1. 令牌泄露风险

  • 问题:JWT一旦泄露,在有效期内可被滥用

  • 解决方案

    // 短有效期Access Token + 长有效期Refresh Token
    String accessToken = generateToken(30 * 60); // 30分钟
    String refreshToken = generateToken(7 * 24 * 60 * 60); // 7天// 服务端存储Refresh Token(Redis)
    redisTemplate.opsForValue().set("REFRESH:" + userId, refreshToken, 7, TimeUnit.DAYS
    );

2. 无法即时注销

  • 问题:服务端无法主动使JWT失效

  • 解决方案

    // 令牌黑名单(短期)
    @PostMapping("/logout")
    public void logout(@RequestHeader("Authorization") String token) {token = token.replace("Bearer ", "");long exp = getExpirationFromToken(token); // 从JWT提取过期时间long ttl = exp - System.currentTimeMillis() / 1000;if(ttl > 0) {// 将未过期的Token加入黑名单redisTemplate.opsForValue().set("BLACKLIST:" + token, "revoked", ttl, TimeUnit.SECONDS);}
    }// 验证时检查黑名单
    if(redisTemplate.hasKey("BLACKLIST:" + token)) {throw new JwtException("Token revoked");
    }
    

3. 敏感数据暴露

  • 问题:Payload数据可被Base64解码查看

  • 解决方案

    // 方案1:仅存储用户ID
    .setSubject("user123")// 方案2:使用JWE加密(JSON Web Encryption)
    String jwe = Jwts.builder().setSubject("user123").encryptWith(Key, keyAlg, encAlg) // 加密配置.compact();

2.3.4签名算法对比

算法类型代表算法密钥要求适用场景
对称HS256服务端保存相同密钥内部服务、单点部署
非对称RS256私钥签名/公钥验证多系统集成、开放平台
现代EdDSA高效椭圆曲线签名高安全性要求场景

JWT为分布式系统提供了无状态身份验证方案,通过标准化结构实现跨语言/跨平台支持。在实施时必须配合短有效期、HTTPS传输、黑名单机制等安全措施,才能发挥其最大价值。

2.4三种方案对比

2.4.1核心机制对比表

对比维度Session-Cookie自定义TokenJWT
工作原理服务端存储会话状态
客户端存Session ID
服务端存储Token-用户映射
客户端存Token
无状态令牌
自包含签名验证
状态管理有状态(服务端存储)有状态(服务端存储)无状态(服务端不存储)
数据结构会话ID(通常128bit)随机字符串(通常32-64字节)结构化JSON(Header.Payload.Signature)
客户端存储Cookie(自动管理)LocalStorage/手动管理LocalStorage/手动管理
传输方式自动Cookie头手动Authorization头手动Authorization头
典型应用场景传统Web应用(JSP/Thymeleaf)前后端分离API服务微服务/跨域认证/SSO

2.4.2安全性与控制力对比

安全特性Session-Cookie自定义TokenJWT
CSRF防护❌ 需额外Anti-CSRF Token✅ 天然免疫✅ 天然免疫
XSS防护✅ HttpOnly Cookie❌ LocalStorage易受XSS攻击❌ LocalStorage易受XSS攻击
令牌泄露影响中(会话可即时终止)中(可删除服务端Token)(有效期无法提前终止)
数据暴露风险低(仅ID在客户端)低(仅标识符在客户端)中高(Payload可解码查看)
即时注销能力✅ session.invalidate()✅ 删除Redis记录❌ 需额外黑名单机制
防重放攻击❌ 需额外措施✅ 绑定设备指纹✅ JTI声明+短期有效期

2.4.3性能与扩展性对比

性能指标Session-Cookie自定义TokenJWT
服务端开销会话存储查询(内存/Redis)Token存储查询(Redis)仅签名验证(无存储查询)
网络开销低(仅传Session ID)中(传完整Token)高(传完整JWT,体积最大)
集群扩展需Session共享(如Redis)需Token存储共享完美支持(无状态设计)
跨域支持❌ 需复杂CORS配置✅ 简单CORS配置✅ 简单CORS配置
移动端适配困难(Cookie管理问题)优秀优秀
第三方集成困难中等优秀(标准化格式)

2.4.4开发复杂度对比

开发环节Session-Cookie自定义TokenJWT
服务端实现简单(框架原生支持)中等(需自建验证逻辑)复杂(密钥管理/黑名单/Refresh机制)
前端集成零配置(浏览器自动管理)手动存储/携带Token手动存储/携带JWT
分布式会话复杂(需Spring Session等)简单(Redis直连)无需实现
调试难度低(Cookie可见)中(需查看网络请求)高(需解析JWT内容)
标准规范RFC 7519标准

2.4.5典型应用场景推荐

场景推荐方案原因说明
传统企业OA系统Session-Cookie内部网络环境安全,需严格会话控制,多页面跳转体验流畅
电商平台(前后端分离)自定义Token需兼顾API性能和移动端支持,高频查询需要快速验证
微服务架构JWT服务间无状态通信,避免会话共享瓶颈,网关统一认证
第三方开放平台JWT + OAuth2标准化令牌格式,合作伙伴系统可自主验证
高安全金融系统Session + 双因素认证需要即时会话终止能力,配合生物识别等强认证手段
物联网设备认证JWT(RS256)设备资源有限,非对称签名降低服务端压力,长期有效减少验证频率

3.小结

用户校验机制的选择本质是安全性、扩展性与开发成本的三角博弈

  1. 传统Session方案在服务端强状态控制场景仍具优势,但需通过Spring Session+Redis解决分布式一致性痛点;

  2. 自定义Token以服务端存储换取架构灵活性,是RESTful API服务的均衡之选;

  3. JWT的无状态特性天然契合微服务,但必须通过“短时效Access Token+服务端管控的Refresh Token”组合弥补注销缺陷;

无论何种方案,HTTPS传输、敏感数据脱敏、凭证安全存储是必须坚守的底线。技术决策应始于架构诉求,终于安全实践,方能在业务迭代中构建稳固的认证基石。

今天的分享到这里就结束了,喜欢的小伙伴点点赞点点关注,你的支持就是对我最大的鼓励,大家加油!

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

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

相关文章

融合优势:SIP 广播对讲联动华为会议 全场景沟通响应提速​

SIP 广播对讲与华为视频会议融合解决方案&#xff0c;是基于 SIP 协议将广播对讲系统与华为视频会议系统进行整合&#xff0c;实现通信资源共享与业务流程联动&#xff0c;可提升应急响应效率与沟通协作能力。融合原理&#xff1a;SIP 是一种基于文本的应用层协议&#xff0c;具…

Milvus Dify 学习笔记

目录 docker方式&#xff1a; 模式一&#xff1a;Milvus Lite linux docker方式&#xff1a; 下载yml文件&#xff0c; https://github.com/milvus-io/milvus/releases docker启动&#xff1a; docker compose up -d from pymilvus import connections connections.conne…

汽车ECU控制器通信架构

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…

【Linux】基本指令(入门篇)(上)

目录 前言 1.目录操作指令 1.1指令 1.2理论 1.2.1文件 1.2.2目录与路径 2.文件操作指令 2.1指令 2.2理论 2.2.1输出与输入 2.2.2一切皆文件 前言 这是Linux学习下的第一篇文章&#xff0c;后续Linux的学习也会持续更新分享。 Linux的基本指令是使用Linux操作系统的基础…

正向代理与反向代理理解

问&#xff1a; 应用a请求ng&#xff0c;然后ng根据不同请求路径将请求转发到不同的服务器&#xff0c;对于应用a来说这个ng是正向代理角色还是反向代理呢&#xff1f; 答&#xff1a; 在这个场景中&#xff0c;Nginx 扮演的是反向代理的角色&#xff0c;而不是正向代理。以下是…

【Kafka】深入理解 Kafka MirrorMaker2 - 实战篇

文章目录一、把“家伙事儿”都备齐二、部署其实很简单三、配置 MirrorMaker2四、修改启动脚本五、集群启动与验证六、这集群“结实”吗&#xff1f;聊聊它的高可用它没有“大脑”&#xff0c;但活得很好极限测试&#xff1a;干掉两个节点会怎样&#xff1f;写在最后最近在跟 Ka…

借助AI学习开源代码git0.7之四update-cache

借助AI学习开源代码git0.7之四update-cache update-cache.c 主要负责对索引&#xff08;index&#xff09;&#xff0c;也即缓存&#xff08;cache&#xff09;&#xff0c;进行增、删、改操作。现在的高层命令 git add 的部分核心功能就是由这个代码实现的。 核心功能 该程序的…

【48】MFC入门到精通——MFC 文件读写总结 CFile、CStdioFile、CFileDialog

文章目录1 打开文件1.2 打开文件模式总结2 常用函数2.1 写文件2.2 读文件2.3 获取文件长度3. 文件打开读写实力3.1 写文件 覆盖写3.2 文尾追加写3.3 换行写4 文件对话框 CFileDialog4.2 文件对话框实例5 CStdioFile 类 读写CStingMFC提供了一个文件操作的基类CFile&#xff0c;…

Leetcode 124. 二叉树中的最大路径和

递归/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode…

MTSC2025参会感悟:手工测试用例的智能化生成

目录 一、测试用例生成的时代困境与 AI 机遇 1.1 传统手工测试用例的固有痛点 1.2 AI 时代的测试新挑战 1.3 智能化转型的机遇窗口 二、智能用例生成的核心特性与产品功能 2.1 核心特性解析 2.2 四大核心产品功能 功能一&#xff1a;基于 PRD 理解的一键生成用例 功能二…

后台管理系统登录模块(双token的实现思路)

最近在写后台管理&#xff0c;这里分享一下我的登录模块的实现&#xff0c;我是使用reacttypescript实现的&#xff0c;主要是登录的逻辑和双token的处理方式&#xff0c;请求接口的二次封装aixos1.首先我们需要渲染登录界面的窗口&#xff0c;这个很简单就不详细讲解了&#x…

第十四讲 | AVL树实现

AVL树实现一、AVL的概念二、AVL树的实现1、AVL树的结构2、AVL树的插入&#xff08;1&#xff09;、AVL树插入一个值的大概过程&#xff08;2&#xff09;、平衡因子更新更新原则更新停止条件插入结点及更新平衡因子的代码实现3、旋转&#xff08;1&#xff09;、旋转的原则&…

《P3398 仓鼠找 sugar》

题目描述小仓鼠的和他的基&#xff08;mei&#xff09;友&#xff08;zi&#xff09;sugar 住在地下洞穴中&#xff0c;每个节点的编号为 1∼n。地下洞穴是一个树形结构。这一天小仓鼠打算从从他的卧室&#xff08;a&#xff09;到餐厅&#xff08;b&#xff09;&#xff0c;而…

锤子助手插件功能六:启用拦截消息撤回

锤子助手插件功能六&#xff1a;启用拦截消息撤回锤子助手插件功能六&#xff1a;启用拦截消息撤回&#x1f6e1;️ 插件简介 拦截撤回消息&#xff0c;信息不再消失&#x1f527; 功能说明⚠️ 使用风险与注意事项&#x1f3af; 适合人群❤️ 结语锤子助手插件功能六&#xf…

深度解析:基于EasyX的C++黑白棋AI实现 | 算法核心+图形化实战

摘要 本文详解C黑白棋AI实现&#xff0c;使用EasyX图形库打造完整人机对战系统。涵盖&#xff1a; 递归搜索算法&#xff08;动态规划优化&#xff09; 棋盘状态评估函数设计 图形界面与音效集成 胜负判定与用户交互 附完整可运行代码资源文件&#xff0c;提供AI难度调节方案…

树同构(Tree Isomorphism)

树同构&#xff08;Tree Isomorphism&#xff09;​​ 是图论中的一个经典问题&#xff0c;主要研究两棵树在结构上是否“相同”或“等价”&#xff0c;即是否存在一种节点的一一对应关系&#xff0c;使得两棵树的结构完全一致&#xff08;不考虑节点的具体标签或位置&#xff…

分享如何在保证画质的前提下缩小视频体积实用方案

大文件在通过互联网分享或上传时会遇到很多限制&#xff0c;比如电子邮件附件大小限制、社交媒体平台的文件大小要求等。压缩后的视频文件更小&#xff0c;更容易上传到网络、发送给他人或共享在社交平台上。它是一款无需安装的视频压缩工具&#xff0c;解压后直接运行&#xf…

SpringBoot 统一功能处理(拦截器、@ControllerAdvice、Spring AOP)

文章目录拦截器快速入门拦截器详解拦截路径拦截器执行流程全局控制器增强机制(ControllerAdvice)统一数据返回格式&#xff08;ControllerAdvice ResponseBodyAdvice&#xff09;​​全局异常处理机制​​&#xff08;ControllerAdvice ExceptionHandler&#xff09;全局数据…

建筑墙壁损伤缺陷分割数据集labelme格式7820张20类别

数据集格式&#xff1a;labelme格式(不包含mask文件&#xff0c;仅仅包含jpg图片和对应的json文件)图片数量(jpg文件个数)&#xff1a;7820标注数量(json文件个数)&#xff1a;7820标注类别数&#xff1a;20标注类别名称:["Graffiti","Bearing","Wets…

图书管理软件iOS(iPhone)

图书管理软件iOS(iPhone)开发进度表2025/07/19图书管理软件开发开始一&#xff1a;图书管理软件开发iOS&#xff08;iPhone&#xff09;