Java实现防盗链案例

wen java案例 1

本文目录导读:

Java实现防盗链案例

  1. 基于Spring Boot的防盗链实现
  2. 前端HTML测试页面
  3. 高级防盗链实现(基于令牌)
  4. 配置文件示例(application.yml)
  5. 使用说明

我来为您提供一个完整的Java防盗链实现案例,防盗链主要用于防止其他网站直接引用您的资源(如图片、视频、文件等)。

基于Spring Boot的防盗链实现

使用拦截器(Interceptor)实现

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
/**
 * 防盗链拦截器
 */
@Component
public class AntiHotlinkInterceptor implements HandlerInterceptor {
    // 允许访问的域名白名单
    private static final List<String> ALLOWED_DOMAINS = Arrays.asList(
        "www.example.com",
        "example.com",
        "localhost",
        "127.0.0.1"
    );
    // 需要防盗链保护的路径
    private static final List<String> PROTECTED_PATHS = Arrays.asList(
        "/images/",
        "/files/",
        "/video/",
        "/static/resources/"
    );
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        String requestURI = request.getRequestURI();
        // 检查是否需要防盗链保护
        if (!isProtectedPath(requestURI)) {
            return true; // 不需要保护,直接放行
        }
        // 获取Referer头信息
        String referer = request.getHeader("Referer");
        // 直接访问(无Referer)或Referer为空
        if (referer == null || referer.isEmpty()) {
            // 可以选择允许直接访问或拒绝
            // 这里设为拒绝直接访问
            handleBlockedRequest(response);
            return false;
        }
        // 检查Referer是否在白名单中
        if (!isAllowedReferer(referer)) {
            handleBlockedRequest(response);
            return false;
        }
        return true; // 验证通过,放行
    }
    /**
     * 判断是否是需要保护的路径
     */
    private boolean isProtectedPath(String path) {
        return PROTECTED_PATHS.stream().anyMatch(path::contains);
    }
    /**
     * 判断Referer是否合法
     */
    private boolean isAllowedReferer(String referer) {
        try {
            java.net.URL url = new java.net.URL(referer);
            String domain = url.getHost().toLowerCase();
            return ALLOWED_DOMAINS.stream()
                    .anyMatch(domain::equalsIgnoreCase);
        } catch (Exception e) {
            return false; // URL解析失败视为非法请求
        }
    }
    /**
     * 处理被阻止的请求
     */
    private void handleBlockedRequest(HttpServletResponse response) 
            throws Exception {
        response.setContentType("text/html;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        // 可以返回一个提示图片或者错误信息
        response.getWriter().write("""
            <html>
            <body style="text-align:center;padding:50px;">
                <h1 style="color:red;">403 - 禁止盗链</h1>
                <p>您的请求来源不合法,请通过正规渠道访问。</p>
                <img src="/images/anti-hotlink.png" alt="禁止盗链"/>
            </body>
            </html>
            """);
    }
}

使用过滤器(Filter)实现

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URL;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
 * 防盗链过滤器
 */
