Java安全基础之Java Web核心技术

这篇具有很好参考价值的文章主要介绍了Java安全基础之Java Web核心技术。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录
  • Java EE
  • Java MVC
  • Servlet
  • JSP
  • Filter
  • JDBC
  • RMI
  • JNDI

Java EE

Java 平台有 3 个主要版本:

  • Java SE(Java Platform Standard Edition,Java平台标准版)

  • Java EE(Java Platform Enterprise Edition,Java 平台企业版)

  • Java ME(Java Platform Micro Edition,Java 平台微型版)

其中 Java EE 是 Java 应用最广泛的版本。Java EE 也称为 Java 2 Platform 或 Enterprise Edition(J2EE),它提供了一套全面的技术规范和API,用于构建分布式、可伸缩、安全的企业级应用程序。

几乎所有的 Java Web 应用都是基于 Java EE 平台开发。

Java MVC

当谈到 Web 应用的时候就不得不提到大名鼎鼎的 MVC。

MVC(Model-View-Controller)框架是一种设计模式,它将应用程序分为三个核心部分:模型(Model)、视图(View)和控制器(Controller)。目的是更好地组织和管理应用程序的代码以提高代码的可维护性、可扩展性和可重用性。

MVC 工作流程

首先 Controller 层接收用户的请求,并决定应该调用哪个 Model 来进行处理。然后由 Model 使用逻辑处理用户的请求并返回数据。最后返回的数据通过 View 层呈现给用户。

Java安全基础之Java Web核心技术

MVC 的主要优势

  • 分离关注点: MVC 将应用程序的数据逻辑、用户界面和用户交互分离开来,降低耦合度,开发者可以更加关注各自的功能。

  • 可重用性: MVC 将应用程序分为模型、视图和控制器,每个部分都可以独立开发,可以在不同的项目中重复使用。

  • 易于维护: MVC 使代码分为不同的模块,每个模块都有特定的责任,使得应用程序的维护更加简单。

MVC 充分展现了 低耦合(Low Coupling)和 高内聚(High Cohesion)这两个重要的软件设计原则。

Java MVC 模式与普通 MVC 的区别不大:

  • 模型(Model):负责管理数据的状态和行为,以及处理与数据相关的操作。模型通常包括实体类、数据访问对象(DAO)、业务逻辑层等组件。

  • 视图(View):负责展示应用程序的用户界面。它将模型中的数据以可视化的形式呈现给用户,并负责接收用户的输入。

  • 控制器(Controller):控制器充当模型和视图之间的中介,负责处理用户的请求并作出相应的响应。控制器通常由Java类实现,处理URL映射和请求路由。

比较常见的一些Java MVC 框架:Struts 2、Spring MVC、JSF 等。

Servlet

Servlet 毫不夸张地说可以是 Java EE 的核心技术,也是所有 MVC 框架的实现的根本。

Servlet 主要用于创建 Web 应用程序中的服务器端组件,能够接收来自客户端(浏览器)的请求,并生成相应的响应。使用 Servlet 来处理一些较为复杂的服务器端的业务逻辑。

Servlet 的配置

Servlet 的配置有两种方式:

  1. Servlet 3.0 之前的版本都是在 web.xml 中配置。

  2. Servlet 3.0 之后的版本使用注解来配置。

在 web.xml 中,Servlet 的配置在 Servlet 标签中,Servlet 标签是由 Servlet 和 Servlet-mapping 标签组成,两者通过在 Servlet 和 Servlet-mapping 标签中相同的 Servlet-name 名称实现关联。

比如这样:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>com.example.HelloServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>HelloServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>

</web-app>

<servlet> 用于定义一个 Servlet,其中 <servlet-name> 标签用于指定 Servlet 的名称,<servlet-mapping> 标签用于将 Servlet 映射到 URL 地址。

使用注解配置,在 Servlet 类中直接使用注解来定义 Servlet 的属性和映射关系。

