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

前后端分离项目接口权限检查方案

基于handleMethod写的一款分级式接口权限检查方案。
权限自动同步机制(启动更新,页面不提供增删改):

public class AuthorizationMappingGenerateExecutor implements EasyApplicationRunner {@Autowiredprivate AuthorizationMappingMapper authorizationMappingMapper;@Autowiredprivate RequestMappingHandlerMapping mapping;@Autowiredprivate SupportSecurityProperties securityProperties;// 启动时执行一次,随便用用作个排序。没安全问题int i = 0;@Overridepublic void doBusiness() {Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = mapping.getHandlerMethods();Collection<HandlerMethod> handlerMethods = handlerMethodMap.values();// 表中已存在的codeList<String> existsCodes = authorizationMappingMapper.selectProperties(Wrappers.lambdaQuery(), AuthorizationMapping::getCode);// 排重setSet<String> currentExistsCodes = new HashSet<>();// 配置的级别MappingLevel mappingLevel = securityProperties.getMappingLevel();// 添加的权限List<AuthorizationMapping> insertMappings = new ArrayList<>();// 需要更新的权限List<AuthorizationMapping> updateMappings = new ArrayList<>();for (HandlerMethod handlerMethod : handlerMethods) {Class<?> beanType = handlerMethod.getBeanType();String name = beanType.getName();// 只取本系统内的接口if (!name.contains("xxxx")) {continue;}AuthorizationMapping moduleMapping = buildModuleMapping(handlerMethod);int type = chooseType(existsCodes, currentExistsCodes, moduleMapping);if (type == 1) {insertMappings.add(moduleMapping);} else if (type == 2) {updateMappings.add(moduleMapping);}// 只开启了一级权限检查if (mappingLevel.is(MappingLevel.MODULE)) {continue;}AuthorizationMapping controllerMapping = buildControllerMapping(moduleMapping.getCode(), handlerMethod);int controllerType = chooseType(existsCodes, currentExistsCodes, controllerMapping);if (controllerType == 1) {insertMappings.add(controllerMapping);} else if (controllerType == 2) {updateMappings.add(controllerMapping);}// 只开启了二级权限检查if (mappingLevel.is(MappingLevel.CONTROLLER)) {continue;}// 方法级的权限检查AuthorizationMapping methodMapping = buildMethodMapping(controllerMapping.getCode(), handlerMethod);int methodType = chooseType(existsCodes, currentExistsCodes, methodMapping);if (methodType == 1) {insertMappings.add(methodMapping);} else if (methodType == 2) {updateMappings.add(methodMapping);}}// 批量插入authorizationMappingMapper.insertSplitBatch(insertMappings, 200);// 配置是否更新,一条条更新太慢,影响启动速度。if (securityProperties.getEnableMappingUpdate()) {updateMappings.forEach(authorizationMappingMapper::updateById);}// 清掉子级,不占表空间。开放的话重启构建即可。code不会变LambdaQueryWrapper<AuthorizationMapping> wrapper = Wrappers.lambdaQuery();if (mappingLevel.is(MappingLevel.MODULE)) {wrapper.in(AuthorizationMapping::getLevel, ZYListUtils.toList(MappingLevel.CONTROLLER.level(), MappingLevel.METHOD.level()));} else if (mappingLevel.is(MappingLevel.CONTROLLER)) {wrapper.eq(AuthorizationMapping::getLevel, MappingLevel.METHOD.level());}authorizationMappingMapper.delete(wrapper);}private int chooseType(List<String> existsCodes, Set<String> currentExistsCodes, AuthorizationMapping methodMapping) {String code = methodMapping.getCode();if (currentExistsCodes.contains(code)) {return 0;}// 去重currentExistsCodes.add(methodMapping.getCode());// 更新or插入if (!existsCodes.contains(code)) {return 1;} else {return 2;}}// 构建模块权限private AuthorizationMapping buildModuleMapping(HandlerMethod handlerMethod) {Class<?> beanType = handlerMethod.getBeanType();Package aPackage = beanType.getPackage();AuthorizationMapping packageMapping = buildBaseMapping(MappingLevel.MODULE);packageMapping.setMapping("");packageMapping.setParentCode(ZYTreeUtils.TREE_ROOT_ID);String moduleCode = PermissionMappingGenerator.generateModuleKey(handlerMethod);packageMapping.setCode(moduleCode);String packageName = aPackage.getName();packageMapping.setFileLocal(packageName);String packageChinaName = try2getPackageChinaName(handlerMethod);packageMapping.setName(null != packageChinaName ? packageChinaName : packageName);return packageMapping;}// 构建controller级权限private AuthorizationMapping buildControllerMapping(String parentCode, HandlerMethod handlerMethod) {AuthorizationMapping packageMapping = buildBaseMapping(MappingLevel.CONTROLLER);packageMapping.setMapping("");packageMapping.setParentCode(parentCode);String controllerCode = PermissionMappingGenerator.generateControllerKey(handlerMethod);packageMapping.setCode(controllerCode);Class<?> beanType = handlerMethod.getBeanType();packageMapping.setFileLocal(beanType.getName());Theme theme = beanType.getAnnotation(Theme.class);packageMapping.setName(null != theme ? theme.value() : beanType.getSimpleName());return packageMapping;}// 构建方法级权限private AuthorizationMapping buildMethodMapping(String parentCode, HandlerMethod handlerMethod) {String methodCode = PermissionMappingGenerator.generateMethodKey(handlerMethod);AuthorizationMapping packageMapping = buildBaseMapping(MappingLevel.METHOD);packageMapping.setCode(methodCode);packageMapping.setParentCode(parentCode);Class<?> beanType = handlerMethod.getBeanType();Method method = handlerMethod.getMethod();packageMapping.setFileLocal(beanType.getName() + "." + method.getName());packageMapping.setMapping(spellMapping(method, beanType));packageMapping.setName(getMappingName(handlerMethod));return packageMapping;}private AuthorizationMapping buildBaseMapping(MappingLevel level) {AuthorizationMapping packageMapping = new AuthorizationMapping();packageMapping.setCreateDate(System.currentTimeMillis());packageMapping.setIsMiniAuth(0);packageMapping.setSort(++i);packageMapping.setLevel(level.level());packageMapping.setRemarks(level.getMappingName());return packageMapping;}private String getMappingName(HandlerMethod handlerMethod) {SystemLog systemLog = handlerMethod.getMethodAnnotation(SystemLog.class);if (null != systemLog) {return systemLog.value();}return handlerMethod.getMethod().getName();}private String spellMapping(Method method, Class<?> beanType) {StringBuilder path = new StringBuilder();RequestMapping parentRequestMapping = beanType.getAnnotation(RequestMapping.class);if (null != parentRequestMapping) {String[] value = parentRequestMapping.value();if (value.length > 0) {path.append(value[0]);}}RequestMapping methodRequestMapping = method.getAnnotation(RequestMapping.class);GetMapping methodGetRequestMapping = method.getAnnotation(GetMapping.class);PostMapping methodPostRequestMapping = method.getAnnotation(PostMapping.class);if (null != methodRequestMapping) {String[] value = methodRequestMapping.value();if (value.length > 0) {path.append(value[0]);}} else if (null != methodGetRequestMapping) {String[] value = methodGetRequestMapping.value();if (value.length > 0) {path.append(value[0]);}} else if (null != methodPostRequestMapping) {String[] value = methodPostRequestMapping.value();if (value.length > 0) {path.append(value[0]);}}return path.toString();}private String try2getPackageChinaName(HandlerMethod handlerMethod) {Class<?> beanType = handlerMethod.getBeanType();Package aPackage = beanType.getPackage();Theme packageTheme = aPackage.getAnnotation(Theme.class);if (null != packageTheme) {return packageTheme.value();}return null;}
}

