基于SpringBoot的Jwt认证以及密码aes加密解密技术

目录

前言

1.SpringBoot项目的创建

2.相关技术

3.项目架构

4.项目关键代码

5.项目最终的运行效果     

​编辑

6.PostMan测试接口结果 


前言

        学习了SpringBoot之后,才觉得SpringBoot真的很方便,相比传统的SSH,SSM,SpringBoot无疑是更轻量级的框架开发,约定大于配置的思想,使得项目的搭建更加简单。

1.SpringBoot项目的创建

        可以通过https://start.spring.io/ ,这个网址创建一个简单的SpringBoot项目,然后修改成适配当前编译器的SpringBoot版本。

2.相关技术

        SpringBoot:项目的核心框架

        MybatisPlus:一个半自动的ORM框架,用于操作数据库的组件

        JWT:用于登录身份校验

        MYSQL:用于存储业务数据

3.项目架构

  common层:用于存放公共的方法,以及公共类,公共实体

 config层:存放相应的配置类,如:MybatisPlus配置类,SpringMVC的配置类

controller层:存放给前端提供接口请求的类,也称之为视图层。

entity层:一般存放与数据库关联的实体类,属性和数据库的字段相对相应。

interpretor层:存放拦截器,如token拦截

mapper层:存放抽象的数据库接口,如Mybatis的一些接口映射,以及方法映射。

dao层:数据库访问层

service层:业务逻辑层,主要存放业务逻辑代码,供controller层调用。

配置文件 pom.xml:存放Java类库的依赖,如SpringBoot起步依赖,Mysql类库依赖、MybatisPlus类库依赖

4.项目关键代码

        JWT组件: 用于生成token和解析token的组件

package com.example.harmonybackend.utils;import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import java.util.Date;
import java.util.Map;
import java.util.UUID;/*** token生成组件类*/
@Component
@Slf4j
public class JWTUtils {private static String secret = "123456";public static String getToken(Map<String,Object> claims){// JWT的签发时间long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);// 指定签名的时候使用的签名算法SignatureAlgorithm signatureAlgotithm = SignatureAlgorithm.HS256;long expMillis = nowMillis +  8*60L;        // 默认设置8小时过期Date expirationDate = new Date(expMillis);String token = Jwts.builder()               // 创建jwt builder.setClaims(claims)                      // 必须放最前面,不然后面设置的东西都会没有:如setExpiration会没有时间.setId(UUID.randomUUID().toString())     // jwt唯一标识.setIssuedAt(now)                                    // 签发时间.setExpiration(expirationDate)                 // 过期时间.signWith(signatureAlgotithm, secret)          // 设置签名实用的签名算法和使用的密钥.compact();return token;}/*** 解析token的方法* @param token* @return*/public static Claims  parseToken(String token){String msg = null;try{Claims claims = Jwts.parser().setAllowedClockSkewSeconds(480) // 允许8小时的偏移.setSigningKey(secret)                         // 设置签名密钥.parseClaimsJws(token).getBody(); // 设置需要解析的JWTreturn claims;}catch (SignatureException se) {msg = "密钥错误";log.error(msg, se);throw new RuntimeException(msg);}catch (MalformedJwtException me) {msg = "密钥算法或者密钥转换错误";log.error(msg, me);throw new RuntimeException(msg);}catch (MissingClaimException mce) {msg = "密钥缺少校验数据";log.error(msg, mce);throw new RuntimeException(msg);}catch (ExpiredJwtException mce) {msg = "密钥已过期";log.error(msg, mce);throw new RuntimeException(msg);}catch (JwtException jwte) {msg = "密钥解析错误";log.error(msg, jwte);throw new RuntimeException(msg);}}}

