Java日志脱敏(二)——fastjson Filter + 注解 + 工具类实现

背景简介

日志脱敏 是常见的安全需求,最近公司也需要将这一块内容进行推进。看了一圈网上的案例,很少有既轻量又好用的轮子可以让我直接使用。我一直是反对过度设计的,而同样我认为轮子就应该是可以让人拿去直接用的。所以我准备分享两篇博客分别实现两种日志脱敏方案。

方案分析

  • logback MessageConverter + 正则匹配上一篇介绍此方法

    • 优势
      • 侵入性低、工作量极少, 只需要修改xml配置文件,适合老项目
    • 劣势
      • 效率低,会对每一行日志都进行正则匹配检查,效率受日志长度影响,日志越长效率越低,影响日志吞吐量
      • 因基于正则匹配 存在错杀风险,部分内容难以准确识别
  • fastjson Filter + 注解 + 工具类本篇博客介绍

    • 优势
      • 性能损耗低、效率高、扩展性强,精准脱敏,适合QPS较高日志吞吐量较大的项目。
    • 劣势
      • 侵入性较高,需对所有可能的情况进行脱敏判断
      • 存在漏杀风险,全靠开发控制

本篇博客部分代码未贴出,可以在上一篇博客中找到 传送门:Java日志脱敏——基于logback MessageConverter实现

fastjson Filter + 注解 + 工具类

流程图解

依托于 alibaba fastjson 提供的扩展能力,自定义ContextValueFilter,在将对象JSON序列化时,返回脱敏后的value,实现打印日志脱敏
在这里插入图片描述

代码案例

定义注解

定义 元注解 用于标记脱敏策略注解

package com.zhibo.log.sensitive.annotation.metadata;import java.lang.annotation.*;/*** @Author: Zhibo* @Description: 用于自定义 sensitive 脱敏策略注解,*/
@Inherited
@Documented
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveStrategy {
}

定义 手机号 脱敏策略注解

package com.zhibo.log.sensitive.annotation.strategy;import com.zhibo.log.sensitive.annotation.metadata.SensitiveStrategy;
import java.lang.annotation.*;/*** @Author: Zhibo* @Description: 手机号脱敏注解*/
@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@SensitiveStrategy
public @interface SensitiveStrategyPhone {
}

定义 身份证号码 脱敏策略注解

package com.zhibo.log.sensitive.annotation.strategy;import com.zhibo.log.sensitive.annotation.metadata.SensitiveStrategy;
import java.lang.annotation.*;/*** @Author: zhibo* @Description: 中国身份证号脱敏注解*/
@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@SensitiveStrategy
public @interface SensitiveStrategyIdNo {
}

定义 Map 类型扩展注解,考虑到部分自定义Bean 中会有一些Map的成员变量,而Map中可能也有敏感信息需要处理。