权限检查拦截器

public class AuthorizationMappingInterceptor implements EasyMvcInterceptor, InitializingBean {private final static Set<String> NO_NEED_CHECK_CODES = new HashSet<>();private final static Set<String> CHECK_CODES = new HashSet<>();@Autowiredprivate AuthorizationMappingMapper authorizationMappingMapper;@Autowiredprivate SupportSecurityProperties securityProperties;@Overridepublic boolean doBusiness(HandlerMethod handlerMethod, HttpServletRequest request, HttpServletResponse response) {// 开放接口if (ZYRequestUtils.isDirectApi()) {return true;}// 超管if (UserHelper.isSuperAdmin()) {return true;}// 需要跳过的权限if (isSkipMappingCheck(handlerMethod)) {return true;}// 查询需要的权限MappingLevel mappingLevel = securityProperties.getMappingLevel();String permissionCode = PermissionMappingGenerator.generateByLevel(handlerMethod, mappingLevel);// 缓存在存在,直接通过检查if (NO_NEED_CHECK_CODES.contains(permissionCode)) {return true;}// 该接口没有做权限限制,直接放行if (!CHECK_CODES.contains(permissionCode)) {NO_NEED_CHECK_CODES.add(permissionCode);return true;}// 用户权限信息LoginUser loginAreaUser = UserHelper.getLoginAreaUser();Set<String> mappingCodes = loginAreaUser.getMappingCodes();if (!hasPermission(permissionCode, mappingCodes)) {throw new PermissionException("您没有访问接口的权限");}return true;}private boolean hasPermission(String permissionCode, Set<String> mappingCodes) {return null != mappingCodes && mappingCodes.contains(permissionCode);}@Overridepublic void afterPropertiesSet() {// 根据权限级别查询需要检查的HandleMethod或controllerMappingLevel mappingLevel = securityProperties.getMappingLevel();LambdaQueryWrapper<AuthorizationMapping> wrapper = Wrappers.lambdaQuery();wrapper.eq(AuthorizationMapping::getLevel, mappingLevel.level());wrapper.eq(AuthorizationMapping::getIsMiniAuth, NO);wrapper.select(AuthorizationMapping::getCode);List<String> authorizationMappings = authorizationMappingMapper.selectProperties(wrapper, AuthorizationMapping::getCode);CHECK_CODES.addAll(authorizationMappings);}private boolean isSkipMappingCheck(HandlerMethod handlerMethod) {Class<?> beanType = handlerMethod.getBeanType();Package aPackage = beanType.getPackage();SkipMappingCheck packageSkip = aPackage.getAnnotation(SkipMappingCheck.class);if (null != packageSkip) {return true;}SkipMappingCheck classSkip = beanType.getAnnotation(SkipMappingCheck.class);if (null != classSkip) {return true;}Method method = handlerMethod.getMethod();SkipMappingCheck methodSkip = method.getAnnotation(SkipMappingCheck.class);return null != methodSkip;}
}
public enum MappingLevel implements Matcher {MODULE("模块"),CONTROLLER("控制类"),METHOD("接口方法");private String mappingName;public String level() {return this.name().toLowerCase();}@Overridepublic Object statusCode() {return level();}
}

code生成器

public class PermissionMappingGenerator {private final static Map<String, String> PACKAGE_KEY_CACHE = new HashMap<>();private final static Map<String, String> CONTROLLER_KEY_CACHE = new HashMap<>();private final static Map<String, String> METHOD_KEY_CACHE = new HashMap<>();public static String generateByLevel(HandlerMethod handlerMethod, MappingLevel mappingLevel) {switch (mappingLevel) {case METHOD:return generateMethodKey(handlerMethod);case CONTROLLER:return generateControllerKey(handlerMethod);case MODULE:return generateModuleKey(handlerMethod);default:throw new LocalException("不能确定的权限级别");}}public static String generateModuleKey(HandlerMethod handlerMethod) {Class<?> beanType = handlerMethod.getBeanType();Package aPackage = beanType.getPackage();String packageName = aPackage.getName();String cryptPackageCode = PACKAGE_KEY_CACHE.get(packageName);if (ZYStrUtils.isNull(cryptPackageCode)) {cryptPackageCode = ZYCryptUtils.encryptMD5(packageName).toLowerCase();PACKAGE_KEY_CACHE.put(packageName, cryptPackageCode);}return cryptPackageCode;}public static String generateControllerKey(HandlerMethod handlerMethod) {Class<?> beanType = handlerMethod.getBeanType();String className = beanType.getName();String cryptControllerCode = CONTROLLER_KEY_CACHE.get(className);if (ZYStrUtils.isNull(cryptControllerCode)) {cryptControllerCode = ZYCryptUtils.encryptMD5(className).toLowerCase();CONTROLLER_KEY_CACHE.put(className, cryptControllerCode);}return cryptControllerCode;}public static String generateMethodKey(HandlerMethod handlerMethod) {Class<?> beanType = handlerMethod.getBeanType();Method method = handlerMethod.getMethod();List<String> keySign = new ArrayList<>();keySign.add(beanType.getSimpleName());keySign.add(method.getName());Parameter[] parameters = method.getParameters();for (Parameter parameter : parameters) {keySign.add(parameter.getName());}String key = StrUtils.join(keySign, "#");String cryptKey = METHOD_KEY_CACHE.get(key);if (null == cryptKey) {cryptKey = CryptUtils.encryptMD5(key).toLowerCase();METHOD_KEY_CACHE.put(key, cryptKey);}return cryptKey;}
}
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SkipMappingCheck {
}

与传统的路径匹配接口权限检查相比,本方案采用HandleMethod信息与角角直接绑定。多了一个权限颗粒度划分。对减少接品权限数量及减少人工分配量有明显的优势。自动的权限生成工具省去了维护权限编号协删改查的人工操作。减去了大量人工。实际使用中,默认使用module级,维护量很少。较严情况下,使用controller级。严格安全等保要求的情况下。使用mothod级。方案选择很灵活。重启服务即可任意切换。可以使用SkipMappingCheck 注解对整个模块,整个controller进行跳过处理。

相关文章:

前后端分离项目接口权限检查方案

基于handleMethod写的一款分级式接口权限检查方案。 权限自动同步机制&#xff08;启动更新&#xff0c;页面不提供增删改&#xff09;&#xff1a; public class AuthorizationMappingGenerateExecutor implements EasyApplicationRunner {Autowiredprivate AuthorizationMap…...

步入React正殿 - 事件处理

目录 扩展学习资料 React事件和DOM事件 和传统DOM事件处理异同 this关键字的处理 this关键字 在JSX中使用bind方法 在构造函数中使用bind方法 使用箭头函数【推荐】 向事件处理程序传递参数【不跨组件】 向父组件传递参数 /src/App.js /src/components/listItem.jsx…...

NLP(六十四)使用FastChat计算LLaMA-2模型的token长度

LLaMA-2模型部署 在文章NLP&#xff08;五十九&#xff09;使用FastChat部署百川大模型中&#xff0c;笔者介绍了FastChat框架&#xff0c;以及如何使用FastChat来部署百川模型。   本文将会部署LLaMA-2 70B模型&#xff0c;使得其兼容OpenAI的调用风格。部署的Dockerfile文件…...

个保新标 | 《信息安全技术 敏感个人信息处理安全要求》(征求意见稿)发布

8 月 9 日&#xff0c;全国信息安全标准化技术委员会公开发布关于国家标准《信息安全技术 敏感个人信息处理安全要求》&#xff08;征求意见稿&#xff09;&#xff08;以下简称《标准》&#xff09;的通知&#xff0c;面向社会广泛征求意见。 《标准》的制定背景是为支撑《个人…...

【uniapp 返回顶部】

返回顶部 参数说明示例官方链接 uni.pageScrollTo(OBJECT) 将页面滚动到目标位置。 参数说明 参数名类型必填说明scrollTopNumber否滚动到页面的目标位置&#xff08;单位px&#xff09;selectorString否选择器&#xff0c;App、H5、微信小程序2.7.3 、支付宝小程序1.20.0支持…...

无代码集成励销云CRM连接更多应用

场景描述&#xff1a; 基于励销云的开放API&#xff0c;实现无代码集成连接励销云与其它应用。通过Aboter可轻松搭建业务自动化流程&#xff0c;实现多个应用之间的数据连接。 接口能力&#xff1a; 用户模块业务模块拜访签到模块公海客户模块联系人模块合同模块客户模块任务…...

QT自带PDF库的使用

QT自带PDF库可以方便的打开PDF文件&#xff0c;并将文件解析为QImage&#xff0c;相比网上提供的开源库&#xff0c;QT自带PDF库使用更方便&#xff0c;也更加可靠&#xff0c;然而&#xff0c;QT自带PDF库的使用却不同于其他通用库的使用&#xff0c;具备一定的技巧。 1. 安装…...

SQL | 排序检索的数据

3-排序检索的数据 使用order by语句排序检索到的数据。 3.1-排序数据 使用SQL语句返回一个数据表的列。 select prod_id from products; --------------------- | prod_name | --------------------- | 8 inch teddy bear | | 12 inch teddy bear | | 18 inch teddy bear |…...

8. yaml文件管理

文章目录 yaml文件管理编写yaml配置文件获取配置模板方法一方法二方法三方法四 yaml文件管理 Kubernetes 支持 YAML 和 JSON 格式管理资源对象 JSON 格式&#xff1a;主要用于 api 接口之间消息的传递YAML 格式&#xff1a;用于配置和管理&#xff0c;YAML 是一种简洁的非标记性…...

Cobbler自定义yum源

再次了解下Cobbler的目录结构&#xff1a; 在/var/www/cobbler/ks_mirror目录下存放的是所有的镜像。 存放的是仓库镜像&#xff1a; 在/var/lib/cobbler/kickstarts目录下是存放的所有的kickstarts文件。 再有就是/etc/cobbler这个目录&#xff1a; [rootvm1 loaders]# cd /…...

《算法竞赛·快冲300题》每日一题:“特殊数字”

《算法竞赛快冲300题》将于2024年出版&#xff0c;是《算法竞赛》的辅助练习册。 所有题目放在自建的OJ New Online Judge。 用C/C、Java、Python三种语言给出代码&#xff0c;以中低档题为主&#xff0c;适合入门、进阶。 文章目录 题目描述题解C代码Java代码Python代码 “ 特…...

在R中比较两个矩阵是否相等

目录 方法一&#xff1a;使用all.equal()比较两个R对象是否近似相等 方法二&#xff1a;使用identical比较两个R对象是否精确相等。 方法一&#xff1a;使用all.equal()比较两个R对象是否近似相等 使用函数&#xff1a;all.equal(x,y) 比较两个R对象x和y是否近似相等 > M1…...

商城-学习整理-基础-商品服务API-属性分组(七)

目录 一、创建系统菜单二、开发商品系统-平台属性-属性分组1、将三级分类功能抽取出来2、编写后端代码3、属性分组新增功能4、属性分组修改回显功能 三、商品系统-平台属性-规则参数1、列表展示页面2、新增规格参数页面 四、商品系统-平台属性-销售属性1、列表展示页面2、新增或…...

什么是响应式设计?列举几种实现响应式设计的方法。

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 什么是响应式设计&#xff1f;⭐ 实现响应式设计的方法⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个专栏…...

Java类和对象(一文读懂)

文章目录 类、对象是什么&#xff1f;创建类构造器 创建对象 类、对象是什么&#xff1f; 类&#xff1a;类是一个模板&#xff0c;它描述一类对象的行为和状态。类可以看成是创建 Java 对象的模板。 对象&#xff1a;对象是类的一个实例&#xff08;对象不是找个女朋友&#x…...

用友移动管理系统 任意文件上传漏洞复现(HW0day)

0x01 产品简介 用友移动系统管理是用友公司推出的一款移动办公解决方案&#xff0c;旨在帮助企业实现移动办公、提高管理效率和员工工作灵活性。它提供了一系列功能和工具&#xff0c;方便用户在移动设备上管理和处理企业的系统和业务。 0x02 漏洞概述 用友移动管理系统 uploa…...

启动springboot,出现Unable to start embedded Tomcat

报错信息 org.apache.catalina.core.ContainerBase : A child container failed during startjava.util.concurrent.ExecutionException: org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Tomcat].StandardHost[localhost].TomcatEmbedd…...

