在 Spring Boot 項目中實現防盜鏈可以通過多種方式,下面為你介紹兩種常見的實現方法,分別是基于請求頭 Referer 和基于令牌(Token)的防盜鏈。
基于請求頭 Referer 的防盜鏈
這種方法通過檢查請求頭中的 Referer 字段,判斷請求是否來自合法的來源。如果不是,則拒絕該請求。
以下是實現步驟和示例代碼:
- 創建一個過濾器:用于攔截請求并檢查 Referer 字段。
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;public class AntiLeachingFilter implements Filter {private List<String> allowedReferers;@Overridepublic void init(FilterConfig filterConfig) throws ServletException {String refererConfig = filterConfig.getInitParameter("allowedReferers");if (Objects.nonNull(refererConfig)) {allowedReferers = Arrays.asList(refererConfig.split(","));}}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;String referer = httpServletRequest.getHeader("Referer");boolean isAllowed = false;if (Objects.isNull(referer)) {isAllowed = false;} else {for (String allowedReferer : allowedReferers) {if (referer.contains(allowedReferer)) {isAllowed = true;break;}}}if (isAllowed) {filterChain.doFilter(httpServletRequest, httpServletResponse);} else {httpServletResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");}}@Overridepublic void destroy() {Filter.super.destroy();}
}
- 配置過濾器:在 Spring Boot 中注冊該過濾器。
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class FilterConfig {@Beanpublic FilterRegistrationBean<AntiLeachingFilter> filterRegistrationBean() {FilterRegistrationBean<AntiLeachingFilter> registration = new FilterRegistrationBean<>();registration.setFilter(new AntiLeachingFilter());registration.addInitParameter("allowedReferers", "127.0.0.1");registration.addUrlPatterns("/*");return registration;}}
基于令牌(Token)的防盜鏈
此方法為每個請求生成一個唯一的令牌,并在請求時驗證令牌的有效性。
以下是實現步驟和示例代碼:
- 創建令牌生成和驗證工具類:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;public class TokenGeneratorUtils {private static final String SECRET_KEY = "4t3t35y546yertdgsr3w4";/*** 生成用于防盜鏈的令牌(Token)。* 該方法通過將資源路徑、過期時間和密鑰拼接后進行 SHA-256 哈希計算,* 最終將計算得到的哈希值轉換為十六進制字符串作為令牌返回。** @param resourcePath 請求的資源路徑,用于標識具體要訪問的資源* @param expirationTime 令牌的過期時間,以毫秒為單位* @return 生成的令牌,是一個十六進制字符串*/public static String generateToken(String resourcePath, long expirationTime) {String date = resourcePath + expirationTime + SECRET_KEY;try {// 借助 MessageDigest.getInstance("SHA-256") 方法獲取一個 MessageDigest 實例,// 該實例使用的哈希算法為 SHA-256。SHA-256 屬于安全哈希算法,能夠將任意長度的輸入數據轉換為固定長度(256 位)的哈希值。MessageDigest digest = MessageDigest.getInstance("SHA-256");//調用 digest 方法對拼接后的字符串 data 進行哈希計算,返回一個字節數組 hash,此數組就是 data 的 SHA-256 哈希值。byte[] hash = digest.digest(date.getBytes());StringBuilder hexString = new StringBuilder();for (byte b : hash) {//字節轉換為對應的十六進制字符串。String hex = Integer.toHexString(0xff & b);if (hex.length() == 1) {//轉換后的十六進制字符串長度為 1,在前面補一個 0,以保證每個字節都用兩位十六進制數表示。hexString.append('0');}//轉換后的十六進制字符串追加到 hexString 中。hexString.append(hex);}return hexString.toString();} catch (NoSuchAlgorithmException e) {throw new RuntimeException(e);}}public static boolean verifyToken(String resourcePath, String token, long expirationTime) {if (new Date().getTime() > expirationTime) {return false;}String generatedToken = generateToken(resourcePath, expirationTime);return generatedToken.equals(token);}}
- 創建控制器處理請求并驗證
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import java.util.Date;@RestController
@Slf4j
public class TestController {/*** curl --location '127.0.0.1:2223/test' \* --header 'Referer: 127.0.0.1'** header頭不傳合法的Referer 127.0.0.1則拒絕訪問* @return*/@RequestMapping("/test")public String test() {return "ok";}@GetMapping("/protectedResource")public ResponseEntity<String> getProtectedResource(@RequestParam String token, @RequestParam long expirationTime) {String resourcePath = "/protectedResource";if (TokenGeneratorUtils.verifyToken(resourcePath, token, expirationTime)) {return ResponseEntity.ok("Access granted");} else {return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid token");}}public static void main(String[] args) {// 資源路徑String resourcePath = "/protectedResource";// 設置令牌過期時間,這里設置為當前時間往后 1 小時long expirationTime = new Date().getTime() + 3600 * 1000;// 生成令牌String token = TokenGeneratorUtils.generateToken(resourcePath, expirationTime);System.out.println(token);System.out.println(expirationTime);}
}
總結
- 基于請求頭 Referer 的防盜鏈:實現簡單,但 Referer 字段容易被偽造。
- 基于令牌(Token)的防盜鏈:安全性較高,但實現相對復雜,需要處理令牌的生成和驗證邏輯。