  AES加密解密组件:用于登录密码的加密与解密

package com.example.harmonybackend.utils;import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;public class AesEncryption {private static final String ALGORITHM = "AES";private static final int KEY_SIZE = 128; // 或者 192, 256public static SecretKey generateKey() throws Exception {KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);keyGen.init(KEY_SIZE);return keyGen.generateKey();}public static String encrypt(String plainText, SecretKey secretKey) throws Exception {Cipher cipher = Cipher.getInstance(ALGORITHM);cipher.init(Cipher.ENCRYPT_MODE, secretKey);byte[] encryptedBytes = cipher.doFinal(plainText.getBytes());return Base64.getEncoder().encodeToString(encryptedBytes);}public static String decrypt(String encryptedText, SecretKey secretKey) throws Exception {Cipher cipher = Cipher.getInstance(ALGORITHM);cipher.init(Cipher.DECRYPT_MODE, secretKey);byte[] decodedBytes = Base64.getDecoder().decode(encryptedText);byte[] decryptedBytes = cipher.doFinal(decodedBytes);return new String(decryptedBytes);}public static void main(String args[]) throws Exception {// 仅需执行一次以生成并保存密钥/* SecretKey secretKey = AesEncryption.generateKey();byte[] encodedKey = secretKey.getEncoded();String base64EncodedKey11 = Base64.getEncoder().encodeToString(encodedKey);System.out.println("base64EncodedKey11=="+base64EncodedKey11);*/// 假设你已经将密钥保存为Base64字符串形式在某个地方String base64EncodedKey = "SGRieOb4PY1HbSXOnmQohw==";byte[] decodedKey = Base64.getDecoder().decode(base64EncodedKey);SecretKey originalKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES");String encryptPass = encrypt("admin",originalKey);System.out.println(encryptPass);System.out.println("解密:"+ decrypt(encryptPass,originalKey));}
}

          拦截器类JwtInterpretor:用于Token的拦截

package com.example.harmonybackend.interpretor;import com.example.harmonybackend.utils.JWTUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** SpringMVC拦截器*/
public class JwtInterpretor  implements HandlerInterceptor {private final List<String> excludePaths = Arrays.asList("/user/login", "/public/**");@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String uri = request.getRequestURI();for (String path : excludePaths) {if (new AntPathMatcher().match(path, uri)) {return true;}}Map<String, Object> map = new HashMap<>();// 获取请求头中令牌String token = request.getHeader("login-token");if(!StringUtils.isEmpty(token)) {try {Claims claims = JWTUtils.parseToken(token); // 如果找到令牌就使用 JWTUtils.parseJwt() 方法解析令牌return true;                                     // 解析成功,即令牌有效,返回true} catch (RuntimeException e) {          // 如果解析失败,会捕获 RuntimeException 异常e.printStackTrace();map.put("msg", e.getMessage());  // map.put("msg","密钥错误!");}}map.put("message","token为null,必须携带token");map.put("state",false);             // 设置状态// 将map 转为 jsonString json = new ObjectMapper().writeValueAsString(map);response.setContentType("application/json;charset=UTF-8");response.getWriter().println(json);return false;}
}

        SpringMVC的配置:  基于方法的拦截器,通过拦截控制接口的访问。

package com.example.harmonybackend.interpretor;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** mvc拦截器*/
@Configuration
public class MVCConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {String[] patterns = new String[] {"/user/login/**","/user/register/**","/*.html","/css/**","/js/**","/images/**","/layui/**"};    // 添加不拦截的方法//添加jwt拦截器registry.addInterceptor(new JwtInterpretor()).excludePathPatterns(patterns)// 其他接口token验证;.addPathPatterns("/**").order(0);        // 不进行token验证}}