比如这样:

@WebServlet(name = "HelloServlet", urlPatterns = {"/hello"})
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
		Code...
    }
}

使用 @WebServlet 注解来定义 Servlet,也可以指定 Servlet 的名称和 URL 映射等。

Servlet 工作流程&生命周期

1、加载和初始化阶段:

  • Servlet 容器启动时,会加载部署配置的 Servlet 类。

  • 加载后,容器会实例化 Servlet 类,并调用其 init() 方法进行初始化。

    在 init() 方法中,可以进行一些初始化操作,如读取配置文件、建立数据库连接等。init() 方法只会在 Servlet 的生命周期中被调用一次。

2、请求处理阶段:

  • 当客户端发送请求到达时,Servlet 容器会根据配置信息,将请求映射到相应的 Servlet。

  • Servlet 容器会创建一个 HttpServletRequest 对象和一个 HttpServletResponse 对象,并将它们传递给相应的 Servlet 的 service() 方法。

    在 service() 方法中,Servlet 根据请求类型(GET、POST 等)调用相应的处理方法,如 doGet()、doPost() 等。

3、销毁阶段:

  • 当 Servlet 容器关闭或者 Servlet 长时间不被使用时,容器会调用 Servlet 的 destroy() 方法进行销毁。

    在 destroy() 方法中,开发者可以进行一些清理操作,如关闭数据库连接、释放资源等。

在 Servlet 的整个生命周期中,init() 和 destroy() 方法只会被调用一次,而 service() 方法会根据请求的到达而被多次调用。整个生命周期的管理由 Servlet 容器负责。

Java安全基础之Java Web核心技术

JSP

JSP (JavaServer Pages) 是与 PHP、ASP 等类似的脚本语言,JSP 是为了简化 Servlet 的处理流程而出现的替代品。

在 JSP 中可以直接调用 Java 代码,这就导致了一些安全问题,比如一些 JSP 的Webshell。虽然说现在比较新的 Java MVC 框架中已经放弃了 JSP,但还是需要稍稍了解一点。

工作原理

从本质上说 JSP 就是一个Servlet,在 JSP 页面在第一次被访问时会被 Servlet 容器(如 Tomcat)编译成一个特殊的 Servlet,并在服务器上运行。

当客户端请求一个 JSP 页面时,Servlet 容器将 JSP 页面转换成一个 Servlet,并执行其中的 Java 代码。然后,Servlet 生成 HTML 页面,并将其发送给客户端。

JSP 的基本语法

指令

  • <%@ 开头,以 %> 结尾,用于设置全局的 JSP 属性引入 Java 类库等。

  • <%@ page ... %> 定义网页依赖属性,比如脚本语言、error页面、缓存需求等。

  • <%@ include ... %> 包含其他文件(静态包含)。

脚本:

  • <% 开头,以 %> 结尾,用于插入 Java 代码块,可以在其中编写任意的 Java 代码。

表达式:

  • <%= 开头,以 %> 结尾,用于输出 Java 表达式的结果到页面上。

EL 表达式

EL(Expression Language)表达式,常用于在 JSP 页面中插入和操作数据。

可以直接访问 JavaBean 对象的属性,例如 ${user.name} 可以获取名为 "name" 的属性值。

可以调用 JavaBean 对象的方法,并获取返回值。例如 ${user.getName()} 可以调用 "getName" 方法并获取返回值。

Filter

在 Java Servlet 中,过滤器(Filter)是一种用于在请求被处理之前或之后执行某些任务的组件。主要用于过滤 URL 请求,通过 Filter 我们可以实现 URL 请求资源权限验证、用户登陆检测等功能。

Filter 的生命周期由容器管理,通过实现 javax.servlet.Filter 接口来创建,可以在 web.xml 或者使用注解来对 Filter 进行配置。