加密和安全

加密和安全 一.安全机制 安全攻击的几种典型方式&#xff1a; STRIDE Spoofing 假冒 Tampering 篡改 Repudiation 否认 Information Disclosure 信息泄漏 Denial of Service 拒绝服务 Elevation of Privilege 提升…...

Maven基础总结

前言 Maven 是一个项目管理工具&#xff0c;可以对 Java 项目进行构建、依赖管理。 基本要求掌握 配置Maven环境直接查。 得会在IDEA创建Maven的java项目吧、会创建Maven的web项目吧、会创建多模块项目吧。 得会配置插件pligin、依赖dependency吧 一、Maven四大特性 1、…...

Java 编程实战:如何用 Java 编写一个简单而强大的 Tomcat

学习完了JavaWeb&#xff0c;为了深入了解tomcat&#xff0c;打算手撕tomcat搭建自己的tomcat&#xff0c;希望对来访小伙伴也有帮助 引言 Tomcat 是一个开源的 Web 服务器和 Servlet 容器&#xff0c;它可以提供动态 Web 内容的处理和交互功能。Tomcat 是用 Java 语言编写的&a…...

【JavaSE】数组的定义与使用

详解数组 数组的基本概念什么是数组数组的创建及初始化数组的使用 数组是引用类型基本类型变量与引用类型变量的区别引用变量认识 null 数组的应用场景数组练习二维数组 数组的基本概念 什么是数组 数组可以看成是相同类型元素的一个集合。在内存中是一段连续的空间。比如现实…...