                   注册和登录的业务逻辑类UserService:用于登录验证,以及注册时,密码的加密解密

package com.example.harmonybackend.service.impl;import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.harmonybackend.common.ApiResponse;
import com.example.harmonybackend.config.PasswordConfig;
import com.example.harmonybackend.entity.UserEntity;
import com.example.harmonybackend.mapper.UserMapper;
import com.example.harmonybackend.myemnu.ApiError;
import com.example.harmonybackend.request.RegisterRequstParam;
import com.example.harmonybackend.request.UserRequestParam;
import com.example.harmonybackend.service.IUserService;
import com.example.harmonybackend.utils.AesEncryption;
import com.example.harmonybackend.utils.JWTUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements IUserService {@Resourceprivate PasswordConfig passwordConfig;@Overridepublic ApiResponse login(UserRequestParam requestParam) {QueryWrapper<UserEntity> param = new QueryWrapper<>();ApiResponse apiResponse = new ApiResponse();byte[] decodedKey = Base64.getDecoder().decode(passwordConfig.getBase64EncodedKey());SecretKey originalKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES");try {String encryptPass = AesEncryption.encrypt(requestParam.getPassword(),originalKey);param.eq("username",requestParam.getUsername());param.eq("password",encryptPass);UserEntity userEntity =  getOne(param);if (null == userEntity){apiResponse.setCode(ApiError.INTERNAL_ERROR.getCode());apiResponse.setMessage("账号不存在或密码错误");return apiResponse;}}catch (Exception e){e.printStackTrace();apiResponse.setCode(ApiError.INTERNAL_ERROR.getCode());apiResponse.setMessage("账号不存在或密码错误");return apiResponse;}apiResponse.setCode(ApiError.SUCCESS.getCode());apiResponse.setMessage(ApiError.SUCCESS.getMessage());Map<String,Object>resMap = new HashMap<>();resMap.put("username",requestParam.getUsername());String login_token  = JWTUtils.getToken(resMap);Map<String,Object>dataMap = new HashMap<>();dataMap.put("login-token",login_token);apiResponse.setData(dataMap);return apiResponse;}@Overridepublic ApiResponse register(RegisterRequstParam registerRequstParam) {UserEntity entity = new UserEntity();ApiResponse apiResponse = new ApiResponse();byte[] decodedKey = Base64.getDecoder().decode(passwordConfig.getBase64EncodedKey());SecretKey originalKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES");try {QueryWrapper<UserEntity>queryMap = new QueryWrapper<>();queryMap.eq("username",registerRequstParam.getUsername());UserEntity  existUser =  getOne(queryMap);if (null!=existUser){apiResponse.setCode(ApiError.INTERNAL_ERROR.getCode());apiResponse.setMessage("用户已存在,请勿重复注册!");return apiResponse;}String encryptPass = AesEncryption.encrypt(registerRequstParam.getPassword(),originalKey);registerRequstParam.setPassword(encryptPass);//对象拷贝BeanUtils.copyProperties(registerRequstParam ,entity);//保存数据save(entity);}catch (Exception e){e.printStackTrace();apiResponse.setCode(ApiError.INTERNAL_ERROR.getCode());apiResponse.setMessage("注册失败!");return apiResponse;}apiResponse.setCode(ApiError.SUCCESS.getCode());apiResponse.setMessage(ApiError.SUCCESS.getMessage());return apiResponse;}@Overridepublic ApiResponse<List<UserEntity>> findAll() {List<UserEntity> userEntityList= list();ApiResponse<List<UserEntity>>apiResponse = new ApiResponse<>();apiResponse.setCode(ApiError.SUCCESS.getCode());apiResponse.setMessage(ApiError.SUCCESS.getMessage());apiResponse.setData(userEntityList);return apiResponse;}}

        实体类

package com.example.harmonybackend.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;@Data
@TableName("sys_user")
public class UserEntity {@TableId(type = IdType.ASSIGN_ID)private Long id;//用户名private String username;//密码private String password;//岁数private Integer age;//邮箱private String email;//性别 0女 1 男private Integer gender;//真实姓名private String realName;@TableLogicprivate Integer deleted; // 逻辑删除字段}

                Mysql数据库脚本

/*Navicat Premium Data TransferSource Server         : localhost_3306Source Server Type    : MySQLSource Server Version : 80037Source Host           : localhost:3306Source Schema         : harmonydbTarget Server Type    : MySQLTarget Server Version : 80037File Encoding         : 65001Date: 27/12/2024 11:37:05
*/SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (`id` bigint(0) NOT NULL COMMENT 'id',`username` varchar(18) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '用户名',`password` varchar(32) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '密码',`age` int(0) NULL DEFAULT NULL COMMENT '年龄',`email` varchar(32) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '邮箱',`deleted` int(0) NULL DEFAULT 0 COMMENT '逻辑删除',`gender` int(0) NULL DEFAULT NULL COMMENT '性别(0女,1男)',`real_name` varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1235653235656, 'admin', 'Y8itXSytDD76om64lGZ1Wg==', 18, '123456789@qq.com', 0, NULL, NULL);
INSERT INTO `sys_user` VALUES (1872205908998721538, 'zhangsan', 'xoixyTmaBgbAwb4Ly2VnxQ==', 18, '1255656@qq.com', 0, NULL, '张三');
INSERT INTO `sys_user` VALUES (1872219115704414209, 'lisi', 'xoixyTmaBgbAwb4Ly2VnxQ==', 18, '1255656@qq.com', 0, NULL, '张三');
INSERT INTO `sys_user` VALUES (1872219905814818818, 'wangwu', 'xoixyTmaBgbAwb4Ly2VnxQ==', 18, '1255656@qq.com', 0, 0, '王五');
INSERT INTO `sys_user` VALUES (1872220108869464066, 'laoliu', 'SYhcFqjbKKqeJbC0qO5i0Q==', 18, '1255656@qq.com', 0, 0, '老六');SET FOREIGN_KEY_CHECKS = 1;

5.项目最终的运行效果     

6.PostMan测试接口结果 

        

   博文代码只有关键部分,如有需要,请前往下载完整版。此博文有助于初学者学习SpringBoot,你们的点赞和收藏是我前进的动力。

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

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

相关文章

Spark SQL DML语句

【图书介绍】《Spark SQL大数据分析快速上手》-CSDN博客 《Spark SQL大数据分析快速上手》【摘要 书评 试读】- 京东图书 Spark本地模式安装_spark3.2.2本地模式安装-CSDN博客 DML&#xff08;Data Manipulation Language&#xff0c;数据操作语言&#xff09;操作主要用来对…

线性直流电流

电阻网络的等效 等效是指被化简的电阻网络与等效电阻具有相同的 u-i 关系 (即端口方程)&#xff0c;从而用等效电阻代替电阻网络之后&#xff0c;不 改变其余部分的电压和电流。 串联等效&#xff1a; 并联等效&#xff1a; 星角变换 若这两个三端网络是等效的&#xff0c;从任…

B站推荐模型数据流的一致性架构

01 背景 推荐系统的模型&#xff0c;通过学习用户历史行为来达到个性化精准推荐的目的&#xff0c;因此模型训练依赖的样本数据&#xff0c;需要包括用户特征、服务端推荐的视频特征&#xff0c;以及用户在推荐视频上是否有一系列的消费行为。 推荐模型数据流&#xff0c;即为…

【LeetCode】839、相似字符串组

【LeetCode】839、相似字符串组 文章目录 一、并查集1.1 并查集 二、多语言解法 一、并查集 1.1 并查集 求共有几组, 联想到并查集, 即并查集有几个集合 字符串相似: 相差0个字符, 或2个字符 其中所有字符串长度都相同, 是比较方便处理的 // go var sets int var father […

官宣!低空经济司,挂牌成立!

近日&#xff0c;国家发展改革委网站“机关司局”栏目悄然更新&#xff0c;一个新设立的部门——低空经济发展司&#xff08;简称“低空司”&#xff09;正式进入公众视野。低空司的成立&#xff0c;无疑是对当前国家经济发展形势的深刻把握和前瞻布局。 低空经济是以各类低空飞…

不安全物联网的轻量级加密:综述

Abstract 本文综述了针对物联网&#xff08;IoT&#xff09;的轻量级加密解决方案。这项综述全面覆盖了从轻量级加密方案到不同类型分组密码的比较等多个方面。同时&#xff0c;还对硬件与软件解决方案之间的比较进行了讨论&#xff0c;并分析了当前最受信赖且研究最深入的分组…

【小程序】全局数据共享

目录 全局数据共享 1. 什么是全局数据共享 2. 小程序中的全局数据共享方案 全局数据共享 - MobX 1. 安装 MobX 相关的包 2. 创建 MobX 的 Store 实例 3. 将 Store 中的成员绑定到页面中 4. 在页面上使用 Store 中的成员 ​5. 将 Store 中的成员绑定到组件中 6. 在组件中…

自动化测试- 自动化测试模型

目录 自动化测试模型简介 1、线性模型 举例 测试页面html文件 测试脚本 2. 关键字驱动测试&#xff08;Keyword-Driven Testing&#xff09; 需测试内容 关键字驱动测试框架 创建测试用例文件 运行测试 3. 数据驱动测试&#xff08;Data-Driven Testing&#xff09; …

【GlobalMapper精品教程】091:根据指定字段融合图斑(字段值相同融合到一起)

文章目录 一、加载数据二、符号化三、融合图斑1. 根据图斑位置进行融合2. 根据指定字段四、注意事项一、加载数据 订阅专栏后,从私信中查收配套实验数据包,找到data091.rar,解压并加载,如下图所示: 属性表如下: 二、符号化 为了便于比对不同的融合结果,查看属性表根据…

strace工具使用

下载地址&#xff1a; https://github.com/strace/strace/releases/tag/v6.12 解压后执行以下命令 ./configure --hostarm-linux --prefix/home/wei/Code/strace/strace-6.12/out CC/home/wei/Code/firmware/prebuilts/host/gcc/gcc-arm-10.2-2020.11-x86_64-arm-none-linux…

图像处理-Ch2-空间域的图像增强

Ch2 空间域的图像增强 文章目录 Ch2 空间域的图像增强Background灰度变换函数(Gray-level Transformation)对数变换(Logarithmic)幂律变换(Power-Law)分段线性变换函数(Piecewise-Linear)对比度拉伸(Contrast-Stretching)灰度级分层(Gray-level Slicing) 直方图处理(Histogram …

Linux | Ubuntu零基础安装学习cURL文件传输工具

目录 介绍 检查安装包 下载安装 手册 介绍 ‌cURL是一个利用URL语法在命令行下工作的文件传输工具&#xff0c;首次发行于1997年‌‌12。cURL支持多种协议&#xff0c;包括FTP、FTPS、HTTP、HTTPS、TFTP、SFTP、Gopher、SCP、Telnet、DICT、FILE、LDAP、LDAPS、IMAP、POP3…

cesium通过经纬度获取3dtiles 得feature信息

找到这里3dtiles的两种访问方式&#xff1a; 1.1 3DTileContent#getFeature 这里涉及3DTile 数据结构&#xff0c;暂不了解3DTile 数据结构&#xff0c;因此暂不使用。 1.2 scene.pick 本次使用 scene表示虚拟场景中所有 3D 图形对象和状态的容器&#xff1b;scene中…

内置ALC的前置放大器D2538A/D3308

一、概述 D2538A/D3308是芯谷科技推出的带有ALC&#xff08;自动电平控制&#xff09;的前置音频放大器芯片&#xff0c;最初产品为单声道/立体声收录机及盒式录音机而开发&#xff0c;作为录音/回放的磁头放大器使用&#xff1b;由于产品的高增益、低噪声及ALC外部可调的特性&…

基于SSM的“快递管理系统”的设计与实现(源码+数据库+文档+PPT)

基于SSM的“快递管理系统”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SSM 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 登陆页面 注册页面 快递员页面 派单员订单管理页面 派单员订单添…

frp(s) 内网穿透 Liunx环境双端Docker部署

FRP(Fast Reverse Proxy)是一款高性能的反向代理应用,主要用于内网穿透、负载均衡和反向代理等多种场景。它能够将内网中的服务暴露给公网,实现远程访问。此外,FRP还可以用于接收类似GitHub或第三方提供的Webhook请求。在微服务架构中,FRP可以作为服务调用的反向代理,提…

STM32F103RCT6学习之五:ADC

1.ADC基础 ADC&#xff08;Analog-Digital Converter&#xff09;模拟-数字转换器ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量&#xff0c;建立模拟电路到数字电路的桥梁12位逐次逼近型ADC&#xff0c;1us转换时间 输入电压范围&#xff1a;0~3.3V&#xff…

实现类似gpt 打字效果

1. css的动画&#xff08;animation) css中实现动画有两种方式&#xff1a;transition过渡动画、 animation自定义动画。 具体的可以看MDN链接&#xff1a;https://developer.mozilla.org/zh-CN/docs/Web/CSS/animation 使用keyframes自定义关键帧动画并未其命名使用自定义动…

微软远程桌面APP怎么用

微软远程桌面&#xff08;Remote Desktop&#xff09;客户端&#xff08;RD Client&#xff09;是一款由微软开发的应用程序&#xff0c;允许用户通过网络连接远程访问和控制另一台计算机。同时&#xff0c;微软远程桌面RD Client支持多种设备和操作系统&#xff0c;包括Window…

Unity3d UGUI如何优雅的实现Web框架(Vue/Rect)类似数据绑定功能(含源码)

前言 Unity3d的UGUI系统与Web前端开发中常见的数据绑定和属性绑定机制有所不同。UGUI是一个相对简单和基础的UI系统&#xff0c;并不内置像Web前端&#xff08;例如 Vue.js或React中&#xff09;那样的双向数据绑定或自动更新UI的机制。UGUI是一种比较传统的 UI 系统&#xff…