SpringBoot實(shí)現(xiàn)圖片防盜鏈的五種方式詳解
什么是圖片防盜鏈?
想象一下,你的網(wǎng)站有一張超可愛(ài)的貓咪圖片(/images/cute_cat.jpg),但某天發(fā)現(xiàn)別的網(wǎng)站直接用 <img src="http://yourdomain.com/images/cute_cat.jpg"> 把你的圖偷走了!這就是盜鏈——別人不勞而獲,占用你服務(wù)器的流量和資源。
防盜鏈的核心思想:
- 檢查請(qǐng)求來(lái)源(Referer):只允許指定域名的請(qǐng)求訪問(wèn)資源。
- 白名單機(jī)制:某些資源可以完全開(kāi)放訪問(wèn)。
- 默認(rèn)圖片兜底:拒絕請(qǐng)求時(shí)返回一張“禁止盜鏈”的提示圖。
場(chǎng)景重現(xiàn)
你運(yùn)行的Spring Boot服務(wù)突然流量暴增,但發(fā)現(xiàn)90%請(qǐng)求來(lái)自第三方網(wǎng)站。這時(shí)候你會(huì)不會(huì)想:“難道我的貓咪圖成了別人的廣告位?”
實(shí)現(xiàn)方式對(duì)比:5種方法深度解析
| 方法 | 優(yōu)點(diǎn) | 缺點(diǎn) |
|---|---|---|
| 過(guò)濾器(Filter) | 全局?jǐn)r截,適合靜態(tài)資源 | 無(wú)法處理復(fù)雜邏輯 |
| 攔截器(Interceptor) | 可訪問(wèn)Spring上下文 | 僅限MVC請(qǐng)求 |
| Nginx配置 | 性能高,無(wú)需代碼 | 不靈活 |
| 簽名URL | 安全性強(qiáng) | 增加復(fù)雜度 |
| 混合策略 | 多層防護(hù) | 配置復(fù)雜 |
方法1:過(guò)濾器(Filter)實(shí)現(xiàn)防盜鏈
1. 創(chuàng)建Spring Boot項(xiàng)目
添加必要依賴(lài)(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(簡(jiǎn)化代碼) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. 配置防盜鏈參數(shù)(application.yml)
# 防盜鏈配置
anti-hotlink:
# 是否啟用防盜鏈
enabled: true
# 允許的域名列表(支持子域名和正則)
allowed-domains:
- localhost
- 127.0.0.1
- "*.example.com"
- "^test\\d+\\.domain\\.com$" # 匹配test1.domain.com等
# 需要保護(hù)的資源格式(結(jié)尾匹配)
protected-formats:
- .jpg
- .jpeg
- .png
- .gif
# 是否允許直接訪問(wèn)(無(wú)Referer)
allow-direct-access: true
# 拒絕訪問(wèn)時(shí)的動(dòng)作(REDIRECT/FORBIDDEN/DEFAULT_IMAGE)
deny-action: DEFAULT_IMAGE
# 默認(rèn)圖片路徑
default-image: /images/no-hotlinking.png
# 白名單路徑(無(wú)需檢查)
whitelist-paths:
- /api/public/**
- /images/public/**
3. 編寫(xiě)過(guò)濾器
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;
/**
* 圖片防盜鏈過(guò)濾器
*/
@Component
@Slf4j
public class AntiHotlinkFilter implements Filter {
// 從配置中讀取參數(shù)
@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;
}
// 檢查是否是受保護(hù)的資源格式
if (!isProtectedResource(requestURI)) {
chain.doFilter(request, response);
return;
}
// 直接訪問(wèn)(無(wú)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;
}
/**
* 檢查是否是受保護(hù)的資源格式
*/
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;
}
/**
* 處理非法請(qǐng)求
*/
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:核心邏輯入口,檢查請(qǐng)求是否合法。isWhitelisted:判斷路徑是否在白名單中。isValidReferer:支持正則、通配符和精確匹配。handleInvalidRequest:根據(jù)配置返回不同響應(yīng)。
方法2:攔截器(Interceptor)實(shí)現(xiàn)
1. 創(chuàng)建攔截器類(lèi)
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 {
// 配置參數(shù)(需從配置文件注入)
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");
// 檢查是否是受保護(hù)的資源格式
if (!isProtectedResource(requestURI)) {
return true;
}
// 直接訪問(wèn)(無(wú)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作為反向代理,可以更高效地實(shí)現(xiàn)防盜鏈:
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;
}
}
性能對(duì)比:
- Nginx:毫秒級(jí)響應(yīng),無(wú)需Java處理
- Java過(guò)濾器:延遲約10ms
方法4:簽名URL(Token驗(yàn)證)
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分鐘過(guò)期
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. 修改過(guò)濾器邏輯
@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");
// 檢查是否是簽名請(qǐng)求
if (isSignedRequest(httpRequest)) {
chain.doFilter(request, response);
return;
}
// 其余邏輯同方法1
}
注意事項(xiàng)與優(yōu)化建議
1. Referer不可靠
- 偽造問(wèn)題:惡意客戶(hù)端可偽造Referer頭。
- 解決方案:結(jié)合Token驗(yàn)證或Nginx配置。
2. 緩存優(yōu)化
@Cacheable("hotlink_domains")
private boolean isAllowedDomain(String domain) {
// 緩存域名檢查結(jié)果
return allowedDomains.contains(domain);
}
3. 白名單陷阱
- 路徑匹配漏洞:
/api/public/**可能被繞過(guò)。 - 解決方案:使用嚴(yán)格路徑匹配規(guī)則。
以上就是SpringBoot實(shí)現(xiàn)圖片防盜鏈的五種方式詳解的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot圖片防盜鏈的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java 學(xué)習(xí)筆記(入門(mén)篇)_java程序helloWorld
安裝配置完Java的jdk,下面就開(kāi)始寫(xiě)第一個(gè)java程序--hello World.用來(lái)在控制臺(tái)輸出“Hello World”,接下來(lái)詳細(xì)介紹,感興趣的朋友可以參考下2013-01-01
Java實(shí)現(xiàn)圖片倒影的源碼實(shí)例內(nèi)容
在本篇文章里小編給大家整理的是關(guān)于Java實(shí)現(xiàn)圖片倒影的源碼以及相關(guān)知識(shí)點(diǎn),有需要的朋友們學(xué)習(xí)下。2019-09-09
springboot運(yùn)行到dokcer中 dockerfile的場(chǎng)景分析
這篇文章主要介紹了springboot運(yùn)行到dokcer中 dockerfile,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03
Java事件處理機(jī)制(自定義事件)實(shí)例詳解
這篇文章主要介紹了Java事件處理機(jī)制(自定義事件)實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2016-12-12
SpringBoot使用Guava實(shí)現(xiàn)日志脫敏的示例代碼
本文主要介紹了SpringBoot使用Guava實(shí)現(xiàn)日志脫敏的示例代碼,使用Guava中的Strings、Maps和CharMatcher類(lèi)來(lái)進(jìn)行日志脫敏,保護(hù)敏感數(shù)據(jù)的安全,感興趣的可以了解一下2024-01-01
使用Java高效將CSV轉(zhuǎn)換為Excel的實(shí)現(xiàn)方法
在日常的數(shù)據(jù)處理和分析工作中,CSV文件因其輕量級(jí)和通用性而廣受歡迎,然而,當(dāng)我們需要進(jìn)行復(fù)雜的數(shù)據(jù)分析或利用Excel強(qiáng)大的圖表和公式功能時(shí),將CSV數(shù)據(jù)轉(zhuǎn)換為Excel文件就成了開(kāi)發(fā)者們常見(jiàn)的需求和痛點(diǎn),本文將深入探討如何利用Java高效地將CSV轉(zhuǎn)換為Excel2025-08-08
SpringMVC通過(guò)Ajax處理Json數(shù)據(jù)的步驟詳解
這篇文章主要介紹了SpringMVC通過(guò)Ajax處理Json數(shù)據(jù)的步驟詳解,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04

