【静态分析】在springboot使用太阿(Tai-e)02

参考:使用太阿(Tai-e)进行静态代码安全分析(spring-boot篇二) - 先知社区

 本文章使用的被分析代码为GitHub - JoyChou93/java-sec-code: Java web common vulnerabilities and security code which is base on springboot and spring security

----------------------------------------------------------------------------------

1. 原理分析

1.1 控制反转

什么是控制反转?控制反转(IoC)是一种设计原则,其中应用程序的控制权被反转,由框架或容器负责管理对象的生命周期和控制对象之间的关系。这意味着应用程序不再负责直接实例化和管理对象,而是将这些任务委托给外部的框架或容器。
以如下代码为例:

@Configuration
public class AppConfig {@Beanpublic MyServiceImpl myService() {return new MyServiceImpl();}
}
@RestController
public class MovieRecommenderController {@Autowiredprivate MyServiceImpl service;@GetMapping("/")public void doSomeAction(){service.action();}// ...
}

观察上面两段代码, 发现service的实例化和管理被Spring框架控制,而不是由应用程序直接控制。使用控制反转这样的技术可以使代码的对象的创建与使用解耦,降低组件之间的耦合度,使系统更易于维护、扩展和测试。而依赖注入是IoC的一种实现方式。它是指将一个对象的依赖关系通过构造函数、方法参数或属性注入到对象中,而不是在对象内部硬编码这些依赖关系。通过依赖注入,对象变得更加灵活和可配置。

1.2 指针分析如何处理函数调用?

在Tai-e,污点分析是作为指针分析的一个插件,在指针分析中完成了污点分析。所以在进一步分析控制反转前,我们需要先了解一下指针分析的基础。
指针分析就是计算指针(变量、字段)可以指向哪个对象的过程。指针分析首先需要对New语句进行分析,然后才能对assign、store、load语句进行分析,在此基础上进行指针分析传播,以及后续method call的处理。而对New语句初始化就需要用到堆抽象技术。

1.2.1 堆抽象

指针分析首先要进行堆抽象。对指针所代表的内存内容进行建模,Tai-e使用New Stmt创建一个新的对象(NewObj) 来实现堆抽象,即将指针所代表内容用对象构造时的Stmt代替。如下是构建一个堆抽象的案例。

A a = new A();
PT(a)={NewObj{<New: void main(java.lang.String[])>[0@L4] new A}}

这种堆抽象方法又叫做Allocation-Site abstraction,这是指针分析中最常见的堆抽象方法。

1.2.2 函数调用处理

在指针分析中会碰到许多函数调用,对象会沿着函数调用进行传播。在处理方法调用时时,需要分析出指针调用的具体方法。java的方法调用大概有以下四种,具体所调用方法的解析原理如下:

invokeinterface和invokevirtual
a.f(p1, p2)

当分析如上语句时,a.f所对应的函数就是指针a所对应Method即类A的方法。而在分析控制反转中,无法将service与其调用点关联起来,便无法分析出a.f实际所调用的函数
如上代码便是Tai-e中处理方法调用的相关代码:

private void processCall(CSVar recv, PointsToSet pts) {Context context = recv.getContext();Var var = recv.getVar();for (Invoke callSite : var.getInvokes()) {pts.forEach(recvObj -> {// resolve calleeJMethod callee = CallGraphs.resolveCallee(recvObj.getObject().getType(), callSite);if (callee != null) {// select contextCSCallSite csCallSite = csManager.getCSCallSite(context, callSite);Context calleeContext = contextSelector.selectContext(csCallSite, recvObj, callee);............} else {plugin.onUnresolvedCall(recvObj, context, callSite);}});}
}

首先遍历pts(指针所指向堆抽象的集合),然后根据obj类型与当前函数调用callSite解析出具体调用的方法。

resolveCallee对应的源码如下,如果callSite所调用的方法为interface或者virtual,就会根据obj的类型与方法引用派生出具体的方法。

public static JMethod resolveCallee(Type type, Invoke callSite) {MethodRef methodRef = callSite.getMethodRef();if (callSite.isInterface() || callSite.isVirtual()) {return World.get().getClassHierarchy().dispatch(type, methodRef);} else if (callSite.isSpecial()) {return World.get().getClassHierarchy().dispatch(methodRef.getDeclaringClass(), methodRef);} else if (callSite.isStatic()) {return methodRef.resolveNullable();} else {throw new AnalysisException("Cannot resolve Invoke: " + callSite);}
}

dispatch会调用lookupMethod,其核心逻辑如下:

  1. 遍历超类,如果超类含有该方法,则返回超类中的该方法
  2. 遍历超类的所实现的所有接口,返回接口中的该方法
private JMethod lookupMethod(JClass jclass, Subsignature subsignature,boolean allowAbstract) {// JVM Spec. (11 Ed.), 5.4.3.3 Method Resolution// 1. If C is an interface, method resolution throws// an IncompatibleClassChangeError. TODO: what does this mean???// 2. Otherwise, method resolution attempts to locate the// referenced method in C and its superclassesfor (JClass c = jclass; c != null; c = c.getSuperClass()) {JMethod method = c.getDeclaredMethod(subsignature);if (method != null && (allowAbstract || !method.isAbstract())) {return method;}}// 3. Otherwise, method resolution attempts to locate the// referenced method in the superinterfaces of the specified class Cfor (JClass c = jclass; c != null; c = c.getSuperClass()) {for (JClass iface : c.getInterfaces()) {JMethod method = lookupMethodFromSuperinterfaces(iface, subsignature, allowAbstract);if (method != null) {return method;}}}return null;// TODO://  1. check accessibility//  2. handle phantom methods//  3. double-check correctness
}

2. 问题分析

在使用控制反转时,类的实例化是由spring的IoC容器控制,而不是应用程序自身控制。这使得在指针分析中,无法通过传统的dispatch获取到callsite具体调用的方法,而无法解析控制反转所控制的实例。
如果我们可以分析出当前对象所对应的具体实现类,那便可以在指针分析处理call调用时获取到具体的方法调用。
代码的具体实现如下:

solver.addPointsTo(solver.getCSManager().getCSVar(csMethod.getContext(), var),solver.getHeapModel().getMockObj(() -> "DEPENDENCY_INJECTION", cls.getName(), cls.getType()));

问题的关键就是如何找出当前被注入者所对应的具体实现类

spring支持两种依赖注入的配置方式:

1. 通过注解配置

@Service
public class CustomizedService {@Resourceprivate HttpService httpService;@GetMapping("/restTemplate/vuln1")public String RestTemplateUrlBanRedirects(String url){HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON_UTF8);return httpService.RequestHttpBanRedirects(url, headers);}
}
@Service
public class HttpServiceImpl implements HttpService {public String RequestHttpBanRedirects(String url, HttpHeaders headers) {HttpEntity<String> entity = new HttpEntity<>(headers);ResponseEntity<String> re = restTemplateBanRedirects.exchange(url, HttpMethod.GET, entity, String.class);return re.getBody();}
}

如上代码会将字段httpService的配置为HttpServiceImpl的实例,无需在CustomizedService.java添加构造httpService的代码。

2. 通过xml文件配置

package x.y;public class ThingOne {public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {// ...}
}
<beans><bean id="beanOne" class="x.y.ThingOne"><constructor-arg ref="beanTwo"/><constructor-arg ref="beanThree"/></bean><bean id="beanTwo" class="x.y.ThingTwo"/><bean id="beanThree" class="x.y.ThingThree"/>
</beans>

通过如上配置,便可以将beanTwobeanThree 自动配置为ThingOne构造参数,不需要在ThingOne中添加beanTwobeanThree的构造过程。

3. 控制反转处理

如下为仓库GitHub - JoyChou93/java-sec-code: Java web common vulnerabilities and security code which is base on springboot and spring security中包含SSRF漏洞且利用了控制反转特性的代码片段

@RestController
@RequestMapping("/ssrf")
public class SSRF {private static final Logger logger = LoggerFactory.getLogger(SSRF.class);@Resourceprivate HttpService httpService;@GetMapping("/restTemplate/vuln1")public String RestTemplateUrlBanRedirects(String url){HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON_UTF8);return httpService.RequestHttpBanRedirects(url, headers);}@GetMapping("/restTemplate/vuln2")public String RestTemplateUrl(String url){HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON_UTF8);return httpService.RequestHttp(url, headers);}}

这个SSRF漏洞对应的调用链如下

org.joychou.controller.SSRF#RestTemplateUrlBanRedirects
org.joychou.impl.HttpServiceImpl#RequestHttpBanRedirects
org.springframework.web.client.RestTemplate#exchange

sink点的方法为org.springframework.web.client.RestTemplate#exchange,所以需要把该方法加入污点分析的配置文件。

另一个漏洞的sink点所调用方法与上个漏洞一致,不过调用链有所区别。

org.joychou.controller.SSRF#RestTemplateUrl
org.joychou.impl.HttpServiceImpl#RequestHttp
org.springframework.web.client.RestTemplate#exchange

4. 类CHA(Class Hierarchy Analysis)实现方式

根据原理分析一节中的描述,方法调用是通过堆抽象(用obj代表具体的类)所对应的类型解析出来的,当我们分析出当前指针所对应的实际类时,需要构造如下的MockObj,并将类型配置为我们解析出来的类型。

default Obj getMockObj(Descriptor desc, Object alloc, Type type) {return getMockObj(desc, alloc, type, null);
}
solver.addPointsTo(solver.getCSManager().getCSVar(csMethod.getContext(), var),solver.getHeapModel().getMockObj(() -> "DEPENDENCY_INJECTION", implementor.getName(), implementor.getType()));

实际代码如上,其中implementor为我们所分析出的具体的实现类。本质是将解析出的实际类所代表的MockObj添加至对应指针的指向集中。
具体实现思路如下:
遍历被分析代码中的所有方法,提取出Invoke语句即方法调用语句

具体实现思路如下:
遍历被分析代码中的所有方法,提取出Invoke语句即方法调用语句

solver.getCallGraph().reachableMethods().forEach(csMethod -> {if (csMethod.getMethod().getDeclaringClass().getName().matches("^(java\\.|sun\\.|javax\\.|com\\.sun\\.).+$")){return;}csMethod.getMethod().getIR().getStmts().forEach(stmt -> {})
)

如果语句是静态方法调用或special调用,那不用分析

if(stmt instanceof Invoke invoke &&(invoke.isVirtual() || invoke.isInterface()) &&invoke.getRValue() instanceof InvokeInstanceExp invokeInstanceExp){//执行分析逻辑
}

如果接受者已经有了指向信息,代表接受者并不是由控制反转构造,不用分析

if(solver.getCSManager().getCSVar(context, var).getPointsToSet() != null &&!solver.getCSManager().getCSVar(context, var).getPointsToSet().isEmpty()) {return;
}

如果对象的类型为接口,那么可能的类就是所有实现类;
如果对象的类型为类,而非接口,那么可能的类就是所有子类。

JClass jClass = World.get().getClassHierarchy().getClass(var.getType().getName());
Collection<JClass> implementors = new ArrayList<>();
if(invoke.isInterface()){implementors.addAll(World.get().getClassHierarchy().getDirectImplementorsOf(jClass));
}else{implementors.add(jClass);implementors.addAll(World.get().getClassHierarchy().getDirectSubclassesOf(jClass));
}
System.out.printf("%s %s %s\n", var, jClass, implementors);

最后将可能的类加到对象的指向集中,指针分析会再分析方法调用,找到真实调用的方法

if(implementors.size() <= 3) {implementors.forEach(implementor -> {solver.addPointsTo(solver.getCSManager().getCSVar(csMethod.getContext(), var),solver.getHeapModel().getMockObj(() -> "DEPENDENCY_INJECTION", implementor.getName(), implementor.getType()));});
}

还有最后一个问题,对应的分析代码应该放在指针分析的哪个阶段?

根据DefaultSolver.javaanalyze方法的源码,指针分析会在完毕前调用onPhaseFinish,如果把代码放在此处,此时除控制反转外的其他分析都已经完成,会使我们的判断准确。

private void analyze() {while (!workList.isEmpty() && !isTimeout) {// phase startswhile (!workList.isEmpty() && !isTimeout) {............}plugin.onPhaseFinish();}if (!workList.isEmpty() && isTimeout) {logger.warn("Pointer analysis stops early as it reaches time limit ({} seconds)," +" and the result may be unsound!", timeLimit);} else if (timeLimiter != null) { // finish normally but time limiter is still runningtimeLimiter.stop();}plugin.onFinish();
}

完整代码如下

package pascal.taie.analysis.pta.plugin.taint;import pascal.taie.World;
import pascal.taie.analysis.pta.core.cs.context.Context;
import pascal.taie.analysis.pta.core.solver.Solver;
import pascal.taie.analysis.pta.plugin.Plugin;
import pascal.taie.ir.exp.InvokeInstanceExp;
import pascal.taie.ir.exp.Var;
import pascal.taie.ir.stmt.Invoke;
import pascal.taie.language.classes.JClass;import java.util.ArrayList;
import java.util.Collection;public class DependencyInjectionHandler implements Plugin {private Solver solver;private boolean isCalled;public DependencyInjectionHandler(){isCalled = false;}@Overridepublic void setSolver(Solver solver) {this.solver = solver;}@Overridepublic void onPhaseFinish() {if(isCalled){return;}isCalled = true;solver.getCallGraph().reachableMethods().forEach(csMethod -> {if (csMethod.getMethod().getDeclaringClass().getName().matches("^(java\\.|sun\\.|javax\\.|com\\.sun\\.).+$")){return;}csMethod.getMethod().getIR().getStmts().forEach(stmt -> {if(stmt instanceof Invoke invoke &&(invoke.isVirtual() || invoke.isInterface()) &&invoke.getRValue() instanceof InvokeInstanceExp invokeInstanceExp){Var var = invokeInstanceExp.getBase();Context context = csMethod.getContext();if(solver.getCSManager().getCSVar(context, var).getPointsToSet() != null &&!solver.getCSManager().getCSVar(context, var).getPointsToSet().isEmpty()) {return;}JClass jClass = World.get().getClassHierarchy().getClass(var.getType().getName());Collection<JClass> implementors = new ArrayList<>();if(invoke.isInterface()){implementors.addAll(World.get().getClassHierarchy().getDirectImplementorsOf(jClass));}else{implementors.add(jClass);implementors.addAll(World.get().getClassHierarchy().getDirectSubclassesOf(jClass));}if(invoke.toString().contains("RequestHttp")){System.out.printf("%s %s %s\n", var, jClass, implementors);}if(implementors.size() <= 3) {implementors.forEach(implementor -> {solver.addPointsTo(solver.getCSManager().getCSVar(csMethod.getContext(), var),solver.getHeapModel().getMockObj(() -> "DEPENDENCY_INJECTION", implementor.getName(), implementor.getType()));});}}});});}
}

该方法可以扫描出依赖于控制反转的漏洞,但是上述方式缺乏准确性,如果一个接口有多个实现类,就无法准确识别出该指针所对应的实现类。

5. 基于注解的配置解析

在代码中,控制反转是通过xml、注解进行实际配置。因此通过解析xml、注解的实际含义,会使结果更加准确。

本文只分析基于注解的配置解析。
实现思路为识别注入点以及依赖实现类,然后将依赖实现类构造成的obj添加到注入点变量的的指向集中。

对于如下代码而言,需要识别出CustomizedService的注入点httpService,然后找到HttpService的实现类,最后把实现类HttpServiceImpl以obj的形式添加到变量的指向集中。

@Service
public class CustomizedService {@Resourceprivate HttpService httpService;@GetMapping("/restTemplate/vuln1")public String RestTemplateUrlBanRedirects(String url){HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON_UTF8);return httpService.RequestHttpBanRedirects(url, headers);}
}
@Service
public class HttpServiceImpl implements HttpService {public String RequestHttpBanRedirects(String url, HttpHeaders headers) {HttpEntity<String> entity = new HttpEntity<>(headers);ResponseEntity<String> re = restTemplateBanRedirects.exchange(url, HttpMethod.GET, entity, String.class);return re.getBody();}
}

实现方法如下:

5.1 注入点识别

List<JField> injectedFields = new ArrayList<>();
World.get().getClassHierarchy().allClasses().map(JClass::getDeclaredFields).flatMap(Collection::stream).forEach(field -> {boolean isInjectedField = field.hasAnnotation("javax.annotation.Resource") ||field.hasAnnotation("org.springframework.beans.factory.annotation.Autowired") ||field.hasAnnotation("javax.inject.Inject");if(isInjectedField){injectedFields.add(field);}});

遍历所有类的所有字段,若字段包含以下注解,则说明该字段是注入点

  • javax.annotation.Resource
  • org.springframework.beans.factory.annotation.Autowired
  • javax.inject.Inject

5.2 寻找实现类

List<JClass> implementationClasses = new ArrayList<>();
implementationClasses.addAll(World.get().getClassHierarchy().allClasses().filter(cls -> cls.hasAnnotation("org.springframework.stereotype.Service") ||cls.hasAnnotation("org.springframework.stereotype.Component")).collect(Collectors.toSet())
);

遍历所有类,若类包含以下注解,则说明该类为实现类

  • org.springframework.stereotype.Service
  • org.springframework.stereotype.Component

5.3 将实现类与注入点关联起来

如果在方法中要使用当前类的字段,会从%this变量中载入该字段,然后使用。
[4@L281] $r4 = %this.<org.joychou.controller.SSRF: org.joychou.service.HttpService httpService>;

@org.springframework.web.bind.annotation.GetMapping({"/restTemplate/vuln1"})
public java.lang.String RestTemplateUrlBanRedirects(java.lang.String url) {org.springframework.http.HttpHeaders $r0;org.springframework.http.MediaType $r1;org.joychou.service.HttpService $r4;java.lang.String $r5;[0@L279] $r0 = new org.springframework.http.HttpHeaders;[1@L279] invokespecial $r0.<org.springframework.http.HttpHeaders: void <init>()>();[2@L280] $r1 = <org.springframework.http.MediaType: org.springframework.http.MediaType APPLICATION_JSON_UTF8>;[3@L280] invokevirtual $r0.<org.springframework.http.HttpHeaders: void setContentType(org.springframework.http.MediaType)>($r1);[4@L281] $r4 = %this.<org.joychou.controller.SSRF: org.joychou.service.HttpService httpService>;[5@L281] $r5 = invokeinterface $r4.<org.joychou.service.HttpService: java.lang.String RequestHttpBanRedirects(java.lang.String,org.springframework.http.HttpHeaders)>(url, $r0);[6@L281] return $r5;
}

如果要将实现类与注入点关联起来,就相当于把实现类与LoadField的返回值关联起来。具体关联代码如下:

injectedFields.stream().forEach(field -> {JClass jClass = field.getDeclaringClass();Collection<JClass> subClasses = World.get().getClassHierarchy().getAllSubclassesOf(World.get().getClassHierarchy().getClass(field.getType().getName()));List<JClass> implementors = new ArrayList<>(subClasses);implementors.retainAll(implementationClasses);System.out.printf("%s %s\n", field, implementors);Set<CSMethod> csMethodSet = solver.getCallGraph().reachableMethods().filter(csMethod -> csMethod.getMethod().getDeclaringClass().equals(jClass)).collect(Collectors.toSet());csMethodSet.forEach(csMethod -> {List<Var> vars = csMethod.getMethod().getIR().getStmts().stream().filter(stmt -> stmt instanceof LoadField loadField &&loadField.getFieldAccess().getFieldRef().resolve().equals(field)).map(stmt -> (LoadField) stmt).map(AssignStmt::getLValue).toList();implementors.forEach(implementor -> {vars.forEach(var -> {solver.addPointsTo(solver.getCSManager().getCSVar(csMethod.getContext(), var),solver.getHeapModel().getMockObj(() -> "DEPENDENCY_INJECTION", implementor.getName(), implementor.getType()));});});});
});
  1. 获取注入点字段的所属类,然后获取该类中所有上下文有关的方法;
  2. 获取字段类型的所有子类,如果是接口,则获取所有实现类。与之前所有的实现类取交集,这个交集就是控制反转中该字段的实现类;
  3. 遍历第一步获取到的方法的所有语句,若语句的类型是LoadField,则判断其FieldAccess的字段是否为注入点的字段,若是,则用第二步的实现类构造MockObj,将其添加到LoadField语句的返回值的指向集中。

6. 结果展示

lcark、keanu两位大佬更改的相关代码以及配置文件已经上传至github
具体食用方法如下:

 

1. 下载代码,并移动至spring-boot-2目录下

    git clone https://github.com/lcark/Tai-e-demo
    cd Tai-e-demo/spring-boot-2
    git submodule update --init

2. 将java-sec-code文件夹移至与Tai-e-demo文件夹相同目录下

3. 将DependencyInjectionHandler.java移动至Tai-e源码的src/main/java/pascal/taie/analysis/pta/plugin/taint/目录下,并重新编译打包

4. 使用如下命令运行tai-e便可以成功获取到扫描结果

java -cp xxx\tai-e-all-0.5.1-SNAPSHOT.jar pascal.taie.Main --options-file=options.yml

5. 如下图所示,两个测试案例都被检测到

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

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

相关文章

本地部署 MiniCPM-Llama3-V 2.5

本地部署 MiniCPM-Llama3-V 2.5 0. 引言1. 性能评估2. 典型示例3. 本地部署4. 运行 WebUI Demo5. vLLM 部署 0. 引言 MiniCPM-Llama3-V 2.5 是 MiniCPM-V 系列的最新版本模型&#xff0c;基于 SigLip-400M 和 Llama3-8B-Instruct 构建&#xff0c;共 8B 参数量&#xff0c;相较…

Llama模型家族训练奖励模型Reward Model技术及代码实战(三) 使用 TRL 训练奖励模型

LlaMA 3 系列博客 基于 LlaMA 3 LangGraph 在windows本地部署大模型 &#xff08;一&#xff09; 基于 LlaMA 3 LangGraph 在windows本地部署大模型 &#xff08;二&#xff09; 基于 LlaMA 3 LangGraph 在windows本地部署大模型 &#xff08;三&#xff09; 基于 LlaMA…

闲话 .NET(3):.NET Framework 的缺点

前言 2016 年&#xff0c;微软正式推出 .NET Core 1.0&#xff0c;并在 2019 年全面停止 .NET Framework 的更新。 .NET Core 并不是 .NET Framework 的升级版&#xff0c;而是一个从头开始开发的全新平台&#xff0c;一个跟 .NET Framework 截然不同的开源技术框架。 微软为…

2024第三届AIGC开发者大会圆桌论坛:AI Agent中国落地发展现状及多模态结合具身智能的发展展望

在2024年第三届AIGC开发者大会上&#xff0c;多位业内专家齐聚一堂&#xff0c;共同探讨了AI Agent在中国的落地发展现状以及多模态结合具身智能的发展前景。本次圆桌论坛的嘉宾包括&#xff1a; Fast JP作者于金龙Agent创始人莫西莫必胜作者秦瑞January Agent创始人李晨 多模…

Android NDK系列(一)手动搭建Native Project

使用NDK编写的本地代码具有高性能等特性&#xff0c;在游戏、图形处理等领域有广泛应用&#xff0c;下面介绍如何手动搭建一个纯C版的Android项目&#xff0c;通过该项目可以理解Android的项目结构。 一、创建settings.gradle Android项目是基于Gradle构建的&#xff0c;首先得…

Captura完全免费的电脑录屏软件

一、简介 1、Captura 是一款免费开源的电脑录屏软件&#xff0c;允许用户捕捉电脑屏幕上的任意区域、窗口、甚至是全屏画面&#xff0c;并将这些画面录制为视频文件。这款软件具有多种功能&#xff0c;例如可以设置是否显示鼠标、记录鼠标点击、键盘按键、计时器以及声音等。此…

JVM1.8分代的理论基础和简单测试

你好&#xff0c;我是 shengjk1&#xff0c;多年大厂经验&#xff0c;努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注&#xff01;你会有如下收益&#xff1a; 了解大厂经验拥有和大厂相匹配的技术等 希望看什么&#xff0c;评论或者私信告诉我&#xff01; 文章目录 一…

海外仓系统哪家好?闭坑指南,擦亮眼睛选对系统

可以说现在的海外仓系统市场还是比较杂乱的&#xff0c;各种不同类型&#xff0c;不同收费标准的系统比比皆是&#xff0c;这让很多想引进海外仓系统的企业不知所措&#xff0c;不知道怎么选。 今天就聊一下在选择海外仓系统的时候应该如何考量&#xff0c;才能避免被坑&#…

C++之对象的使用

1、static成员 2、static成员优点 2、static成员函数 静态成员函数不能访问非静态成员原因&#xff1a;因为没有this指针。也不可以访问非静态成员函数。 可以通过对象来访问静态成员&#xff0c;但是不推荐这么使用&#xff0c;会让人误解成这个x_是属于对象的&#xff0c;但…

PyCharm基本配置内容

如何更换 Python 解释器 输入一段代码点击运行后&#xff0c;画面下方有一个路径如图中框中所示&#xff1a; 上面的路径为虚拟路径&#xff0c;可以改为我们自己设置的路径 点击设置&#xff0c;选择settings 选择Project&#xff1a;y002———》Python Interpreter&#…

python爬虫之pandas库——数据清洗

安装pandas库 pip install pandas pandas库操作文件 已知在本地桌面有一名为Python开发岗位的csv文件(如果是excel文件可以做简单修改即可&#xff0c;道理是通用的) 打开文件&#xff1a; 打开文件并查看文件内容 from pandas import DataFrame import pandas as pd data_c…

【自动驾驶技术栈学习】2-软件《大话自动驾驶》| 综述要点总结 by.Akaxi

----------------------------------------------------------------------------------------------------------------- 致谢&#xff1a;感谢十一号线人老师的《大话自动驾驶》书籍&#xff0c;收获颇丰 链接&#xff1a;大话自动驾驶 (豆瓣) (douban.com) -------------…

nuxt3+Element Plus项目搭建过程记录

背景 本文只记录项目搭建过程中遇到的一些问题和关键点&#xff0c;nuxt框架的说明和API请参照官网学习 官网&#xff1a;https://nuxt.com/docs/getting-started/introduction 1. 初始化项目 指令如下: npx nuxilatest init <project-name>我在安装过程中出现报错&a…

本地源码方式部署启动MaxKB知识库问答系统,一篇文章搞定!

MaxKB 是一款基于 LLM 大语言模型的知识库问答系统。MaxKB Max Knowledge Base&#xff0c;旨在成为企业的最强大脑。 开箱即用&#xff1a;支持直接上传文档、自动爬取在线文档&#xff0c;支持文本自动拆分、向量化、RAG&#xff08;检索增强生成&#xff09;&#xff0c;智…

AI视频智能分析技术赋能营业厅:智慧化管理与效率新突破

一、方案背景 随着信息技术的快速发展&#xff0c;图像和视频分析技术已广泛应用于各行各业&#xff0c;特别是在营业厅场景中&#xff0c;该技术能够有效提升服务质量、优化客户体验&#xff0c;并提高安全保障水平。TSINGSEE青犀智慧营业厅视频管理方案旨在探讨视频监控和视…

七人拼购新模式:革新购物体验,共创价值

在数字时代&#xff0c;消费者的购物体验正经历着前所未有的变革。七人拼购模式作为一种新兴的购物方式&#xff0c;通过汇集消费者的力量&#xff0c;实现商品价格的最优化&#xff0c;让消费者享受到前所未有的实惠与便利。以下&#xff0c;我们将以一款标价499元的商品为例&…

消防体验馆升级,互动媒体点亮安全之路!

在当下这个科技日新月异的时代&#xff0c;多媒体互动技术已深深融入现代化消防体验馆的设计之中&#xff0c;它们不仅为这些场馆注入了前所未有的创意与活力&#xff0c;更通过其互动性、趣味性等独特优势&#xff0c;彻底革新了消防宣传教育的传统模式。如今&#xff0c;这种…

联想打印APP添加打印机方法

联想打印APP添加打印机操作方法&#xff1a; 1、在手机上下载“联想打印”APP&#xff1b; 2、打开“联想打印”APP,然后在软件内右下角找到“我的”图标并选择&#xff1b; 3、点击“请登录/注册”&#xff1b; 4、勾选“我已阅读并同意”然后在上面填写手机号码后&#xff0…

Ansys Speos|微光学结构尾灯设计

附件下载 联系工作人员获取附件 汽车照明行业在过去几年中有了很大的发展&#xff0c;对复杂光学结构的需求需要先进的设计能力。Speos 3D Texture是一个独特的功能&#xff0c;允许在给定的身体表面以图案的形式设计和模拟微纹理。它的优点依赖于图案(网格)的光学模拟模型&a…

Java—二分查找

介绍 二分查找&#xff08;Binary Search&#xff09;是一种在有序数组中查找特定元素的搜索算法。其基本思想是将目标值与数组中间的元素进行比较&#xff1a; 如果目标值等于中间元素&#xff0c;则查找成功。如果目标值小于中间元素&#xff0c;则在数组左半部分继续进行二…