SpringBoot实现图片防盗链的五种方式详解
作者:墨瑾轩
出于安全考虑,我们需要后端返回的图片只允许在某个网站内展示,不想被爬虫拿到图片地址后被下载,或者,不想浏览器直接访问图片链接,出于性能考虑,不想要别人的网站,拿着我们的图片链接去展示,所以本文给大家介绍了SpringBoot实现图片防盗链的五种方式
什么是图片防盗链?
想象一下,你的网站有一张超可爱的猫咪图片(/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 boolean isWhitelisted(String requestURI) {
for (String path : whitelistPaths) {
if (pathMatcher.match(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 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");
}
}
}
代码解析:
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 validateToken(String filePath, String token, long timestamp) {
return generateToken(filePath, timestamp).equals(token);
}
}
方法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图片防盗链的资料请关注脚本之家其它相关文章!
