0

0

如何在Java中构建图片防盗链接系统 Java判断来源并控制资源访问

星夢妙者

星夢妙者

发布时间:2025-07-23 19:13:02

|

1193人浏览过

|

来源于php中文网

原创

图片防盗链系统的核心实现方案有两种:基于http referer的校验和基于token的动态链接验证。1. 基于http referer的校验通过检查请求头中的referer字段判断来源是否合法,但该方式易被伪造或因隐私设置失效;2. 基于token的动态链接方案在生成图片链接时附加带签名和时间戳的token,并在服务器端验证其有效性,安全性更高。具体实现中需完成token生成、传递、验证流程,并结合spring boot拦截器统一处理验证逻辑,同时面临性能开销、cdn兼容性、浏览器缓存等挑战。

如何在Java中构建图片防盗链接系统 Java判断来源并控制资源访问

在Java中构建图片防盗链系统,核心思路是通过服务器端对请求的来源进行判断。这通常意味着我们会检查HTTP请求头中的Referer信息,或者更安全地,为图片资源生成带有时间戳或签名的动态链接(token),在图片被请求时,后端再验证这个token的有效性,从而决定是否返回图片数据。这种做法能有效防止图片被未经授权的网站直接引用,保护你的带宽和内容版权。

如何在Java中构建图片防盗链接系统 Java判断来源并控制资源访问

解决方案

构建一个图片防盗链系统,可以从两个主要方向入手:基于HTTP Referer的简单校验,以及更健壮的基于Token的动态链接方案。

方案一:基于HTTP Referer的校验

立即学习Java免费学习笔记(深入)”;

如何在Java中构建图片防盗链接系统 Java判断来源并控制资源访问

这是最直接也最容易实现的方式。当浏览器请求一个资源时,通常会在HTTP请求头中带上Referer字段,指明这个请求是从哪个页面发起的。我们可以在服务器端检查这个字段。

假设你有一个Spring Boot应用,可以这样做:

如何在Java中构建图片防盗链接系统 Java判断来源并控制资源访问
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@Controller
public class ImageController {

    private final String ALLOWED_DOMAIN = "http://yourdomain.com"; // 替换为你的域名

    @GetMapping("/images/{imageName:.+}") // .+: 匹配文件名,包括点
    @ResponseBody
    public void getImage(@PathVariable String imageName,
                         @RequestHeader(value = "Referer", required = false) String referer,
                         HttpServletResponse response) throws IOException {

        if (referer == null || !referer.startsWith(ALLOWED_DOMAIN)) {
            // 如果Referer为空或者不是来自允许的域名,则拒绝访问
            response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403 Forbidden
            // 可以返回一个默认的“禁止盗链”图片,或者直接结束
            return;
        }

        // 假设图片存储在某个目录下
        Path imagePath = Paths.get("/path/to/your/images/", imageName); // 替换为你的图片存储路径

        if (Files.exists(imagePath) && Files.isReadable(imagePath)) {
            response.setContentType(Files.probeContentType(imagePath));
            try (InputStream is = Files.newInputStream(imagePath)) {
                is.transferTo(response.getOutputStream());
            }
        } else {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND); // 404 Not Found
        }
    }
}

这段代码很简单,但需要注意的是,Referer头很容易被伪造,或者在某些隐私设置下可能不会发送。所以,这更多是一种初级的、象征性的防护。

方案二:基于Token的动态链接防盗链

这是更推荐、更健壮的方案。核心思想是,当你的页面(比如yourdomain.com/article/123)加载时,其中包含的图片链接不再是静态的/images/pic.jpg,而是动态生成的,例如/images/pic.jpg?token=XYZ&timestamp=123。服务器端在处理/images/pic.jpg的请求时,会验证这个tokentimestamp是否有效。

实现步骤概览:

  1. Token生成: 在生成包含图片的HTML页面时,为每个图片生成一个唯一的、有时效性的Token。这个Token可以包含图片路径、过期时间,并用一个密钥进行签名(例如HMAC-SHA256),防止篡改。
  2. Token传递: 将生成的Token作为查询参数附加到图片URL上。
  3. Token验证: 当用户浏览器请求图片时,服务器端截取URL中的Token,验证其签名和过期时间。如果验证通过,则返回图片;否则,返回403 Forbidden或一个默认的错误图片。