实现一个 Filter 只需要重写 init()、doFilter()、destroy() 方法,过滤逻辑都在 doFilter 方法中实现。

比如这样:

public class MyFilter implements Filter {
    public void init(FilterConfig config) throws ServletException {
        // 过滤器初始化时执行的操作
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 执行过滤逻辑,对请求进行处理
        chain.doFilter(request, response); // 将请求传递给下一个过滤器或目标 Servlet
        // 在此可对响应进行处理
    }

    public void destroy() {
        // 过滤器销毁时执行的操作
    }
}

对于基于 Filter 和 Servlet 实现的项目,代码审计的重心集中于找出所有的 Filter 分析其过滤规则,找出是否有做全局的安全过滤、敏感的 URL 地址是否有做权限校验并尝试绕过 Filter 过滤。

Filter 和 Servlet

Filter 和 Servlet 基础概念不一样,Servlet 定义是容器端小程序,用于直接处理后端业务逻辑,而 Filter 的思想则是实现对 Java Web 请求资源的拦截过滤。

filter 的生命周期与 Servlet 的生命周期比较类似,在一个生命周期中,filter 和 Servlet 都经历了被加载、初始化、提供服务及销毁的过程。

JDBC

JDBC (Java Database Connectivity) 是 Java 语言访问数据库的标准 API。

使用 JDBC 连接数据库通常包括以下步骤:

1、加载数据库驱动程序:使用 Class.forName() 方法加载数据库驱动程序,使 JVM 能够与数据库通信。

2、建立数据库连接:使用 DriverManager.getConnection() 方法建立与数据库的连接。

3、创建和执行 SQL 语句:创建一个 Statement 对象或者 PreparedStatement 对象,用于执行 SQL 查询或更新操作。

4、处理结果集:如果执行的是查询操作,那么将返回一个结果集 ResultSet 对象,通过遍历该结果集来获取查询结果。

5、关闭连接和资源:在使用完数据库连接和相关资源后,需要关闭它们以释放数据库资源。

import java.sql.*;

public class JDBCExample {
    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;