@WebFilter(urlPatterns = "/*", filterName = "antiHotlinkFilter")
public class AntiHotlinkFilter implements Filter {
    // 使用Set存储允许的域名,并且支持动态更新
    private Set<String> allowedDomains = ConcurrentHashMap.newKeySet();
    // 需要保护的资源类型
    private Set<String> protectedExtensions = ConcurrentHashMap.newKeySet();
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化允许的域名
        allowedDomains.add("localhost");
        allowedDomains.add("127.0.0.1");
        allowedDomains.add("www.example.com");
        allowedDomains.add("example.com");
        // 初始化需要保护的资源类型
        protectedExtensions.add(".jpg");
        protectedExtensions.add(".jpeg");
        protectedExtensions.add(".png");
        protectedExtensions.add(".gif");
        protectedExtensions.add(".bmp");
        protectedExtensions.add(".ico");
        protectedExtensions.add(".mp4");
        protectedExtensions.add(".avi");
        protectedExtensions.add(".pdf");
        protectedExtensions.add(".mp3");
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String requestURI = httpRequest.getRequestURI();
        // 检查是否为需要保护的资源
        if (!isProtectedResource(requestURI)) {
            chain.doFilter(request, response); // 非保护资源直接放行
            return;
        }
        // 检查请求来源
        String referer = httpRequest.getHeader("Referer");
        // 允许同域名站内的请求没有Referer(直接访问)或来自允许的域名
        if (isValidReferer(referer)) {
            chain.doFilter(request, response);
        } else {
            // 非法请求,返回错误信息
            respondWithError(httpResponse);
        }
    }
    /**
     * 判断是否是受保护的资源
     */
    private boolean isProtectedResource(String uri) {
        // 检查文件扩展名
        int lastDotIndex = uri.lastIndexOf(".");
        if (lastDotIndex == -1 || lastDotIndex == uri.length() - 1) {
            return false;
        }
        String extension = uri.substring(lastDotIndex).toLowerCase();
        return protectedExtensions.contains(extension);
    }
    /**
     * 验证Referer是否合法
     */
    private boolean isValidReferer(String referer) {
        // 无Referer的请求(直接访问)默认允许
        if (referer == null || referer.isEmpty()) {
            return true;
        }
        try {
            URL url = new URL(referer);
            String host = url.getHost().toLowerCase();
            return allowedDomains.contains(host);
        } catch (Exception e) {
            return false;
        }
    }
    /**
     * 返回错误响应
     */
    private void respondWithError(HttpServletResponse response) throws IOException {
        // 方式1:返回自定义图片
        // response.getOutputStream().write(getBlockImageBytes());
        // 方式2:返回错误状态和信息
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write("{\"code\":403,\"message\":\"请求被拒绝:检测到盗链行为\"}");
    }
    @Override
    public void destroy() {
        // 清空资源
        allowedDomains.clear();
        protectedExtensions.clear();
    }
}

Web MVC配置类

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
 * Web MVC配置
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册防盗链拦截器
        registry.addInterceptor(new AntiHotlinkInterceptor())
                // 指定拦截的路径
                .addPathPatterns("/images/**", "/files/**", "/video/**")
                // 排除不需要拦截的路径
                .excludePathPatterns("/images/logo.png", "/favicon.ico");
    }
}

前端HTML测试页面

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">防盗链测试页面</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .test-card {
            background: white;
            padding: 20px;
            margin-bottom: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        img, video {
            max-width: 100%;
            height: auto;
        }
        button {
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin-right: 10px;
        }
        button:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>
    <h1 style="text-align:center;">防盗链功能测试页面</h1>
    <div class="test-card">
        <h2>正常访问(本站)</h2>
        <p>此图片来自本站服务器,应该可以正常显示:</p>
        <img src="/images/sample.jpg" alt="本地图片">
    </div>
    <div class="test-card">
        <h2>测试视频资源</h2>
        <p>此视频来自本站服务器:</p>
        <video controls>
            <source src="/video/sample.mp4" type="video/mp4">
            您的浏览器不支持视频标签
        </video>
    </div>
    <div class="test-card">
        <h2>模拟外部引用</h2>
        <p>以下按钮模拟外部网站请求本站资源(实际测试中会携带外部Referer):</p>
        <button onclick="testExternalLink()">模拟外部链接点击</button>
        <button onclick="testDirectAccess()">直接访问测试</button>
        <div id="result"></div>
    </div>
    <script>
        // 模拟外部链接引用
        function testExternalLink() {
            // 这里在实际场景中,请求会带有外部网站的Referer
            var imgUrl = '/images/protected-image.png';
            fetch(imgUrl, {
                headers: {
                    'Referer': 'https://evil-site.com/page.html'
                }
            })
            .then(response => {
                if (response.status === 403) {
                    document.getElementById('result').innerHTML = 
                        '<p style="color:red;">❌ 请求被拒�绝:检测到盗链行为(403)</p>';
                } else {
                    document.getElementById('result').innerHTML = 
                        '<p style="color:green;">✅ 请求成功</p>';
                }
            })
            .catch(error => {
                document.getElementById('result').innerHTML = 
                    '<p style="color:red;">❌ 请求出错: ' + error.message + '</p>';
            });
        }
        // 直接访问测试
        function testDirectAccess() {
            var img = new Image();
            img.src = '/images/protected-image.png';
            img.onload = function() {
                document.getElementById('result').innerHTML = 
                    '<p style="color:green;">✅ 直接访问成功(图片已加载)</p>';
            };
            img.onerror = function() {
                document.getElementById('result').innerHTML = 
                    '<p style="color:red;">❌ 直接访问被拒绝</p>';
            };
        }
    </script>