Token生成示例(一个简化的HMAC签名):

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public class ImageTokenUtil {

    private static final String SECRET_KEY = "your_very_secret_key_here"; // 生产环境请使用更复杂的密钥
    private static final String HMAC_ALGORITHM = "HmacSHA256";
    private static final long TOKEN_EXPIRATION_MILLIS = 5 * 60 * 1000; // Token有效期:5分钟

    public static String generateSignedToken(String imagePath) {
        long expires = System.currentTimeMillis() + TOKEN_EXPIRATION_MILLIS;
        String dataToSign = imagePath + ":" + expires; // 待签名数据:图片路径 + 过期时间

        try {
            Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
            mac.init(secretKeySpec);
            byte[] hmacSha256 = mac.doFinal(dataToSign.getBytes(StandardCharsets.UTF_8));
            String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(hmacSha256);

            return Base64.getUrlEncoder().withoutPadding().encodeToString(dataToSign.getBytes(StandardCharsets.UTF_8)) + "." + signature;
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            // 实际应用中需要更好的异常处理
            e.printStackTrace();
            return null;
        }
    }

    public static boolean validateSignedToken(String fullToken) {
        if (fullToken == null || !fullToken.contains(".")) {
            return false;
        }
        String[] parts = fullToken.split("\.");
        if (parts.length != 2) {
            return false;
        }

        String encodedData = parts[0];
        String receivedSignature = parts[1];

        try {
            String dataToSign = new String(Base64.getUrlDecoder().decode(encodedData), StandardCharsets.UTF_8);
            String[] dataParts = dataToSign.split(":");
            if (dataParts.length != 2) {
                return false;
            }
            String imagePath = dataParts[0];
            long expires = Long.parseLong(dataParts[1]);

            if (System.currentTimeMillis() > expires) {
                // Token已过期
                return false;
            }

            // 重新计算签名并比对
            Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
            mac.init(secretKeySpec);
            byte[] hmacSha256 = mac.doFinal(dataToSign.getBytes(StandardCharsets.UTF_8));
            String expectedSignature = Base64.getUrlEncoder().withoutPadding().encodeToString(hmacSha256);

            return expectedSignature.equals(receivedSignature);

        } catch (Exception e) {
            // 签名解析或验证失败
            e.printStackTrace();
            return false;
        }
    }
}

图片服务Controller结合Token验证:

// 假设ImageController中引入了ImageTokenUtil
// ... 其他导入和类定义 ...

@GetMapping("/secure-images/{encodedToken}/{imageName:.+}")
@ResponseBody
public void getSecureImage(@PathVariable String encodedToken,
                           @PathVariable String imageName,
                           HttpServletResponse response) throws IOException {

    // 实际的图片路径需要从token中解析出来,或者这里只作为辅助验证
    // 假设token中包含完整的图片路径信息,这里简化处理
    String fullToken = encodedToken + "." + imageName; // 这里需要根据实际token生成方式调整

    if (!ImageTokenUtil.validateSignedToken(fullToken)) {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403 Forbidden
        return;
    }

    // 从token中解析出实际的图片路径,或者直接使用imageName(如果token只负责验证权限)
    // 为了简化,这里仍然假设imageName是实际的文件名,但实际应用中,token应包含完整路径或校验路径
    Path imagePath = Paths.get("/path/to/your/images/", imageName);

    if (Files.exists(imagePath) && Files.isReadable(imagePath)) {
        response.setContentType(Files.probeContentType(imagePath));
        try (InputStream is = Files.newInputStream(imagePath)) {
            is.transferTo(response.getOutputStream());
        }
    } else {
        response.setStatus(HttpServletResponse.SC_NOT_FOUND); // 404 Not Found
    }
}

在HTML页面中,图片链接会变成类似这样: <img src="/secure-images/eyJpbWFnZVBhdGgiOiIvZm9vL2Jhci5qcGcifQ.somesignature/pic.jpg" /> 这只是一个示意,实际的URL结构和Token的生成与解析需要严格对应。

为什么传统的防盗链方法效果不佳?

谈到传统的防盗链方法,我们通常会想到基于HTTP Referer的校验,或者一些前端JavaScript的判断。这些方法之所以效果不佳,甚至可以说“防君子不防小人”,主要有几个原因:

首先,Referer头信息极易伪造。攻击者或者一些爬虫工具,可以非常轻松地在请求中伪造Referer头,将其设置为你的合法域名。这就好比你家门上贴了个“本小区住户请走正门”的牌子,但小偷换身衣服就混进来了,你根本无法辨别。很多浏览器出于用户隐私考虑,也可能默认不发送Referer,或者只发送部分信息,这导致即使是合法用户也可能被误判。

其次,前端JavaScript防盗链形同虚设。所有在浏览器端执行的逻辑,都可以在开发者工具中被轻易查看、修改甚至禁用。你用JavaScript判断当前页面URL是否是你的域名,如果不是就替换图片链接或者阻止加载,这对于稍微懂点前端知识的人来说,绕过简直不费吹灰之力。就好比你把家里的钥匙藏在门口地毯下,并希望小偷找不到——这显然不现实。

Sesame AI
Sesame AI

一款开创性的语音AI伴侣,具备先进的自然对话能力和独特个性。

下载

再者,CDN缓存可能带来复杂性。如果你的图片通过CDN加速,CDN节点会缓存你的图片。当CDN收到请求时,它可能不会携带原始的Referer信息到你的源站,或者CDN自身的配置会影响Referer的传递。这使得基于Referer的校验在CDN环境下变得更加复杂,甚至失效。你得考虑CDN如何与你的防盗链逻辑配合,这本身就是个挑战。

最后,这些方法都无法阻止直接下载。如果有人直接知道了你的图片URL,并且你的服务器没有做任何校验,他可以直接通过下载工具、脚本等方式批量下载你的图片,完全绕过你的网页访问。

正是因为这些局限性,我们才需要更底层的、服务器端强制执行的、基于加密和时效性的验证机制,也就是Token防盗链。

实现图片防盗链,有哪些关键技术点和挑战?

实现一个健壮的图片防盗链系统,远不止写几行代码那么简单。这里面涉及到不少关键的技术点和需要面对的挑战:

关键技术点:

  1. 安全Token生成与验证:

    • 加密算法选择: HMAC(Hash-based Message Authentication Code)是一个非常好的选择,它能确保Token的完整性和真实性,防止篡改。使用如HMAC-SHA256这样的算法,配合一个只有服务器知道的密钥,可以有效生成和验证签名。
    • Token内容设计: Token里应该包含什么信息?通常包括图片资源的唯一标识(路径或ID)、过期时间戳。有时为了更严格,还可以加入请求用户的IP地址,实现IP绑定,但这也增加了复杂性。
    • 时间戳管理: 确保Token包含过期时间,并且服务器端能正确解析和判断是否过期。这是一个防止Token被无限期重用的关键。
    • 一次性Token(可选): 对于特别敏感的资源,可以考虑生成一次性Token,即Token使用一次后就立即失效。这需要服务器维护Token的使用状态,增加了存储和同步的复杂性。
  2. URL重写与路由:

    • 你需要设计一个清晰的URL结构,能方便地嵌入Token,同时又不显得过于冗长。例如/images/secure/{token}/{filename}
    • 服务器端需要配置路由规则,将这类请求正确地导向你的防盗链处理逻辑。
  3. 图片流式传输:

    • 当验证通过后,你需要将图片文件以字节流的形式写入HTTP响应体。正确设置Content-Type(如image/jpeg, image/png等)和Content-Length头,确保浏览器能正确解析和显示图片。
    • 利用Java NIO的Files.newInputStreamtransferTo方法可以高效地传输文件。
  4. 错误处理与用户体验:

    • 当防盗链验证失败时,应该返回什么?通常是HTTP 403 Forbidden状态码。
    • 为了更好的用户体验,可以配置一个默认的“禁止盗链”图片,而不是直接显示一个破碎的图片图标。这可以通过返回一个预设的图片流来实现。