银河麒麟安装php7.1.33

银河麒麟V10兼容CentOS 8 安装过程与CentOS类似。 TencentOS3.1安装PHPNginxredis测试系统_乐大师的博客-CSDN博客 可以参考之前我写的文章。 不过有2个细节不同&#xff0c;下面说下。 问题1&#xff1a;编译错误提示“error:off_t undefined” 解决方法&#xff1a; 编…...

Kubernetes集群部署上篇(安装部署,但是集群网络未部署)

第四阶段 时 间&#xff1a;2023年8月9日 参加人&#xff1a;全班人员 内 容&#xff1a; Kubernetes集群部署上篇 目录 一、Kubernetes部署方式 &#xff08;一&#xff09;minikube &#xff08;二&#xff09;二进制包 &#xff08;三&#xff09;Kubeadm Kubea…...

跨境电商中的安全挑战与隐擎Fox指纹浏览器的应用

随着全球互联网的蓬勃发展&#xff0c;跨境电商已经成为了国际贸易的重要组成部分。然而&#xff0c;跨境电商的迅速崛起也伴随着一系列安全挑战&#xff0c;其中之一就是恶意活动和隐私泄露。为了应对这些挑战&#xff0c;诸多技术手段被开发出来&#xff0c;其中隐擎Fox指纹浏…...

