SpringBoot实现图片防盗链的五种方式详解
目录
- 什么是图片防盗链?
- 实现方式对比:5种方法深度解析
- 方法1:过滤器(Filter)实现防盗链
- 1. 创建Spring Boot项目
- 2. 配置防盗链参数(application.yml)
- 3. 编写过滤器
- 方法2:拦截器(Interceptor)实现
- 1. 创建拦截器类
- 方法3:Nginx配置防盗链
- 方法4:签名URL(Token验证)
- 1. 生成带Token的URL
- 方法5:混合策略(Filter + Token)
- 1. 修改过滤器逻辑
- 注意事项与优化建议
- 1. Referer不可靠
- 2. 缓存优化
- 3. 白名单陷阱
什么是图片防盗链?
想象一下,你的网站有一张超可爱的猫咪图片(/images/cute_cat.jpg
),但某天发现别的网站直接用 <img src="http://yourdomain.com/images/cute_cat.jpg">
把你的图偷走了!这就是盗链——别人不劳而获,占用你服务器的流量和资源。
防盗链的核心思想:
- 检查请求来源(Referer):只允许指定域名的请求访问资源。
- 白名单机制:某些资源可以完全开放访问。
- 默认图片兜底:拒绝请求时返回一张“禁止盗链”的提示图。
场景重现
你运行的Spring Boot服务突然流量暴增,但发现90%请求来自第三方网站。这时候你会不会想:“难道我的猫咪图成了别人的广告位?”
实现方式对比:5种方法深度解析
方法 | 优点 | 缺点 |
---|---|---|
过滤器(Filter) | 全局拦截,适合静态资源 | 无法处理复杂逻辑 |
拦截器(Interceptor) | 可访问Spring上下文 | 仅限MVC请求 |
Nginx配置 | 性能高,无需代码 | 不灵活 |
签名URL | 安全性强 | 增加复杂度 |
混合策略 | 多层防护 | 配置复杂 |
方法1:过滤器(Filter)实现防盗链
1. 创建Spring Boot项目
添加必要依赖(pom.XML
):
<dependencies> <!-- Web支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 缓存支持(可选) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!-- Lombok(简化代码) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>
2. 配置防盗链参数(application.yml)
# 防盗链配置 anti-hotlink: # 是否启用防盗链 enabled: true # 允许的域名列表(支持子域名和正则) allowed-domains: - localhost - 127.0.0.1 - "*.example.com" - "^test\\d+\\.domain\\.com$" # 匹配test1.domain.com等 # 需要保护的资源格式(结尾匹配) protected-formats: - .jpg - .jpeg - .png - .gif # 是否允许直接访问(无Referer) allow-direct-Access: true # 拒绝访问时的动作(REDIRECT/FORBIDDEN/DEFAULT_IMAGE) deny-action: DEFAULT_IMAGE # 默认图片路径 default-image: /images/no-hotlinking.png # 白名单路径(无需检查) whitelist-paths: - /api/public/** - /images/public/**
3. 编写过滤器
import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; import Javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.*; import java.util.regex.Pattern; /** * 图片防盗链过滤器 */ @Component @Slf4j public class AntiHotlinkFilter implements Filter { // 从配置中读取参数 @Value("${anti-hotlink.enabled}") private boolean enabled; @Value("${anti-hotlink.allowed-domains}") private List<String> allowedDomains; @Value("${anti-hotlink.protected-formats}") private List<String> protectedFormats; @Value("${anti-hotlink.allow-direct-access}") private boolean allowDirectAccess; @Value("${anti-hotlink.deny-action}") private String denyAction; @Value("${anti-hotlink.default-image}") private String defaultImage; @Value("${anti-hotlink.whitelist-paths}") private List<String> whitelistPaths; // 路径匹配工具 private final AntPathMatcher pathMatcher = new AntPathMatcher(); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (!enabled) { chain.doFilter(request, response); return; } HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String requestURI = httpRequest.getRequestURI(); String referer = httpRequest.getHeader("Referer"); // 检查是否在白名单中 if (isWhitelisted(requestURI)) { chain.doFilter(request, response); return; } // 检查是否是受保护的资源格式 if (!isProtectedResource(requestURI)) { chain.doFilter(request, response); return; } // 直接访问(无Referer)且允许 if (allowDirectAccess && referer == null) { chain.doFilter(request, response); return; } // 检查Referer是否合法 if (isValidReferer(referer)) { chain.doFilter(request, response); } else { handleInvalidRequest(httpResponse); } } /** * 检查路径是否在白名单中 */ private booleanandroid isWhitelisted(String requestURI) { for (String path : whitelistPaths) { if (pathMatcher.yOQTgdXkmatch(path, requestURI)) { return true; } } return false; } /** * 检查是否是受保护的资源格式 */ private boolean isProtectedResource(String requestURI) { for (String format : protectedFormats) { if (requestURI.endsWith(format)) { return true; } } return false; } /** * 检查Referer是否合法 */ private boolean isValidReferer(String referer) { if (referer == null) { return false; } for (String domain : allowedDomains) { if (domain.startsWith("^")) { // 正则匹配 Pattern pattjsern = Pattern.compile(domain.substring(1)); if (pattern.matcher(referer).matches()) { return true; } } else if (domain.equals("*")) { // 通配符匹配 return true; } else if (referer.contains(domain)) { // 精确匹配 return true; } } return false; } /** * 处理非法请求 */ private void handleInvalidRequest(HttpServletResponse response) throws IOException { switch (denyAction) { case "REDIRECT": response.sendRedirect("https://example.com/forbidden"); break; case "FORBIDDEN": response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden"); break; case "DEFAULT_IMAGE": response.sendRedirect(defaultImage); break; default: response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden"); } } }
代码解析:
doFilter
:核心逻辑入口,检查请求是否合法。isWhitelisted
:判断路径是否在白名单中。isValidReferer
:支持正则、通配符和精确匹配。handleInvalidRequest
:根据配置返回不同响应。
方法2:拦截器(Interceptor)实现
1. 创建拦截器类
import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 图片防盗链拦截器 */ @Component public class AntiHotlinkInterceptor implements HandlerInterceptor { // 配置参数(需从配置文件注入) private final List<String> allowedDomains; private final List<String> protectedFormats; private final boolean allowDirectAccess; private final String denyAction; private final String defaultImage; public AntiHotlinkInterceptor( List<String> allowedDomains, List<String> protectedFormats, boolean allowDirectAccess, String denyAction, String defaultImage) { this.allowedDomains = allowedDomains; this.protectedFormats = protectedFormats; this.allowDirectAccess = allowDirectAccess; this.denyAction = denyAction; this.defaultImage = defaultImage; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURI = request.getRequestURI(); String referer = request.getHeader("Referer"); // 检查是否是受保护的资源格式 if (!isProtectedResource(requestURI)) { return true; } // 直接访问(无Referer)且允许 if (allowDirectAccess && referer == null) { return true; } // 检查Referer是否合法 if (isValidReferer(referer)) { return true; } else { handleInvalidRequest(response); return false; } } private boolean isProtectedResource(String requestURI) { for (String format : protectedFormats) { if (requestURI.endsWith(format)) { return true; } } return false; } private boolean isValidReferer(String referer) { if (referer == null) { return false; } for (String domain : allowedDomains) { if (domain.startsWith("^")) { // 正则匹配 Pattern pattern = Pattern.compile(domain.substring(1)); if (pattern.matcher(referer).matches()) { return true; } } else if (domain.equals("*")) { // 通配符匹配 return true; } else if (referer.contains(domain)) { // 精确匹配 return true; } } return false; } private void handleInvalidRequest(HttpServletResponse response) throws IOException { switch (denyAction) { case "REDIRECT": response.sendRedirect("https://example.com/forbidden"); break; case "FORBIDDEN": response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden"); break; case "DEFAULT_IMAGE": response.sendRedirect(defaultImage); break; default: response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden"); } } }
方法3:Nginx配置防盗链
如果你使用Nginx作为反向代理,可以更高效地实现防盗链:
location ~* \.(jpg|jpeg|png|gif)$ { # 限制Referer valid_referers none blocked example.com *.example.com ~\.example\.com$; if ($invalid_referer) { rewrite ^/images/(.*)$ /images/no-hotlinking.png last; } }
性能对比:
- Nginx:毫秒级响应,无需Java处理
- Java过滤器:延迟约10ms
方法4:签名URL(Token验证)
1. 生成带Token的URL
import java.security.MessageDigest; import java.time.Instant; import java.util.Base64; public class TokenGenerator { private static final String SECRET_KEY = "your-secret-key"; private static final int TTL_SECONDS = 300; // 5分钟过期 public static String generateSignedUrl(String filePath) { long timestamp = Instant.now().getEpochSecond(); String token = generateToken(filePath, timestamp); return "https://yourdomain.com" + filePath + "?token=" + token + "&ts=" + timestamp; } private static String generateToken(String filePath, long timestamp) { try { String input = filePath + SECRET_KEY + timestamp; MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(input.getBytes()); return Base64.getEncoder().encodeToString(digest); } catch (Exception e) { throw new RuntimeException("Token generation failed", e); } } public static boolean validateTokenhttp://www.devze.com(String filePath, String token, long timestamp) { return generateToken(filePath, timestamp).equals(token); php } }
方法5:混合策略(Filter + Token)
1. 修改过滤器逻辑
@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(); String referer = httpRequest.getHeader("Referer"); // 检查是否是签名请求 if (isSignedRequest(httpRequest)) { chain.doFilter(request, response); return; } // 其余逻辑同方法1 }
注意事项与优化建议
1. Referer不可靠
- 伪造问题:恶意客户端可伪造Referer头。
- 解决方案:结合Token验证或Nginx配置。
2. 缓存优化
@Cacheable("hotlink_domains") private boolean isAllowedDomain(String domain) { // 缓存域名检查结果 return allowedDomains.contains(domain); }
3. 白名单陷阱
- 路径匹配漏洞:
/api/public/**
可能被绕过。 - 解决方案:使用严格路径匹配规则。
以上就是SpringBoot实现图片防盗链的五种方式详解的详细内容,更多关于SpringBoot图片防盗链的资料请关注编程客栈(www.devze.com)其它相关文章!
精彩评论