面临的挑战:

  1. 性能开销: 每次图片请求都需要进行Token的生成(在页面渲染时)和验证(在图片请求时),这会增加服务器的CPU开销。对于高并发、图片数量巨大的网站,这可能是一个显著的性能瓶颈。
    • 优化策略: 可以考虑Token的缓存,或者将Token的生成逻辑下放到前端(通过JS在页面加载后动态生成,但安全性稍弱),或者使用CDN的Token机制。
  2. Token管理与同步: 如果你的应用是集群部署的,那么每个服务器实例都需要能够生成和验证Token。这意味着密钥必须在所有实例间保持一致。如果使用一次性Token,还需要一个共享的、高性能的存储(如Redis)来记录Token的使用状态,并解决分布式环境下的并发问题。
  3. CDN兼容性: 多数大型网站都会使用CDN来加速图片分发。如何让防盗链系统与CDN协同工作是一个复杂的问题。
    • CDN的Token支持: 许多CDN服务商本身就提供了Token防盗链功能(如阿里云OSS、腾讯云COS的签名URL),直接利用CDN的能力往往是更优的选择,它将验证逻辑下沉到CDN边缘节点,减轻源站压力。
    • 回源验证: 如果CDN不支持Token,或者你选择在源站验证,那么CDN每次缓存失效或首次请求时,都需要回源到你的Java应用进行验证,这可能会增加回源带宽和源站压力。
  4. 浏览器缓存: 动态URL(带Token的URL)会导致浏览器每次都认为是一个新资源,从而可能无法有效利用浏览器缓存。这会增加服务器的请求量。
    • 解决方案: 可以将Token的有效期设置得相对长一些,或者在Token中只包含少量动态信息,让图片本身的URL保持相对稳定,或者利用Etag/Last-Modified头进行协商缓存。
  5. 合法外部链接的处理: 有时你可能希望图片能被社交媒体平台(如微博、微信)正常分享和显示。这些平台在抓取图片时,其Referer可能不是你的域名。你需要为这些特定的场景提供白名单机制,或者生成特殊的、长期有效的分享Token。
  6. 密钥管理: 用于签名Token的密钥至关重要,一旦泄露,防盗链机制将彻底失效。密钥应该妥善保管,定期轮换,并避免硬编码在代码中。

这些挑战要求我们在设计系统时,不仅要考虑功能实现,更要从性能、可扩展性、安全性和运维角度进行全面权衡。

如何在Spring Boot项目中优雅地实现图片资源保护?

在Spring Boot项目中实现图片资源保护,我们可以利用Spring框架的特性,比如拦截器(Interceptor)或者过滤器(Filter),来集中处理防盗链逻辑,而不是在每个图片处理方法中重复代码。这使得代码更清晰,维护起来也方便。

这里我们以基于Token的防盗链为例,展示如何通过Spring Interceptor来优雅地实现。

1. Token生成工具类(同上,或更完善):ImageTokenUtil.java (保持不变,或根据实际需求调整,例如generateSignedToken方法可能需要传入图片资源的唯一ID而不是路径,然后通过ID查找实际路径)

2. 图片资源控制器: 这个控制器会变得非常简洁,因为它不再直接处理防盗链逻辑,而是专注于读取和返回图片文件。

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@Controller
public class SecureImageController {

    private final String IMAGE_BASE_PATH = "/path/to/your/images/"; // 你的图片存储根目录

    @GetMapping("/secure-images/{imageName:.+}") // 这里的URL不再包含token,token会在拦截器中处理
    @ResponseBody
    public void getSecureImage(@PathVariable String imageName,
                               HttpServletResponse response) throws IOException {
        // 假设拦截器已经验证了权限,这里直接提供图片
        Path imagePath = Paths.get(IMAGE_BASE_PATH, imageName);

        if (Files.exists(imagePath) && Files.isReadable(imagePath)) {
            response.setContentType(Files.probeContentType(imagePath));
            try (InputStream is = Files.newInputStream(imagePath)) {
                is.transferTo(response.getOutputStream());
            }
        } else {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND); // 图片不存在
        }
    }
}

3. 防盗链拦截器: 这是核心部分,它会在请求到达SecureImageController之前进行拦截和验证。

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