10. Docker Swarm(一)

目录 1、前言 2、Docker Swarm体系架构 2.1、简单介绍 2.2、体系架构 3、简单使用 3.1、环境准备 3.2、初始化master节点 3.3、建立worker节点 3.4、查看集群的节点信息 3.5、部署应用 3.5.1、创建Dockerfile文件 3.5.2、构建镜像 3.5.3、将镜像上传到Docker仓库 …...

【MySQL】InnoDB存储引擎详解

InnoDB引擎是MySQL5.5版本之后默认的存储引擎 逻辑存储结构 首先是表空间Tablespace&#xff08;ibd文件&#xff09;&#xff1a;一个mysql实力可以对应多个表空间&#xff0c;用于存储及记录&#xff0c;索引等数据 这些存储记录&#xff0c;索引等数据中是用段(Segment)来…...

组合求和-矩阵连乘所有加括号方式_2023_08_12

矩阵链加括号方式总数 前言 矩阵链乘积的瓶颈在于其标量运算的次数&#xff0c;不同的结合次序对其时间性能影响远大于矩阵乘积运算本身&#xff0c;可以看到许多教材上把求解矩阵标量运算的最优解作为动态规划的示例&#xff0c;问题隐含动态规划两大特征&#xff1a; 最优子…...

《3D 数学基础》12 几何图元