        try {
            // 1. 加载数据库驱动程序
            Class.forName("com.mysql.cj.jdbc.Driver");

            // 2. 建立数据库连接
            String url = "jdbc:mysql://localhost:3306/mydatabase";
            String username = "root";
            String password = "password";
            conn = DriverManager.getConnection(url, username, password);

            // 3. 创建并执行 SQL 查询
            stmt = conn.createStatement();
            String sql = "SELECT * FROM mytable";
            rs = stmt.executeQuery(sql);

            // 4. 处理结果集
            while (rs.next()) {
                // 处理每一行数据
                int id = rs.getInt("id");
                String name = rs.getString("name");
                System.out.println("ID: " + id + ", Name: " + name);
            }
        } catch (SQLException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            // 5. 关闭连接和资源
            try {
                if (rs != null) rs.close();
                if (stmt != null) stmt.close();
                if (conn != null) conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

为什么第一步需要 Class.forName ?

这一步是利用了 Java 反射和类加载机制往 DriverManager 中注册了驱动包。注册驱动程序是为了使得 DriverManager 能够识别和管理特定的数据库驱动程序。

真实的 Java 项目中通常不会使用原生的 JDBC 的 DriverManager 去连接数据库,而是使用数据源 (javax.sql.DataSource) 来代替 DriverManager 管理数据库的连接。JDK 不提供 DataSource 的具体实现,而它的实现来源于各个驱动程序供应商或数据库访问框架,例如 Spring JDBC、Tomcat JDBC、MyBatis、Druid、C3P0、Seata 等。

JDBC 有两种方法执行 SQL 语句,分别为 Statement 和 PrepareStatement。

  • Statement 是 Java 中执行静态 SQL 语句的接口,每次执行 SQL 语句时都会将 SQL 语句发送给数据库进行解析和编译。

  • PreparedStatement 是 Statement 的子接口,执行时 SQL 语句时会被预先编译,可以通过设置参数来动态地填充 SQL 语句中的占位符。

正确使用 PrepareStatement 可以有效避免 SQL 注入的产生,使用 “?” 作为占位符时,填入对应字段的值会进行严格的类型检查。

Mysql 预编译

Mysql 默认也提供了预编译命令 prepare,使用 prepare 命令可以在 Mysql 数据库服务端实现预编译查询。

RMI

RMI(Remote Method Invocation,远程方法调用)是 Java 中用于实现远程通信的机制。它允许在不同的 Java 虚拟机(JVM)之间调用方法,就像调用本地方法一样。

RMI 的工作原理

RMI 有一个 Client 端和一个 Server 端。

Client 端有一个本地代理对象被称为 Stub,负责将方法调用参数序列化为网络消息,并将其发送到远程服务段。

Server 端有一个接收这个消息的对象被称为 Skeleton,负责将接收到的网络消息反序列化为方法调用,并将其传递给实际的远程对象进行处理。

Java安全基础之Java Web核心技术

这种 Stub 和 Skeleton 的机制使得远程调用在客户端和服务端之间建立了一个中介,隐藏了底层通信的细节,简化了远程方法调用的实现。

假设我们定义一个远程接口 RemoteInterface:

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RemoteInterface extends Remote {
    String sayHello() throws RemoteException;
}

我们实现这个接口的远程对象 RemoteObject:

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
    public RemoteObject() throws RemoteException {
        super();
    }

    @Override
    public String sayHello() throws RemoteException {
        return "Hello from RemoteObject!";
    }
}

在 RMI 服务端注册启动:

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
    public static void main(String[] args) throws Exception {
        RemoteObject remoteObject = new RemoteObject();
        // 注册 RMI 端口
        Registry registry = LocateRegistry.createRegistry(10099);
        // 绑定 Remote 对象
        registry.rebind("RemoteObject", remoteObject);
        System.out.println("Server started.");
    }
}

在客户端调用远程方法:

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
    public static void main(String[] args) throws Exception {
        // 获取到指定主机和端口上运行的 RMI 注册表的引用
        Registry registry = LocateRegistry.getRegistry("localhost", 10099);
        // 通过注册表查找远程对象
        RemoteInterface remoteObject = (RemoteInterface) registry.lookup("RemoteObject");
        // 代码调用远程对象上的 sayHello() 方法
        String message = remoteObject.sayHello();
        System.out.println("Message from server: " + message);
    }
}

启动服务端,当客户端调用 sayHello() 方法时,远程服务器上的 RemoteObject 对象将被调用。

Java安全基础之Java Web核心技术

注意:RMI 通信中所有的对象都是通过 Java 序列化传输的,只要有Java对象反序列化操作就有可能有漏洞。

JNDI

JNDI(Java Naming and Directory Interface)是 Java 提供的一种用于访问命名和目录服务的 API。通过调用 JNDI 的 API 应用程序可以定位资源和其他程序对象。这些对象可以存储在不同的命名或目录服务中,例如 RMI、LDAP、DNS、JDBC、CORBA、NIS。

这看起来似乎不太好理解,其实 JNDI 本质上是一个让配置参数和代码解耦的一种规范和思想

比如,JDBC来连接数据库,我们可以选择在代码中直接写入数据库的连接参数,旦如果数据源发生更改,就必须要改动代码后重新编译才能连接。如果将连接参数改成外部配置的方式,就实现了配置和代码之间的解耦。

JNDI 命名和目录服务

  1. Naming Service

命名服务是将名称和对象进行关联,提供通过名称找到对象的操作,称为"绑定"。

  1. Directory Service

目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录容器环境中保存的是对象的属性信息。