public class AntiHotlinkInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从请求参数中获取token,例如:/secure-images/pic.jpg?token=XYZ
        String token = request.getParameter("token");
        String requestUri = request.getRequestURI(); // /secure-images/pic.jpg
        String imageName = requestUri.substring(requestUri.lastIndexOf('/') + 1); // pic.jpg

        // 假设token中需要包含imageName作为验证的一部分
        // 实际场景中,token可能只包含一个资源ID,然后通过ID去查找资源路径
        String dataToValidate = imageName; // 或者更复杂的,从token中解析出原始数据

        if (token == null || !ImageTokenUtil.validateSignedToken(token)) {
            // Token无效或缺失
            response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403 Forbidden
            // 可以选择返回一个默认的“禁止盗链”图片,或者一个错误页面
            // response.sendRedirect("/error-images/hotlink-forbidden.png");
            return false; // 阻止请求继续向下执行
        }

        // 如果token验证通过,则放行
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 在Controller方法执行后,视图渲染前执行
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 在整个请求完成后执行,用于资源清理等
    }
}

4. 配置拦截器:

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
spring框架介绍
spring框架介绍

本专题整合了spring框架相关内容,想了解更多详细内容,请阅读专题下面的文章。

156

2025.08.06

Java Spring Security 与认证授权
Java Spring Security 与认证授权

本专题系统讲解 Java Spring Security 框架在认证与授权中的应用,涵盖用户身份验证、权限控制、JWT与OAuth2实现、跨站请求伪造(CSRF)防护、会话管理与安全漏洞防范。通过实际项目案例,帮助学习者掌握如何 使用 Spring Security 实现高安全性认证与授权机制,提升 Web 应用的安全性与用户数据保护。

88

2026.01.26

spring boot框架优点
spring boot框架优点

spring boot框架的优点有简化配置、快速开发、内嵌服务器、微服务支持、自动化测试和生态系统支持。本专题为大家提供spring boot相关的文章、下载、课程内容,供大家免费下载体验。

139

2023.09.05

spring框架有哪些
spring框架有哪些

spring框架有Spring Core、Spring MVC、Spring Data、Spring Security、Spring AOP和Spring Boot。详细介绍:1、Spring Core,通过将对象的创建和依赖关系的管理交给容器来实现,从而降低了组件之间的耦合度;2、Spring MVC,提供基于模型-视图-控制器的架构,用于开发灵活和可扩展的Web应用程序等。

408

2023.10.12

Java Spring Boot开发
Java Spring Boot开发

本专题围绕 Java 主流开发框架 Spring Boot 展开,系统讲解依赖注入、配置管理、数据访问、RESTful API、微服务架构与安全认证等核心知识,并通过电商平台、博客系统与企业管理系统等项目实战,帮助学员掌握使用 Spring Boot 快速开发高效、稳定的企业级应用。

73

2025.08.19

Java Spring Boot 4更新教程_Java Spring Boot 4有哪些新特性
Java Spring Boot 4更新教程_Java Spring Boot 4有哪些新特性

Spring Boot 是一个基于 Spring 框架的 Java 开发框架,它通过 约定优于配置的原则,大幅简化了 Spring 应用的初始搭建、配置和开发过程,让开发者可以快速构建独立的、生产级别的 Spring 应用,无需繁琐的样板配置,通常集成嵌入式服务器(如 Tomcat),提供“开箱即用”的体验,是构建微服务和 Web 应用的流行工具。

149

2025.12.22

Java Spring Boot 微服务实战
Java Spring Boot 微服务实战

本专题深入讲解 Java Spring Boot 在微服务架构中的应用,内容涵盖服务注册与发现、REST API开发、配置中心、负载均衡、熔断与限流、日志与监控。通过实际项目案例(如电商订单系统),帮助开发者掌握 从单体应用迁移到高可用微服务系统的完整流程与实战能力。

271

2025.12.24

Spring Boot企业级开发与MyBatis Plus实战
Spring Boot企业级开发与MyBatis Plus实战

本专题面向 Java 后端开发者,系统讲解如何基于 Spring Boot 与 MyBatis Plus 构建高效、规范的企业级应用。内容涵盖项目架构设计、数据访问层封装、通用 CRUD 实现、分页与条件查询、代码生成器以及常见性能优化方案。通过完整实战案例,帮助开发者提升后端开发效率,减少重复代码,快速交付稳定可维护的业务系统。

32

2026.02.11

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

76

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.3万人学习

C# 教程
C# 教程

共94课时 | 11.2万人学习

Java 教程
Java 教程

共578课时 | 81.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号