Spring Boot如何优雅实现动态灵活可配置的高性能数据脱敏功能

1.背景

在当下互联网高速发展的时代下,涉及到用户的隐私数据安全越发重要,一旦泄露将造成不可估量的后果。所以现在的业务系统开发中都会对用户隐私数据加密之后存储落库,同时还要求后端返回数据给前台之前进行数据脱敏。所谓脱敏处理其实就是将数据进行混淆隐藏,如将用户的手机号脱敏展示为`178****5939,采用 * 进行隐藏,以免泄露个人隐私信息。其实我之前就总结过相关联的功能实现:《实现数据加密存储、模糊匹配和脱敏》 但是强调一下今天这里并不是重复再讲一遍,而是在之前总结的基础上进行延伸拓展,重点在于标题里的:动态灵活可配置。那什么是动态灵活可配置呢?且听我娓娓道来。

那是一个惬意的下午时光,我听着歌,敲着一手代码…,突然间产品就来到我桌前,打断了我短暂的文思泉涌高光时刻,企图给我安排一个折磨的活,打开了奶茶app,暗示没有一杯奶茶解决不了的需求。是的最后这活我接了,也就是我们今天所要讲的数据脱敏由之前的前端脱敏改为后端,当然脱敏功能本身并不复杂难实现,那为啥说它是折磨人的活呢?普通的脱敏功能大概是这样的,也就是脱敏上面所说的用户固定隐私数据:姓名、手机号、身份证号、地址、身份证...等,但是我们的系统需求要求不只是前面的字段,还需要支持其他字段,一句话生动形象地总结概括就是客户想脱啥就脱啥,想在哪脱就在哪脱,包括我们系统支持的自定义字段,都可以通过配置进行脱敏,如下图所示:

由图可知,这个脱敏需求功能涉及到以下几点:

① 哪些字段可以脱敏是可配置的,脱敏规则也是可配置的,比如说什么开头、中间、结尾,区间啥的。

② 脱敏设计到组织架构过滤,也就是说需要实现某个部门下的用户看到的数据是脱敏的,某个部门下的用户看到的数据是不脱敏的。这感觉就像后端接口功能菜单权限检验,判断当前用户是否有调用某个功能菜单接口的权限。

③ 涉及到角色的判断,某些角色需要脱敏(小喽啰不给看,防止把客户数据卖了),而某些角色不需要(管理员随便看,随便卖)。

这里我们暂且不讨论脱敏功能这么设计是否合理,反正产品是这样要求实现的,按照上面的列出来的点感觉也还好,也不至于上升到折磨的程度,折磨的是系统的历史原因,要脱敏的字段信息遍布都整个业务系统表单,每个表单的接口数据有冗余的有共用的,这就意味着每个页面表单接口都需要去梳理一遍。同时脱敏字段还涉及到编辑更新功能,而且之前的更新接口都是一个表单整体提交,这就导致一个脱敏字段没有修改,但是前端把脱敏数据传回后端来,又是一个一个去适配啊~~~,难顶。闹骚发完了,言归正传我们接下来看看是如何优雅地实现这个难顶的功能需求。

2.实现思路

所谓”优雅“,就是多写一行代码都算我输…,所以在接口controller层返回之前一个一个地进行脱敏操作是不可取的,重复的工作量太多。思来想去,肯定是需要通过切面思想去解决,也就是通过对接口返回的VO类需要脱敏的字段使用注解打上标识,然后切面统一逻辑处理,这时候想到之前总结的接口响应结果结构统一封装返回:@ControllerAdvice, 不清楚的可跳转:《Spring Boot如何优雅实现结果统一封装和异常统一处理》自行查看,但我们发现使用@ControllerAdvice意味着每次调接口都需要我们自己去反射类获取注解判断字段是否需要脱敏,当返回对象比较复杂,需要递归去反射,性能一下子就会降低。反射这个东西确实是框架的灵魂,但是在业务接口中用多了也的确对性能有一定影响,所以再三斟酌于是换种了中思路,我们想到了平时使用的@JsonFormat,跟我们现在的需求功能场景很类似,通过自定义注解跟字段解析器,对字段进行自定义解析。

  @JsonFormat(pattern = "yyyy-MM-dd")private Date recycleDate;

这样就可以把字段recycleDate转换为日期格式,不需要我们单独代码处理,这就是优雅!!!按照这个思路,我们首先需要定义一个注解进行脱敏字段标注:

@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = MaskSerializer.class)
public @interface MaskField {/*** 脱敏类型* @return*/MaskEnum value();
}

这个注解很简单,只有一个默认属性value()指定字段的脱敏枚举类型MaskEnum

@Retention(RetentionPolicy.RUNTIME):运行时生效。

@Target(ElementType.FIELD):可用在字段上。

@JacksonAnnotationsInside:此注解可以点进去看一下是一个元注解,主要是用户打包其他注解一起使用。

@JsonSerialize(using = MaskSerializer.class):该注解的作用就是可自定义序列化,可以用在注解上,方法上,字段上,类上,运行时生效等等,根据提供的序列化类里面的重写方法实现自定义序列化。关于MaskSerializer就是我们自定义序列化实现脱敏的核心实现所在,后面会详细分析。从注解定义可以,我们给脱敏字段使用注解打标识时,需要指定该字段脱敏类型是什么,所以接下来我们还需要定义脱敏类型枚举类,我们系统大概分为两类:系统字段如(姓名,手机号,身份证号…等等),自定义字段(婚姻状况,月供金额这些额外信息,会存储在数据库一个JSON字段里)。

public enum MaskEnum {/*** 中文名*/NAME,/*** 身份证号*/ID_CARD,/*** 手机号*/MOBILE,/*** 地址*/ADDRESS,/*** 电子邮件*/EMAIL,/*** 银行卡*/BANK_CARD,/*** 自定义字段*/CUSTOM_FIELD
}

注解和脱敏类型我们都定义好,看样子万事俱备只欠东风啦,我们只需要接下来自定义实现序列化解析器完成脱敏即可,但是你可能忘了文章开头一直强调的:动态灵活,我们的脱敏配置信息不是固定的,而是动态配置保存的,这就意味着一个接口的某个字段上一次调用还需要脱敏,紧接着脱敏配置被改了,再调接口该字段就不再需要脱敏,要求我们做到动态的同时还要保证实时性。这如同需要判断功能菜单权限那样通过切面实现判断是否需要脱敏,将脱敏配置信息上下文贯穿整次请求,这里我们在登录认证的过滤器中实现,因为脱敏配置涉及到角色、组织架构,自然是要登录之后才能进行是否需要脱敏判断。

@Component
@Slf4j
public class AuthFilter implements Filter {@Autowired private StringRedisTemplate stringRedisTemplate;@Overridepublic void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)//登录认证逻辑// ........// 登录验证通过之后,就可以登录用户的公司id,设置脱敏配置上下文String rule = stringRedisTemplate.opsForValue().get(KeyCache.ORG_DESENSITIZATION_SETTING + company.getId());if (StringUtils.isNotBlank(rule)) {MaskSetting maskSetting = JSON.parseObject(rule, MaskSetting.class);Boolean isMask = false;Boolean enable = maskSetting.getEnable() == null ? false : maskSetting.getEnable();// 1.先判断脱敏总开关if (enable) {// 脱敏的组织架构List<Long> depTeamIds = maskSetting.getDepTeamIds();Boolean userLevelSwitch = maskSetting.getUserLevelSwitch();Boolean adminLevelSwitch = maskSetting.getAdminLevelSwitch();// 2.脱敏权限角色判断, 如果登录人是坐席或者外访员if (Objects.nonNull(roleId) && (UserUtils.isChairMan() || UserUtils.isVisitor())) {// 判断催员属于部门是否脱敏if (userLevelSwitch && !CollectionUtils.isEmpty(depTeamIds) &&(depTeamIds.contains(userSession.getDepId()) || depTeamIds.contains(userSession.getTeamId()))) {isMask = true;}} else if (Objects.nonNull(roleId) && adminLevelSwitch) {// 其余一律按管理员处理isMask = true;}}MaskContextHolder.setMask(isMask);if (isMask) {MaskContextHolder.setMaskSetting(maskSetting);}}filterChain.doFilter(servletRequest, servletResponse);} finally {MaskContextHolder.clear();}}}

可以看到我们在过滤器filter中等登录验证校验通过之后,再进行脱敏配置的上下文设置,这里我们是直接在缓存redis中获取登录用户对应公司的脱敏信息配置,脱敏配置在我们SaaS系统是以公司维度配置的,也就是配置了脱敏信息那么对这个公司整体用户都有效果,而且我们在配置脱敏信息保存落库的同时也会同步保存redis,我们每次调接口都需要设置脱敏信息上下文,如果这些信息都从数据库获取(由于之前的设计脱敏配置信息要查3张表…),性能自然是吃不消的,所以我们需要从缓存redis中取脱敏配置,当然上面的实现不太完善,因为直接从redis中取有可能缓存缺失这时候就拿不到脱敏配置了,所以我们需要有一个兜底实现,在缓存中获取不到再去数据库中查询,取到之后设置上下文的同时更新缓存redis,这样下次我们再从redis中就能获取到了,这个兜底实现很简单,可自行实现哦,接下来看看定义的缓存脱敏配置信息:

@Data
public class MaskSetting implements Serializable {/*** 脱敏总开关*/private Boolean enable;/*** 坐席,外访员界面信息脱敏*/private Boolean userLevelSwitch;/*** 管理员界面信息脱敏*/private Boolean adminLevelSwitch;/*** 绑定的需要脱敏的小组或部门id*/private List<Long> depTeamIds;/*** 脱敏规则*/private List<DesensitizationRuleCreate> rules;}

脱敏规则:DesensitizationRuleCreate:

@ApiModel(description = "脱敏规则")
@Data
public class DesensitizationRuleCreate {@ApiModelProperty(value = "字段英文名称")@NotEmptyprivate String name;@ApiModelProperty(value = "0:隐藏,1:显示")@NotNullprivate Integer type;@ApiModelProperty(value = "规则:开头:0 中间:1 末尾: -1 全部: 2 区间:3")@NotNullprivate Integer scope;@ApiModelProperty(value = "位数")private Integer count;@ApiModelProperty(value = "开始位数")private Integer start;@ApiModelProperty(value = "结束位数")private Integer end;}

完成脱敏信息上下文设置之后,接下来就是真正自定义实现序列化解析器完成脱敏的时刻啦,话不多说,先看看实现类:

public class MaskSerializer<T> extends JsonSerializer<T> implements ContextualSerializer{/*** 脱敏类型*/private MaskEnum type;private static List<String> ADDRESS = Lists.newArrayList("address", "house_address", "company_address","native_address", "bill_path", "other_address");@Overridepublic void serialize(T t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {if (Objects.isNull(t)) {jsonGenerator.writeObject(t);return;}Boolean isMask = MaskContextHolder.getMask();if (!isMask) {jsonGenerator.writeObject(t);return;}MaskSetting maskSetting = MaskContextHolder.getMaskSetting();List<DesensitizationRuleCreate> rules = maskSetting.getRules();if (CollectionUtils.isEmpty(rules)) {jsonGenerator.writeObject(t);return;}switch (this.type) {case NAME: {String s = (String)t;DesensitizationRuleCreate rule = rules.stream().filter(r -> Objects.equals(r.getName(), "name")).findFirst().orElse(null);jsonGenerator.writeString(MaskUtils.commonMask(s, rule));break;}case ID_CARD: {String s = (String)t;DesensitizationRuleCreate rule = rules.stream().filter(r -> Objects.equals(r.getName(), "idCard")).findFirst().orElse(null);jsonGenerator.writeString(MaskUtils.commonMask(s, rule));break;}case MOBILE: {String s = (String)t;DesensitizationRuleCreate rule = rules.stream().filter(r -> Objects.equals(r.getName(), "mobile")).findFirst().orElse(null);jsonGenerator.writeString(MaskUtils.commonMask(s, rule));break;}case ADDRESS: {String s = (String)t;DesensitizationRuleCreate rule = rules.stream().filter(r -> Objects.equals(r.getName(), "address")).findFirst().orElse(null);jsonGenerator.writeString(MaskUtils.commonMask(s, rule));break;}// 自定义字段,是一个<String, String> mapcase CUSTOM_FIELD: {// 自定义字段逻辑Map<String, String> map = (Map)t;map.forEach((k,v) -> {String maskKey = k;// 特殊自定义字段处理if (k.contains("|")) {maskKey = k.substring(0, k.indexOf("|"));}// 一个地址脱敏需要脱敏多个 如:家庭地址,公司地址等if (ADDRESS.contains(k)) {maskKey = "address";}DesensitizationRuleCreate rule = null;// 匹配规则for (DesensitizationRuleCreate r : rules) {if (Objects.equals(r.getName(), maskKey)) {rule = r;break;}}// 脱敏工具类脱敏String maskText = MaskUtils.commonMask(v, rule);map.put(k, maskText);});jsonGenerator.writeObject(map);break;}}}/*** 统一判断返回的vo类的哪些字段需要脱敏* 这里也是通过反射判断字段是否打上自己标识,但是这个方法只会执行一次,* 也就是接口第一次调用会执行,后面就不需要执行,因为这些Spring考虑到这些打注解标识解析就不会再变,没别要每次都来解析浪费性能*/@Overridepublic JsonSerializer <?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {// 为空直接跳过if (beanProperty == null) {return serializerProvider.findNullValueSerializer(beanProperty);}// 非String类直接跳过if (Objects.equals(beanProperty.getType().getRawClass(), String.class) || Objects.equals(beanProperty.getType().getRawClass(), Map.class)) {MaskField maskField = beanProperty.getAnnotation(MaskField.class);if (maskField == null) {maskField = beanProperty.getContextAnnotation(MaskField.class);}if (maskField != null) {// 如果能得到注解,就将注解的 value 传入 MaskSerializereturn new MaskSerializer(maskField.value());}}return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);}public MaskSerializer() {}public MaskSerializer(final MaskEnum type) {this.type = type;}
}

JsonSerializer可以实现的附加接口以获取回调,该回调可用于创建序列化程序的上下文实例以用于处理支持类型的属性。这对于可以通过注释配置的序列化程序很有用,或者应该根据正在序列化的属性类型而具有不同的行为

#createContextual()可以获得字段的类型以及注解,该方法只会在第一次序列化字段时调用(因为字段的上下文信息在运行期不会改变),所以不用担心影响性能。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址:https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨qun:Shepherd_126

最后来看看脱敏工具类实现,流行的Hutool工具包实现了脱敏,先引入依赖:

<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.16</version>
</dependency>

现阶段最新版本的Hutool支持的脱敏数据类型如下,基本覆盖了常见的敏感信息:用户id、中文姓名、身份证号、座机号、手机号、地址、电子邮件、密码 、中国大陆车牌、银行卡

注意 :Hutool 脱敏是通过*来代替敏感信息的,具体实现是在StrUtil.hide方法中,如果我们想要自定义隐藏符号,则可以把Hutool的源码拷出来,重新实现即可。

但是根据上文介绍我们脱敏规则是自定义的,所以我们需要得自己实现一个脱敏工具类MaskUtils:

public class MaskUtils {/*** 中文名字*/public static String maskName(String fullName) {return DesensitizedUtil.chineseName(fullName);}/*** [身份证号]*/public static String maskIdCard(String idCard) {return DesensitizedUtil.idCardNum(idCard, 4,2);}/*** [手机号码] 前三位,后四位,其他隐藏<例子:138****1234>*/public static String maskMobile(String mobile) {return DesensitizedUtil.mobilePhone(mobile);}/*** [地址]** @param sensitiveSize 敏感信息长度*/public static String maskAddress(String address, int sensitiveSize) {return DesensitizedUtil.address(address, sensitiveSize);}/*** [电子邮箱]*/public static String maskEmail(String email) {return DesensitizedUtil.email(email);}/*** [银行卡号] 前六位,后四位,其他用星号隐藏每位1个星号<例子:6222600**********1234>*/public static String maskBankCard(String cardNum) {return DesensitizedUtil.bankCard(cardNum);}/*** 自定义脱敏规则实现* @param text* @param rule* @return*/public static String commonMask(String text, DesensitizationRuleCreate rule) {if (StringUtils.isBlank(text) || Objects.isNull(rule)) {return text;}int length = text.length();// 0:隐藏,1:显示Integer type = rule.getType();// 开头:0 中间:1 末尾: -1 全部: 2 区间:3Integer scope = rule.getScope();Integer count = rule.getCount();Integer start = rule.getStart();Integer end = rule.getEnd();StringBuilder ms = new StringBuilder();// 开头count位if (scope == 0) {for (int i = 0; i < length; i++) {if (i < count) {if (type == 0) ms.append("*");if (type == 1) ms.append(text.charAt(i));} else {if (type == 0) ms.append(text.charAt(i));if (type == 1) ms.append("*");}}}if (scope == 1) {// 中间count位int mid = length/2;int left = count/2 - (count%2 == 0 ? 0 : 1);int right = count/2;left = mid - left;left = left < 0 ? 0 : left;right = mid + right - 1;for (int i = 0; i < length; i++) {if (i >= left && i <= right) {if (type == 0) ms.append("*");if (type == 1) ms.append(text.charAt(i));} else {if (type == 0) ms.append(text.charAt(i));if (type == 1) ms.append("*");}}}if (scope == -1) {// 末尾屏蔽count位int n = length - count;n = n < 0 ? 0 : n;for (int i = 0 ; i < length; i++) {if (i >= n) {if (type == 0) ms.append("*");if (type == 1) ms.append(text.charAt(i));} else {if (type == 0) ms.append(text.charAt(i));if (type == 1) ms.append("*");}}}if (scope == 2) {// 全部for (int i = 0; i < length; i++) {if (type == 0) ms.append("*");if (type == 1) ms.append(text.charAt(i));}}if (scope == 3) {// 区间for (int i = 0; i < length; i++) {if (i >= start -1  && i <= end - 1) {if (type == 0) ms.append("*");if (type == 1) ms.append(text.charAt(i));} else {if (type == 0) ms.append(text.charAt(i));if (type == 1) ms.append("*");}}}return ms.toString();}public static void main(String[] args) {String text = "12316874345";DesensitizationRuleCreate rule = new DesensitizationRuleCreate();rule.setType(1);rule.setScope(3);rule.setCount(4);rule.setStart(1);rule.setEnd(6);String s = commonMask(text, rule);System.out.println(s);}}

使用示例:对某个接口的VO类字段进行脱敏打上注解:

@Data
public class CaseInfoVO extends Base {private static final long serialVersionUID = -3251366134027639518L;@ApiModelProperty(value = "姓名")@MaskField(MaskEnum.NAME)private String name;@ApiModelProperty(value = "身份证号码")@MaskField(MaskEnum.ID_CARD)private String idCard;@ApiModelProperty(value = "电话号码")@MaskField(MaskEnum.MOBILE)private String mobile;@ApiModelProperty(value = "自定义字段")@MaskField(MaskEnum.CUSTOM_FIELD)private Map<String, String> fields;......}  

掉接口的响应结果如下:

{"status": 200,"message": "success","data": {"id": 11592359,"name": "郑**","idCard": "33010219601031****","mobile": "*******0030","fields": {"company_phone": "10922220040","penalty_date": "2023-05-30","amount_paid": "51.00","capital": "129.00","occupation": "职业030","late_fee": "44.00","nation": "壮*","age": "*3","contacts": "*******0040"},......},"success": true
}

3.总结

以上全部就是本期关于数据脱敏知识点的总结介绍啦。 首先介绍了数据脱敏需求的背景、概念和重要性,紧接着我们逐步探讨实现方案,权衡利弊了相关实现选择,最终选择Spring Boot的自带的jackson自定义序列化实现,它的实现原来其实就是在json进行序列化渲染给前端时,进行脱敏,这样可以有效降低性能损耗,并且也不会侵入系统业务层逻辑这样可以保证我们的业务逻辑不会因为数据脱敏出现逻辑错误。与此同时也强调了动态灵活可配置的脱敏信息配置,我们通过拦截器实现脱敏信息上下文设置,在上面思路我们进行代码实现剖析和实操,借助于Hutool工具类提供的脱敏功能,完美实现了字段脱敏。

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

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

相关文章

TikTok Shop美国本土店VS跨境店,如何选择?有何区别?

TikTok不仅仅是一个用于分享有趣短视频的平台&#xff0c;它也逐渐成为了商家们极力推广自己品牌和产品的场所。 在TikTok的商业生态系统中&#xff0c;存在几种不同的商店类型&#xff0c;各有其独特性和适用场景。今天&#xff0c;我们就来深入探讨这些店的差异与特点。 一、…

Si24R2|2.4G单发射芯片 +7dBm可调功率 校讯通

Si24R2是一种通用、低功耗、高性能的2.4GHz无线射频发射芯片&#xff0c;主要用于单向通信系统&#xff0c;以降低系统成B&#xff0c;在运行中与si24r1兼容。 Si24r2具有低功耗和低成B。 它主要用于单向低功率传输系统&#xff0c;如无线控制系统、无线数据采集系统等。 Si2…

在mysql8查询中使用ORDER BY结合LIMIT时,分页查询时出现后一页的数据重复前一页的部分数据。

这里写目录标题 问题描述&#xff1a;问题模拟&#xff1a;原因分析问题解释问题解决验证官方文档支持 问题描述&#xff1a; 在mysql8查询中使用ORDER BY结合LIMIT时&#xff0c;分页查询时出现后一页的数据重复前一页的部分数据。 问题模拟&#xff1a; 表table_lock_test&…

Milvus 介绍

Milvus 介绍 Milvus 矢量数据库是什么&#xff1f;关键概念非结构化数据嵌入向量向量相似度搜索 为什么是 Milvus?支持哪些索引和指标&#xff1f;索引类型相似度指标(Similarity metrics) 应用示例Milvus 是如何设计的&#xff1f;开发者工具API访问Milvus 生态系统工具 本页…

《数据结构、算法与应用C++语言描述》使用C++语言实现数组双端队列

《数据结构、算法与应用C语言描述》使用C语言实现数组双端队列 定义 队列的定义 队列&#xff08;queue&#xff09;是一个线性表&#xff0c;其插入和删除操作分别在表的不同端进行。插入元素的那一端称为队尾&#xff08;back或rear&#xff09;&#xff0c;删除元素的那一…

【vue】el-carousel实现视频自动播放与自动切换到下一个视频功能:

文章目录 一、原因:二、实现代码:三、遇到的问题&#xff1a;【1】问题&#xff1a;el-carousel页面的视频不更新【2】问题&#xff1a;多按几次左按钮&#xff0c;其中跳过没有播放的视频没有销毁&#xff0c;造成再次自动播放时会跳页 一、原因: 由于后端无法实现将多条视频拼…

手机爬虫用Scrapy详细教程:构建高效的网络爬虫

如果你正在进行手机爬虫的工作&#xff0c;并且希望通过一个高效而灵活的框架来进行数据抓取&#xff0c;那么Scrapy将会是你的理想选择。Scrapy是一个强大的Python框架&#xff0c;专门用于构建网络爬虫。今天&#xff0c;我将与大家分享一份关于使用Scrapy进行手机爬虫的详细…

照片后期编辑工具Lightroom Classic 2024 mac中文新增功能

Lightroom Classic 2024&#xff08;lrC2024&#xff09;是专为摄影爱好者和专业摄影师设计的软件&#xff0c;它提供了全面的照片编辑工具&#xff0c;可以精准调整照片的色彩、对比度和曝光等参数&#xff0c;以便定制后期处理效果。 在lrC2024中&#xff0c;用户体验得到了提…

文件的逻辑结构(顺序文件,索引文件)

所谓的“逻辑结构”&#xff0c;就是指在用户看来&#xff0c;文件内部的数据应该是如何组织起来的。 而“物理结构”指的是在操作系统看来&#xff0c;文件的数据是如何存放在外存中的。 1.无结构文件 无结构文件:文件内部的数据就是一系列二进制流或字符流组成。无明显的逻…

SortedSet 和 List 异同点

SortedSet 在 Java 的整个集合体系中&#xff0c;集合可以分成两个体系&#xff0c;一个是 Collection 存储单个对象的集合&#xff0c;另一个是 k-v 结构的 Map 集合。 SortedSet 是 Collection 体系下 Set 接口下的派生类&#xff0c;而 Set 集合的特征是不包含重 复的元素的…

(论文翻译)UFO: Unified Feature Optimization——UFO:统一特性优化

作者&#xff1a; Teng Xi 论文总结&#xff1a;总结 Code: https://github.com/PaddlePaddle/VIMER/tree/main/UFO 摘要&#xff1a; 本文提出了一种新的统一特征优化(Unified Feature Optimization, UFO)范式&#xff0c;用于在现实世界和大规模场景下训练和部署深度模型…

asp.net特色商品购物网站系统VS开发sqlserver数据库web结构c#编程Microsoft Visual Studio

一、源码特点 asp.net特色商品购物网站系统 是一套完善的web设计管理系统&#xff0c;系统采用mvc模式&#xff08;BLLDALENTITY&#xff09;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 vs2010&#xff0c;数据库为sqlserver2008&a…

安装Apache2.4

二、安装配置Apache&#xff1a; 中文官网&#xff1a;Apache 中文网 官网 (p2hp.com) 我下的是图中那个版本&#xff0c;最新的64位 下载下后解压缩。如解压到D:\tool\Apache24 PS&#xff1a;特别要注意使用的场景和64位还是32位版本 2、修改Apcahe配置文件 具体步骤: 打…

Required MultipartFile parameter ‘file‘ is not present

出现这个原因我们首先想到的是加一个RequestParam("file")&#xff0c;但是还有可能的原因是因为我们的名字有错误 <span class"input-group-addon must">模板上传 </span> <input id"uploadFileUpdate" name"importFileU…

内衣专用洗衣机怎么样?选购内衣裤洗衣机的方法

有的小伙伴在问内衣洗衣机有没有必要入手&#xff0c;答案是有必要的&#xff0c;贴身衣物一定要和普通衣服分开来洗&#xff0c;而且用手来清洗衣物真的很耗时间而且还清洗不干净&#xff0c;有了内衣洗衣机&#xff0c;我们不仅可以解放双手&#xff0c;在清洗过程中还能更加…

实现日期间的运算——C++

&#x1f636;‍&#x1f32b;️Take your time ! &#x1f636;‍&#x1f32b;️ &#x1f4a5;个人主页&#xff1a;&#x1f525;&#x1f525;&#x1f525;大魔王&#x1f525;&#x1f525;&#x1f525; &#x1f4a5;代码仓库&#xff1a;&#x1f525;&#x1f525;魔…

SLAM 14 notes

4.23 推导 f ( x ) f(x) f(x)在点a处的泰勒展开 f ( x ) ∑ n 0 ∞ f ( n ) a n ! ( x − a ) n f(x) \sum_{n0}^\infty \frac{f^{(n)}a}{n!}(x-a)^n f(x)∑n0∞​n!f(n)a​(x−a)n l n x lnx lnx的n阶导数 l n ( n ) x ( − 1 ) n − 1 ( n − 1 ) ! x n ln^{(n)}x \fr…

react 中获取多个input输入框中的值的 俩种写法

目录 1. 使用受控组件 2. 使用非受控组件 1. 使用受控组件 这是React中最常见的方法&#xff0c;每个输入框都与React组件的state相关联&#xff0c;并通过onChange事件来更新state。 代码示例&#xff1a; import React, { Component } from react;class MultipleInputExam…

在thonny软件里安装python包 比如 numpy pygame

有一些程序使用了第三方库。如果本地没有安装相应的Python包&#xff0c;这个程序就不能正常运行了。 Python包管理工具提供了对Python 包的查找、下载、安装、卸载的功能。 网络上有很多第三方库&#xff0c;不管要下载哪一个&#xff0c;都需要通过正确的名称来下载安装。 …

websocket+node+vite(vue)实现一个简单的聊天

1.前端逻辑 本项目基于之前搭建的vite环境&#xff1a;https://blog.csdn.net/beekim/article/details/128083106?spm1001.2014.3001.5501 新增一个登录页和聊天室页面 <template><div>登录页</div><div>用户名:<input type"text" pl…