package com.zhibo.log.sensitive.annotation;import java.lang.annotation.*;/*** @Author: Zhibo* @Description: 针对Object对象中如果存在 Map参数,而Map存在敏感字段时使用<br></>* 如果对象中属性为一个Map,则可以使用这个注解指定,Map中特定Key的加密规则。*/
@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveMap {/*** 用于构建一个Map 传递格式 "key1","value1","key2","value2"....* key 为Map中需要脱敏的key,value为脱敏规则** 案例Demo:* @SensitiveMap({"phone", LogSensitiveConstants.STRATEGY_PHONE})* private Map<String,Object> map = new HashMap<>();*     {*         map.put("key", "value");*         map.put("name","王大锤");*         map.put("phone","18123456789");*     }*/String[] value();}
将注解与对应的脱敏策略方法绑定
package com.zhibo.log.sensitive.core.util.strategy;import com.zhibo.log.sensitive.annotation.metadata.SensitiveStrategy;
import com.zhibo.log.sensitive.annotation.strategy.*;
import com.zhibo.log.sensitive.api.IStrategy;
import com.zhibo.log.format.LogSensitiveConstants;
import com.zhibo.log.sensitive.core.strategory.*;import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;/*** @Author: zhibo* @Description: 系统中内置的策略映射、注解和实现之间映射*/
public final class SensitiveStrategyBuiltInUtil {private SensitiveStrategyBuiltInUtil(){}/** 注解和实现策略的映射关系 */private static final Map<Class<? extends Annotation>, IStrategy> CLASS_MAP = new HashMap<>();private static final Map<String, IStrategy> STRATEGY_MAP = new HashMap<>();static {StrategyAddress strategyAddress =new StrategyAddress();StrategyIdNo strategyIdNo = new StrategyIdNo();StrategyPhone strategyPhone = new StrategyPhone();CLASS_MAP.put(SensitiveStrategyAddress.class, strategyAddress);CLASS_MAP.put(SensitiveStrategyIdNo.class, strategyIdNo);CLASS_MAP.put(SensitiveStrategyPhone.class, strategyPhone);STRATEGY_MAP.put(LogSensitiveConstants.STRATEGY_ADDRESS, strategyAddress);STRATEGY_MAP.put(LogSensitiveConstants.STRATEGY_ID_NO, strategyIdNo);STRATEGY_MAP.put(LogSensitiveConstants.STRATEGY_PHONE, strategyPhone);}public static IStrategy getStrategy(String key){return STRATEGY_MAP.get(key);}/*** 获取对应的系统内置实现* @param annotationClass 注解实现类* @return 对应的实现方式*/public static IStrategy require(final Class<? extends Annotation> annotationClass) {return CLASS_MAP.get(annotationClass);}/*** 获取策略* @param annotations 字段对应注解* @return 策略*/public static IStrategy getStrategy(final Annotation[] annotations) {for (Annotation annotation : annotations) {SensitiveStrategy sensitiveStrategy = annotation.annotationType().getAnnotation(SensitiveStrategy.class);if (null != sensitiveStrategy) {return SensitiveStrategyBuiltInUtil.require(annotation.annotationType());}}return null;}
}
实现JSON序列化过滤器

关键代码了,基本的逻辑都在这里