JNDI 的工作原理

  1. 上下文(Context):JNDI 中一个上下文是一系列名称和对象的绑定的集合,应用程序通过上下文来查找和访问对象。

  2. 命名服务提供者:JNDI 使用命名服务提供者来实现对不同命名服务的访问,不同的命名服务有对应的命名服务提供者。

  3. JNDI API:Java 应用程序通过 JNDI API 来与上下文和命名服务提供者进行交互,执行资源查找和操作。

如何使用 JNDI 来查找和访问一个命名和目录服务中的对象(假设这个对象是一个字符串):

// 1. 创建 InitialContext 对象
Context ctx = new InitialContext();

// 2. 指定 JNDI 名称( JNDI 路径)
String jndiName = "java:/comp/env/myString";

// 3. 查找对象
String myString = (String) ctx.lookup(jndiName);

// 4. 访问对象
System.out.println("Found string: " + myString);

// 5. 关闭 InitialContext
ctx.close();

有了 JDNI 之后,我们可以将一些与业务无关的配置转移到外部,更好的方便项目的维护。

JNDI RMI 远程方法调用

JNDI 和 RMI 结合使用时,可以通过 JNDI 来查找远程对象的引用,然后使用 RMI 来调用远程对象的方法。

在服务器端:

  • 创建远程对象的实现,并将其导出为 RMI 服务。

  • 将远程对象的引用绑定到 JNDI 目录中,以便客户端能够查找到它。

在客户端:

  • 使用 JNDI 查找远程对象的引用。

  • 通过 RMI 调用远程对象的方法。

接着使用上面的 RMI 服务器端:

import java.rmi.server.UnicastRemoteObject;
import javax.naming.Context;
import javax.naming.InitialContext;

public class JRServer {
    public static void main(String[] args) throws Exception {
        // 创建远程对象的实现
        RemoteObject remoteObject = new RemoteObject();

        // 导出远程对象为 RMI 服务
        RemoteObject stub = (RemoteObject) UnicastRemoteObject.exportObject(remoteObject, 0);

        // 将远程对象的引用绑定到 JNDI 目录中
        Context namingContext = new InitialContext();
        namingContext.bind("rmi://localhost/RemoteObject", stub);

        System.out.println("Server started.");
    }
}

在客户端:

import javax.naming.Context;
import javax.naming.InitialContext;

public class JRClient {
    public static void main(String[] args) throws Exception {
        // 使用 JNDI 查找远程对象的引用
        Context namingContext = new InitialContext();
        RemoteObject remoteObject = (RemoteObject) namingContext.lookup("rmi://localhost/RemoteObject");

        // 通过 RMI 调用远程对象的方法
        String message = remoteObject.sayHello();
        System.out.println("Message from server: " + message);
    }
}

使用 JNDI 查找了名为 "rmi://localhost/RemoteObject" 的远程对象的引用,然后通过 RMI 调用了远程对象的 sayHello() 方法。


若有错误,欢迎指正!o( ̄▽ ̄)ブ文章来源地址https://www.toymoban.com/news/detail-855163.html