目录 1 表达图元的方法 1.1 隐式表示法 1.2 参数表示 1.3 直接表示 2. 直线和射线 2.1 射线的不同表示法 2.1.1 两点表示 2.1.2 参数表示 2.1.3 相互转换 2.2 直线的不同表示法 2.2.1 隐式表示法 2.2.2 斜截式 2.2.3 相互转换 3. 球 3.1 隐式表示 1 表达图元的方…...

【设计模式——学习笔记】23种设计模式——备忘录模式Memento(原理讲解+应用场景介绍+案例介绍+Java代码实现)

案例引入 游戏角色有攻击力和防御力&#xff0c;在大战Boss前保存自身的状态(攻击力和防御力)&#xff0c;当大战Boss后攻击力和防御力下降&#xff0c;可以从备忘录对象恢复到大战前的状态 传统设计方案 针对每一种角色&#xff0c;设计一个类来存储该角色的状态 【分析】…...

致谢丨感谢有你,JumpServer开源项目九周年致谢名单

2014年到2023年&#xff0c;JumpServer开源项目已经走过了九年的时间。感谢以下社区贡献者对JumpServer项目的帮助和支持。 因为有你&#xff0c;一切才能成真。 JumpServer开源项目贡献者奖杯将于近日邮寄到以上贡献者手中&#xff0c;同时JumpServer开源项目组还准备了一份小…...