package com.zhibo.log.sensitive.core.support.filter;import com.alibaba.fastjson.serializer.BeanContext;
import com.alibaba.fastjson.serializer.ContextValueFilter;
import com.zhibo.log.sensitive.annotation.SensitiveMap;
import com.zhibo.log.sensitive.api.IStrategy;
import com.zhibo.log.sensitive.core.context.SensitiveContext;
import com.zhibo.log.sensitive.core.util.strategy.SensitiveStrategyBuiltInUtil;import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.*;/*** @Author: Zhibo* @Description: 默认的上下文过滤器,支持处理 对象、数组、集合、Map 等类型数据,支持自定义脱敏策略*/
public class SensitiveContextValueFilter implements ContextValueFilter {/** 脱敏上下文 */private final SensitiveContext sensitiveContext;public SensitiveContextValueFilter(SensitiveContext context) {this.sensitiveContext = context;}@Overridepublic Object process(BeanContext context, Object object, String name, Object value) {// 对象为 MAP 的时候,FastJson map 对应的 context 为 NULLif(null == context) {//对象为MAP则检测是否存在指定脱敏策略if (null == sensitiveContext.getMapDesStrategy() || value == null){return value;}else {//执行匹配指定脱敏策略return desMapValue(name,value);}}// 信息初始化final Field field = context.getField();if (field == null){return value;}return handleSensitive(value,field);}/*** Map 类型数据脱敏* @param name key* @param value 未被脱敏的原对象* @return 脱敏后的新对象*/private Object desMapValue(String name, Object value){String desStrategy = sensitiveContext.getMapDesStrategy().get(name);if (desStrategy != null){IStrategy strategy = SensitiveStrategyBuiltInUtil.getStrategy(desStrategy);if (strategy != null){if (value.getClass().isArray()){return desArray(value,strategy);} else if (value instanceof Collection) {return desCollection(value,strategy);}else {return strategy.des(value);}}}return value;}/*** 处理脱敏信息** @param field      当前字段*/private Object handleSensitive(final Object originalFieldVal, final Field field) {// 原始字段值IStrategy strategy = null;//处理 @SensitiveMapSensitiveMap sensitiveMap = field.getAnnotation(SensitiveMap.class);if (null != sensitiveMap) {String[] entry =sensitiveMap.value();if (entry != null && entry.length>0 && (entry.length & 1) == 0){// 不为null 且长度一致则将用户指定的脱敏规则加入本次脱敏上下文Map<String,String> map = sensitiveContext.getMapDesStrategy();if (map == null){map = new HashMap<>();sensitiveContext.setMapDesStrategy(map);}for (int i = 1; i<entry.length;i+=2){map.put(entry[i-1],entry[i]);}}return originalFieldVal;}// 系统内置自定义注解的处理,获取所有的注解Annotation[] annotations = field.getAnnotations();if (null != annotations && annotations.length > 0) {strategy = SensitiveStrategyBuiltInUtil.getStrategy(annotations);}// 判断是否获取到指定脱敏规则,如有则进行脱敏处理if (null != strategy){Class fieldTypeClass = field.getType();if(fieldTypeClass.isArray()) {// 为数组类型return desArray(originalFieldVal,strategy);}else if (Collection.class.isAssignableFrom(fieldTypeClass)){// 为集合类型return desCollection(originalFieldVal,strategy);} else {// 普通类型return strategy.des(originalFieldVal);}}return originalFieldVal;}/*** 处理数据类型,根据元素依次遍历脱敏* @param value 未被脱敏的原对象* @param strategy 脱敏策略* @return 脱敏后的新对象*/private Object desArray(Object value,IStrategy strategy){Object[] arrays = (Object[]) value;if (null != arrays && arrays.length > 0) {final int arrayLength = arrays.length;Object newArray = new Object[arrayLength];for (int i = 0; i < arrayLength; i++) {Array.set(newArray, i, strategy.des(arrays[i]));}return newArray;}return value;}/*** 处理集合类型,根据元素依次遍历脱敏* @param value 未被脱敏的原对象* @param strategy 脱敏策略* @return 脱敏后的新对象*/private Object desCollection(Object value,IStrategy strategy){final Collection<Object> entryCollection = (Collection<Object>) value;if (null != entryCollection && !entryCollection.isEmpty()) {List<Object> newResultList = new ArrayList<>(entryCollection.size());for (Object entry : entryCollection) {newResultList.add(strategy.des(entry));}return newResultList;}return value;}
}

上下文对象 SensitiveContext,目前里面只有一个Map,用来存储对map类型元素进行脱敏的策略规则

package com.zhibo.log.sensitive.core.context;import java.util.Map;/*** @Author: Zhibo* @Description: 脱敏上下文*/
public class SensitiveContext {private SensitiveContext(){}/*** Map中Key的指定脱敏规则*/private Map<String,String> mapDesStrategy;public Map<String, String> getMapDesStrategy() {return mapDesStrategy;}public void setMapDesStrategy(Map<String, String> mapDesStrategy) {this.mapDesStrategy = mapDesStrategy;}/*** 新建一个对象实例* @return this*/public static SensitiveContext newInstance() {return new SensitiveContext();}
}
脱敏工具类使用入口

支持自定义Bean 脱敏 根据注解规则;
有时候日志打印的直接就是一个Map,没有写注解的地方,这里也支持了;
还有很多场景直接打印 参数 直接就是一个String的文本,所以也支持 直接对文本进行脱敏;
当然还有数组、集合的直接打印需求也都支持了;

package com.zhibo.log.sensitive.core;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.ContextValueFilter;
import com.zhibo.log.format.LogSensitiveConstants;
import com.zhibo.log.sensitive.api.IStrategy;
import com.zhibo.log.sensitive.core.context.SensitiveContext;
import com.zhibo.log.sensitive.core.support.filter.SensitiveContextValueFilter;
import com.zhibo.log.sensitive.core.util.strategy.SensitiveStrategyBuiltInUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;/*** @Author: Zhibo* @Description: 脱敏工具类*/
public final class LogDesensitizeUtil {private static final Logger log = LoggerFactory.getLogger(LogDesensitizeUtil.class);private LogDesensitizeUtil(){}/*** 返回脱敏后的对象 json* null 对象,返回字符串 "null"* @param object 对象/map 自定义的对象,依据对象内注解进行脱敏* @param entry 用于构建一个Map 传递格式 "key1","value1","key2","value2".... 如非map对象可以忽略* @return 结果 json,如处理异常则直接返回原对象*/@SuppressWarnings({"unchecked", "rawtypes"})public static String desJson(Object object, String... entry) {try {//String类型则直接调用desStringif (object instanceof String){return desString(object.toString());}Map<String,String> map = null;if (object!= null && entry != null && entry.length>0 && (entry.length & 1) == 0){map = new HashMap<>();for (int i = 1; i<entry.length;i+=2){map.put(entry[i-1],entry[i]);}}return desJson(object,map);} catch (Exception e) {log.warn("对象脱敏失败 desJson异常 异常信息:",e);return JSON.toJSONString(object);}}/*** 返回脱敏后的对象 json* @param object 需要被脱敏的对象 or Map* @param desStrategy Map中指定Key的脱敏策略,此策略只针对String 类型的值进行脱敏,如Map中存储的是对象,请使用注解进行标记* @return 结果 json,如处理异常则直接返回原对象*/public static String desJson(Object object, Map<String, String> desStrategy) {try {if (null == object) {return JSON.toJSONString(object);}final SensitiveContext context = SensitiveContext.newInstance();context.setMapDesStrategy(desStrategy);ContextValueFilter filter = new SensitiveContextValueFilter(context);return JSON.toJSONString(object, filter);} catch (Exception e) {log.warn("对象脱敏失败 desJson异常 异常信息:",e);return JSON.toJSONString(object);}}/*** 通过正则匹配,返回脱敏后的内容,当前支持11位手机号、18位身份证号码、地址信息匹配* 如已知需脱敏的数据类型,请使用{@link LogDesensitizeUtil#desString(String, String)}方法,* @param value 未脱敏的明文* @return 结果 已脱敏的文本*/public static String desString(String value) {try {if (StringUtils.isBlank(value)){return value;}else if (value.length() == 11){// 匹配手机号规则if (Pattern.compile(LogSensitiveConstants.PHONE_REGEX).matcher(value).matches()){return desString(value, LogSensitiveConstants.STRATEGY_PHONE);}}else if (value.length() == 18){// 匹配身份证号码规则if (Pattern.compile(LogSensitiveConstants.ID_NO_REGEX).matcher(value).matches()){return desString(value, LogSensitiveConstants.STRATEGY_ID_NO);}}} catch (Exception e) {log.warn("数据脱敏失败 desString异常 异常信息:",e);}// 未命中任何规则直接返回明文return value;}/*** 依据指定的脱敏策略返回脱敏后的内容* @param value 需要被脱敏的文本* @param type 指定脱敏策略,详见{@link LogSensitiveConstants},*             如脱敏策略不存在,则不进行脱敏处理* @return 结果 已脱敏的文本*/public static String desString(String value, String type) {try {if (StringUtils.isNotBlank(value)){IStrategy strategy = SensitiveStrategyBuiltInUtil.getStrategy(type);return null == strategy? value : strategy.des(value);}} catch (Exception e) {log.warn("数据脱敏失败 desString异常 异常信息:",e);}return value;}/*** 依据指定的脱敏策略返回脱敏后的内容* @param values 需要被脱敏的文本* @param type 指定脱敏策略,详见{@link LogSensitiveConstants},*             如脱敏策略不存在,则不进行脱敏处理* @return 结果 已脱敏的文本*/public static String desString(String[] values, String type) {try {IStrategy strategy = SensitiveStrategyBuiltInUtil.getStrategy(type);if (null != values && values.length>0 && null != strategy){StringBuilder sbd = new StringBuilder("[\"");sbd.append(strategy.des(values[0])).append("\"");for (int i = 1;i<values.length;i++){sbd.append(",\"").append(strategy.des(values[i])).append("\"");}sbd.append("]");return sbd.toString();}} catch (Exception e) {log.warn("数据脱敏失败 desString异常 type:{} 异常信息:",type,e);}return JSON.toJSONString(values);}/*** 依据指定的脱敏策略返回脱敏后的内容* @param values 需要被脱敏的文本* @param type 指定脱敏策略,详见{@link LogSensitiveConstants},*             如脱敏策略不存在,则不进行脱敏处理* @return 结果 已脱敏的文本*/public static String desString(Collection<String> values, String type) {try {IStrategy strategy = SensitiveStrategyBuiltInUtil.getStrategy(type);if (null != values && values.size()>0 && null != strategy){StringBuilder sbd = new StringBuilder("[");for (String entry : values) {sbd.append("\"").append(strategy.des(entry)).append("\",");}sbd.setCharAt(sbd.length()-1,']');return sbd.toString();}} catch (Exception e) {log.warn("数据脱敏失败 desString异常 异常信息:",e);}return JSON.toJSONString(values);}
}

测试Demo

package com.zhibo.demo;import com.zhibo.log.sensitive.annotation.SensitiveMap;
import com.zhibo.log.sensitive.annotation.strategy.SensitiveStrategyIdNo;
import com.zhibo.log.sensitive.annotation.strategy.SensitiveStrategyPhone;
import com.zhibo.log.sensitive.core.LogDesensitizeUtil;
import com.zhibo.log.format.LogSensitiveConstants;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** @author zhibo*/
@Slf4j
@Data
public class DesDemoBean{/** 对身份证进行脱敏*/@SensitiveStrategyIdNoprivate String idNo = "421083202411111111";/** 对手机号集合进行脱敏 */@SensitiveStrategyPhoneprivate List<String> mobileList = Arrays.asList("18611111111","18622222222","18633333333");private String adderss = "广东省深圳市南山区牛马大厦";/*** 对Map进行脱敏* 传递格式 "key1","value1","key2","value2".... key 为Map中需要脱敏的key,value为脱敏规则*/@SensitiveMap({"idNo", LogSensitiveConstants.STRATEGY_ID_NO,"phone", LogSensitiveConstants.STRATEGY_PHONE, "phoneArr", LogSensitiveConstants.STRATEGY_PHONE})private Map<String,Object> map = new HashMap<>();{map.put("name","吕志博");map.put("phone","18123456789");map.put("phoneArr",new String[]{"18123456780","18123456781","18123456782"});map.put("idNo","421083202411111111");}public static void main(String[] args){System.out.println("------------- 通过注解为对象脱敏Begin  ------------------");DesDemoBean user = new DesDemoBean();// LogDesensitizeUtil.desJson 为自定义Bean的专用脱敏方法System.out.println(LogDesensitizeUtil.desJson(user));System.out.println("------------- 通过注解为对象脱敏End  ------------------");System.out.println("------------- 通过工具类为Map脱敏Begin  ------------------");Map<String,Object> desDemoMap = new HashMap();desDemoMap.put("name","吕志博");desDemoMap.put("phone","18888888888");desDemoMap.put("idNo","421083202411111111");desDemoMap.put("DesDemoBean",user);// 写法一 直接传入需要脱敏的key和脱敏规则, 传递格式 map, "key1","value1","key2","value2".... key 为Map中需要脱敏的key,value为脱敏规则System.out.println("写法一:" + LogDesensitizeUtil.desJson(desDemoMap,"idNo", LogSensitiveConstants.STRATEGY_ID_NO,"phone", LogSensitiveConstants.STRATEGY_PHONE));// 写法二 自行构建脱敏规则,然后以map形式传入Map<String,String>  strategyMap = new HashMap<>();strategyMap.put("idNo", LogSensitiveConstants.STRATEGY_ID_NO);strategyMap.put("phone", LogSensitiveConstants.STRATEGY_PHONE);System.out.println("写法二:" + LogDesensitizeUtil.desJson(desDemoMap,strategyMap));System.out.println("------------- 通过工具类为Map脱敏End  ------------------");/*** 指定脱敏策略进行脱敏 支持String、String数组、Collection<String>* @param1 需要被脱敏的文本* @param2 指定脱敏策略,详见{@link LogSensitiveConstants},脱敏策略不存在,则不进行脱敏处理*/System.out.println("对手机号进行脱敏:"+LogDesensitizeUtil.desString("18888888888",LogSensitiveConstants.STRATEGY_PHONE));System.out.println("对手机号集合进行脱敏:" + LogDesensitizeUtil.desString(Arrays.asList("18888888888","18888888889"),LogSensitiveConstants.STRATEGY_PHONE));System.out.println("对手机号集合进行脱敏:" + LogDesensitizeUtil.desString(new String[]{"18888888888","18888888889"},LogSensitiveConstants.STRATEGY_PHONE));/*** 通过正则匹配模式对身份证、手机号、地址进行脱敏*/System.out.println("对身份证号码进行正则匹配脱敏:" + LogDesensitizeUtil.desString("42108320241111111X"));System.out.println("对手机号码进行正则匹配脱敏:" + LogDesensitizeUtil.desString("18888888888"));}
}

内容输出如下

------------- 通过注解为对象脱敏Begin  ------------------
{"adderss":"广东省深圳市南山区牛马大厦","idNo":"421083********1111","map":{"phoneArr":["181****6780[0907ddf0d173216301559631350fa9ba]","181****6781[54ea4b6a5c8e10eac4ef873e4ce14f25]","181****6782[3f52919f044875b182bc5e6b6ba37271]"],"phone":"181****6789[093b20e8d401ee8309534de0d92eb497]","name":"吕志博","idNo":"421083********1111"},"mobileList":["186****1111[d270298c22d999895d58a1e9fd9d0751]","186****2222[2b035cebca7b1552d48db40778c15863]","186****3333[e2171fb6ec098bd41065098dc7cd6d5b]"]}
------------- 通过注解为对象脱敏End  ------------------
------------- 通过工具类为Map脱敏Begin  ------------------
写法一:{"phone":"188****8888[cbd41c6103064d3f0af848208c20ece2]","name":"吕志博","idNo":"421083********1111","DesDemoBean":{"adderss":"广东省深圳市南山区牛马大厦","idNo":"421083********1111","map":{"phoneArr":["181****6780[0907ddf0d173216301559631350fa9ba]","181****6781[54ea4b6a5c8e10eac4ef873e4ce14f25]","181****6782[3f52919f044875b182bc5e6b6ba37271]"],"phone":"181****6789[093b20e8d401ee8309534de0d92eb497]","name":"吕志博","idNo":"421083********1111"},"mobileList":["186****1111[d270298c22d999895d58a1e9fd9d0751]","186****2222[2b035cebca7b1552d48db40778c15863]","186****3333[e2171fb6ec098bd41065098dc7cd6d5b]"]}}
写法二:{"phone":"188****8888[cbd41c6103064d3f0af848208c20ece2]","name":"吕志博","idNo":"421083********1111","DesDemoBean":{"adderss":"广东省深圳市南山区牛马大厦","idNo":"421083********1111","map":{"phoneArr":["181****6780[0907ddf0d173216301559631350fa9ba]","181****6781[54ea4b6a5c8e10eac4ef873e4ce14f25]","181****6782[3f52919f044875b182bc5e6b6ba37271]"],"phone":"181****6789[093b20e8d401ee8309534de0d92eb497]","name":"吕志博","idNo":"421083********1111"},"mobileList":["186****1111[d270298c22d999895d58a1e9fd9d0751]","186****2222[2b035cebca7b1552d48db40778c15863]","186****3333[e2171fb6ec098bd41065098dc7cd6d5b]"]}}
------------- 通过工具类为Map脱敏End  ------------------
对手机号进行脱敏:188****8888[cbd41c6103064d3f0af848208c20ece2]
对手机号集合进行脱敏:["188****8888[cbd41c6103064d3f0af848208c20ece2]","188****8889[3639d5bc5f940edd8800fb7e7f5a15ba]"]
对手机号集合进行脱敏:["188****8888[cbd41c6103064d3f0af848208c20ece2]","188****8889[3639d5bc5f940edd8800fb7e7f5a15ba]"]
对身份证号码进行正则匹配脱敏:421083********111X
对手机号码进行正则匹配脱敏:188****8888[cbd41c6103064d3f0af848208c20ece2]

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

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

相关文章

高效实现SCRM用户管理的最佳实践与策略

内容概要 在当今竞争激烈的市场环境中&#xff0c;SCRM用户管理显得尤为重要。SCRM&#xff08;Social Customer Relationship Management&#xff09;不仅仅是简单的客户管理工具&#xff0c;它更是企业与客户之间建立良好关系的一座桥梁。通过深入了解用户的需求和行为&…

Git - 两种方式撤销已提交到远端仓库的记录并删除提交记录

文章目录 命令行方式附 命令行方式 确定要撤销的提交记录 首先&#xff0c;使用以下命令查看提交历史&#xff1a; git log找到想撤销的提交记录的哈希值&#xff08;SHA&#xff09; &#xff0c;比如9c9c98d6f7f28c41d971f8efd51ed31f9720792c 撤销提交记录 根据需求选择以下…

【C/C++】字符/字符串函数(0)(补充)——由ctype.h提供

零.导言 除了字符分类函数&#xff0c;字符转换函数也是一类字符/字符串函数。 C语言提供了两种字符转换函数&#xff0c;分别是 toupper &#xff0c; tolower。 一.什么是字符转换函数&#xff1f; 顾名思义&#xff0c;即转换字符的函数&#xff0c;如大写字母转小写字母&am…

【排序】5.堆排序(详细图解)

文章目录 前言1.建堆方法的选择2.优先使用向下调整的原因3.堆排序图解&#xff08;大堆-升序为例子&#xff09;3.1 向下调整法-建大堆3.2 进行堆排序 4.堆排序代码4.1 向下调整法4.1. 1 小堆4.1. 2 大堆4.2 堆排序 5. 关于小堆——降序6.性能分析 前言 &#x1f431;个人主页&…

头歌——机器学习(线性回归)

文章目录 线性回归简述答案 线性回归算法答案 线性回归实践 - 波斯顿房价预测LinearRegression代码 利用sklearn构建线性回归模型示例代码如下&#xff1a; 代码 线性回归简述 简单线性回归 在生活中&#xff0c;我们常常能碰到这么一种情况&#xff0c;一个变量会跟着另一个变…

技术美术百人计划 | 《5.4 水体渲染》笔记

一、水体渲染的波形模拟技术-基于物理 基于物理的波形模拟方法&#xff1a; 欧拉方法&#xff08;Eulerian approaches&#xff09;[Kass 1990]拉格朗日方法&#xff08;Lagrangian approaches&#xff09; [Stam 1995]欧拉-拉格朗日混合方法&#xff08;Hybrid approaches&a…

使用 Sortable.js 库 实现 Vue3 elementPlus 的 el-table 拖拽排序

文章目录 实现效果Sortable.js介绍下载依赖添加类名导入sortablejs初始化拖拽实例拖拽完成后的处理总结 在开发过程中&#xff0c;我们经常需要处理表格数据&#xff0c;并为用户提供便捷的排序方式。特别是在需要管理长列表、分类数据或动态内容时&#xff0c;拖拽排序功能显得…

Chrome与火狐的安全功能全面评估

在当今数字化时代&#xff0c;网络安全已成为用户最为关注的问题之一。作为两款广受欢迎的浏览器&#xff0c;Chrome和火狐&#xff08;Firefox&#xff09;都提供了多种安全功能来保护用户的在线隐私和数据安全。本文将全面评估这两款浏览器的安全功能&#xff0c;帮助用户更好…

Java-02

笔试算法&#xff1a; 41. 回文串 我们称一个字符串为回文串&#xff0c;当且仅当这个串从左往右和从右往左读是一样的。例如&#xff0c;aabbaa、a、abcba 是回文串&#xff0c;而 ab、ba、abc 不是回文串。注意单个字符也算是回文串。 现在&#xff0c;给你一个长度为n的…

Windows实用工具推荐(uTools+截图工具Snipaste)

闲言少叙,直奔主题 uTools 官网下载地址 uTools官网 - 新一代效率工具平台 这是工具的输入命令的样式,主题颜色可以自己设置,点击右边的头像进入主页 左侧是已经安装的工具,可以根据自己喜好安装各种实用小工具 可以自定义设置呼出菜单的快捷键 这款工具拥有很多功能,我推荐…

ViT面试知识点

文章目录 VITCLIPSAMYOLO系列问题 VIT 介绍一下Visual Transformer&#xff1f; 介绍一下自注意力机制&#xff1f; 介绍一下VIT的输出方式 介绍一下VIT做分割任务 VIT是将NLP的transformer迁移到cv领域&#xff0c;他的整个流程大概如下&#xff1a;将一张图片切成很多个pat…

STM32之串口字库更新

1.串口通讯介绍 串口通讯&#xff08;Serial Communications&#xff09;是一种通过串口进行数据传输的通讯方式&#xff0c;通过串行口每次传输一个字节的数据&#xff0c;按照约定的协议进行数据的传输和接收。串口通讯的原理是利用串行口的发送和接收线路&#xff0c;将需要…

【大语言模型】ACL2024论文-06 探索思维链COT在多模态隐喻检测中的应用

【大语言模型】ACL2024论文-06 探索思维链COT在多模态隐喻检测中的应用 目录 文章目录 【大语言模型】ACL2024论文-06 探索思维链COT在多模态隐喻检测中的应用目录摘要研究背景问题与挑战如何解决创新点算法模型1. 知识总结模块&#xff08;Knowledge Summarization Module&…

第三十一章 单页与多页应用程序概念

目录 一、概述 ​二、单页与多页对比 一、概述 单页面应用(SPA): 所有功能在一个HTML页面上实现&#xff0c;如网易云音乐。 https://music.163.com/ 多页应用&#xff1a;通过多个HTML页面组合实现整个应用网站的功能。 二、单页与多页对比 单页面应用的主要场景&#xff1…

开源 AI 智能名片 2 + 1 链动模式 S2B2C 商城小程序中积分使用价值的拓展策略

摘要&#xff1a;本文围绕开源 AI 智能名片 2 1 链动模式 S2B2C 商城小程序&#xff0c;深入探讨其积分使用价值的丰富策略。详细分析积分兑换礼品、会员升级、积分抵现等方式在该特定商城小程序环境下的应用特点、存在问题及对用户和商城的影响&#xff0c;旨在为商城的优化运…

C++ | Leetcode C++题解之第526题优美的排列

题目&#xff1a; 题解&#xff1a; class Solution { public:int countArrangement(int n) {vector<int> f(1 << n);f[0] 1;for (int mask 1; mask < (1 << n); mask) {int num __builtin_popcount(mask);for (int i 0; i < n; i) {if (mask &am…

【Linux 25】网络套接字 socket 概念

文章目录 &#x1f308; 一、IP 地址概念⭐ 1. IP 地址的作用⭐ 2. 源 IP 地址和目的 IP 地址 &#x1f308; 二、端口号概念⭐ 1. 源端口号和目的端口号⭐ 2. 端口号范围划分⭐ 3. 端口号 VS 进程 ID⭐ 4. 套接字 socket 的概念 &#x1f308; 三、传输层的典型代表协议⭐ 1. …

利用Docker Compose构建微服务架构

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 利用Docker Compose构建微服务架构 引言 Docker Compose 简介 安装 Docker Compose 创建项目结构 编写 Dockerfile 前端 Dockerf…

【Vue】一个案例带你学会组件通信!!!(1)(父传子props+子传父$emit)

嘿&#xff0c;开发者们&#x1f44b;&#xff01;欢迎来到今天的Vue.js组件通信大冒险。你是否曾在父子组件间的数据同步问题上感到头疼&#xff1f;&#x1f92f; 今天&#xff0c;我们将一起揭开Vue.js父子通信的神秘面纱&#xff0c;学习如何让数据在父子组件间流畅地“跳舞…

在VS中安装chatGPT

2、在VSCode中打开插件窗口 3、输入ChatGPT 4、这里有个ChatGPT中文版&#xff0c;就它了 5、安装 6、这时候侧边栏多了一个chatGPT分页图标&#xff0c;点击它 7、打个招呼 8、好像不行 9、看一下细节描述 10、根据要求按下按下快捷键 Ctrl Shift P 11、切换成国内模式 12、…