到了这里,关于Java安全基础之Java Web核心技术的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • WEB安全基础入门—目录遍历(路径遍历\路径穿越攻击)

    欢迎关注订阅专栏! WEB安全系列包括如下三个专栏: 《WEB安全基础-服务器端漏洞》 《WEB安全基础-客户端漏洞》 《WEB安全高级-综合利用》 知识点全面细致,逻辑清晰、结合实战,并配有大量练习靶场,让你读一篇、练一篇,掌握一篇,在学习路上事半功倍,少走弯路! 欢

    2024年02月02日
    浏览(49)
  • Servlet路径问题(“/“到底代表什么)-“web应用程序的根目录“与“web站点的根目录“

    JavaWeb——Servlet路径问题(\\\"/\\\"到底代表什么) 在JavaWeb中,使用路径时出现了大量的\\\"/“,而每次使用”“时都感觉其代表的含义是不同的,因此,本篇文章将汇总JavaWeb中的”\\\"出现情况及其所代表的含义。 了解这些\\\"/\\\"含义之前,先来了解绝对路径与相对路径的概念。 提前说一下

    2023年04月25日
    浏览(37)
  • java web期末课程设计 学生成绩管理系统(mysql,jstl+el,Javabean)+增删改查,session域+servlet(基础易理解)

    学生成绩管理系统(源码地址在评论区需要的小伙伴可以看看,大二时做的,做得不太好) 为了更好的学习理解JavaWeb,创建了此系统。此系统可以帮助学习Java web基础,特别是javaweb入门,此系统使用大部分Java web基础知识。Java web课程设计联系了前端的HTML、CSS、JAVASCRIPT、JSP等

    2024年02月09日
    浏览(68)
  • Java Servlet 技术

    一、Servlet 简介 Servlet 是 JavaEE 的规范之一,通俗的来说就是 Java 接口,将来我们可以定义 Java 类来实现这个接口,并由 Web 服务器运行 Servlet ,所以 TomCat 又被称作 Servlet 容器。 Servlet 提供了动态 Web 资源开发技术,一种可以将网页数据提交到 Java 代码,并且将 Java 程序的数

    2024年02月11日
    浏览(40)
  • 博客系统 Java Web 开发(Servlet)

    目录 一、准备工作 二、设计数据库 三、编写数据库代码 1、建表sql 2、封装数据库的连接操作 3、创建实体类 4、封装数据库的一些增删改查 (1)BlogDao 新增博客:  根据博客 id 来查询指定博客(用于博客详情页) 直接查询出数据库中所有的博客列表 删除博客 (2)UserDao

    2024年02月10日
    浏览(44)
  • WEB核心【会话技术】第十五章

    目录 💂 个人主页:  爱吃豆的土豆 🤟 版权:  本文由【爱吃豆的土豆】原创、在CSDN首发、需要转载请联系博主 💬 如果文章对你有帮助、 欢迎关注、点赞、收藏(一键三连)和订阅专栏哦 🏆 人必有所执,方能有所成! 🐋希望大家多多支持😘一起进步呀! 1,会话技术   

    2023年04月17日
    浏览(37)
  • web安全之目录遍历

    目录遍历(也称为文件路径遍历)是一个 Web 安全漏洞,允许攻击者读取运行应用程序的服务器上的任意文件。这可能包括应用程序代码和数据、后端系统的凭据以及敏感的操作系统文件。在某些情况下,攻击者可能能够写入服务器上的任意文件,从而允许他们修改应用程序

    2023年04月08日
    浏览(37)
  • web安全之目录穿越

    简介: 目录穿越(又称目录遍历diretory traversal/path traversal)是通过目录控制序列 ../ 或者文件绝对路径来访问存储在文件系统上的任意文件和目录的一种漏洞。 原理: web应用对于url请求没有进行合理的审查与过滤,导致构造目录控制序列直接传入文件系统apl。 危害: 对于存

    2024年02月08日
    浏览(34)
  • 【HTTP 协议】掌握 Web 的核心技术

     哈喽,大家好~我是你们的老朋友: 保护小周ღ    谈起 HTTP 协议(超文本传输协议),不知道大家第一次是从什么地方了解到这个协议的呢?在真实的网络环境中网络协议的种类非常多,其中有一些耳熟能详的协议,网络层的 IP 协议, 传输层的 TCP, UDP 协议, 以及应用层的

    2024年02月07日
    浏览(38)
  • Spring Boot 3.1.2版本使用javax.servlet.Filter时,发现Filter不起作用

    在学习Filter的过程中,我实现了Filter的init和destory方法以及doFilter方法后,运行SpringBoot程序发现,我的控制台中并没有输出ini和destory中的调试信息。 代码如下:  可以看到控制台中并没有输出initialize Filter和destory Filter等信息  利用postman发送http请求发现access Filter也没有输出

    2024年03月09日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包