</body>
</html>

高级防盗链实现(基于令牌)

import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.concurrent.ConcurrentHashMap;
/**
 * 基于令牌的防盗链工具类
 */
@Component
public class TokenBasedAntiHotlink {
    // 存储有效的令牌(实际场景可能使用Redis等分布式存储)
    private ConcurrentHashMap<String, Long> tokenStore = new ConcurrentHashMap<>();
    // 令牌有效期(毫秒)
    private static final long TOKEN_VALIDITY = 24 * 60 * 60 * 1000; // 24小时
    // 加密密钥
    private static final String SECRET_KEY = "your-secret-key-here";
    /**
     * 生成访问令牌
     */
    public String generateToken(String resourcePath) {
        long timestamp = System.currentTimeMillis();
        String rawToken = resourcePath + timestamp + SECRET_KEY;
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] hash = md.digest(rawToken.getBytes());
            String token = Base64.getUrlEncoder().withoutPadding()
                    .encodeToString(hash);
            // 存储令牌和时间戳
            tokenStore.put(token, timestamp);
            return token + "." + timestamp;
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("加密算法不可用");
        }
    }
    /**
     * 验证令牌
     */
    public boolean validateToken(String token, String resourcePath) {
        try {
            // 解析令牌和时间戳
            String[] parts = token.split("\\.");
            if (parts.length != 2) {
                return false;
            }
            String tokenPart = parts[0];
            long timestamp = Long.parseLong(parts[1]);
            // 检查是否过期
            if (System.currentTimeMillis() - timestamp > TOKEN_VALIDITY) {
                tokenStore.remove(tokenPart);
                return false;
            }
            // 验证令牌是否存在
            Long storedTimestamp = tokenStore.get(tokenPart);
            if (storedTimestamp == null || storedTimestamp != timestamp) {
                return false;
            }
            // 验证令牌与资源路径的关联
            String expectedToken = generateHash(resourcePath + timestamp + SECRET_KEY);
            if (!expectedToken.equals(tokenPart)) {
                return false;
            }
            // 验证成功,移除令牌(一次性使用)
            tokenStore.remove(tokenPart);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    /**
     * 生成哈希值
     */
    private String generateHash(String input) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] hash = md.digest(input.getBytes());
        return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
    }
}

配置文件示例(application.yml)

# 应用程序配置
spring:
  application:
    name: anti-hotlink-demo
  mvc:
    static-path-pattern: /static/**
# 自定义防盗链配置
anti-hotlink:
  # 是否启用防盗链
  enabled: true
  # 允许的域名列表(使用逗号分隔)
  allowed-domains: >
    www.example.com,
    example.com,
    localhost,
    *.example.com
  # 需要保护的路径模式
  protected-paths:
    - /images/**
    - /files/**
    - /video/**
    - /static/**
  # 排除的路径
  excluded-paths:
    - /images/logo.png
    - /favicon.ico
  # 拒绝行为
  block-redirect-url: /error/403.html
  block-message: 禁止外部链接访问本站资源

使用说明

主要特点:

  1. 多层防护:支持拦截器、过滤器、令牌多种方式
  2. 灵活配置:可以通过配置文件或代码设置白名单
  3. 多种保护:支持图片、视频、文档等多种资源类型
  4. 错误处理:自动识别盗链行为并返回相应错误

部署要点:

  1. 确保服务器时区正确
  2. 合理设置白名单域名
  3. 对于分布式环境,建议使用Redis存储令牌
  4. 定期更新密钥和安全性配置

这个案例提供了完整的防盗链解决方案,可以根据实际需求选择合适的实现方式。

抱歉,评论功能暂时关闭!