一、Spring Boot中攔截器是什么
??在Spring Boot中,攔截器(Interceptor)是一種基于AOP(面向切面編程)思想的組件,用于在請求處理前后插入自定義邏輯,實現權限校驗、日志記錄、性能監控等非業務功能。
??其核心作用是在不修改業務代碼的前提下,對請求進行統一處理,類似于Servlet中的Filter,但更貼近Spring MVC的體系。
二、攔截器與過濾器的區別
三、攔截器主要方法
在實現攔截器時,HandlerInterceptor接口提供了三個主要方法,它們在請求處理的不同階段發揮著重要作用。
?
preHandle
??這個方法在請求進入Controller之前被調用 。它的返回值是一個布爾類型,如果返回true,請求將繼續被處理,進入Controller。
?? 如果返回false,請求將被攔截,后續的Controller方法將不會被執行。在這個方法中,我們通常可以進行一些前置的處理操作,比如用戶認證、權限校驗、日志記錄等。?
postHandle
??當Controller方法執行完畢后,視圖渲染之前,這個方法會被調用 。
??在這個方法中,我們可以對ModelAndView進行一些操作,比如添加額外的模型數據,修改視圖名稱等。需要注意的是,如果在preHandle方法中返回了false,這個方法將不會被執行。?
afterCompletion
??在整個請求處理完成,包括視圖渲染之后,這個方法會被調用。無論請求處理過程中是否發生異常,只要preHandle方法返回true,這個方法就會被執行。
??我們可以在這個方法中進行一些資源清理、記錄日志等收尾工作。如果請求處理過程中發生了異常,異常信息會通過 ex 參數傳遞進來,我們可以根據這個參數進行相應的異常處理。
@Component
public class MyInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 在請求處理之前執行的邏輯System.out.println("preHandle被調用,請求即將進入Controller");return true; // 返回true表示放行請求,返回false則攔截請求}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {// 在請求處理之后,視圖渲染之前執行的邏輯System.out.println("postHandle被調用,Controller方法已執行完畢,視圖即將渲染");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 在整個請求處理完成,包括視圖渲染之后執行的邏輯System.out.println("afterCompletion被調用,整個請求已處理完畢");}
}
@Configuration
public class WebMvcConfig1 implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**") // 攔截所有請求.excludePathPatterns("/static/**"); // 排除/static目錄下的靜態資源請求}
}
四、5 種常見的攔截器使用場景
1.用戶認證攔截器
??用戶認證攔截器的作用就是在用戶請求進入 Controller 之前,驗證用戶的登錄狀態。如果用戶已經登錄,允許請求繼續處理。如果用戶未登錄,則返回錯誤信息或者重定向到登錄頁面。
@Component
public class JwtHandlerInterceptor implements HandlerInterceptor {@Autowiredprivate JwtTokenProvider jwtTokenProvider;@Autowiredprivate UserInfoService userInfoService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (!(handler instanceof HandlerMethod)) {return true;}// 取出tokenString token = request.getHeader("token");if (!jwtTokenProvider.validateToken(token)) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write("{\"error\": \"Token已失效,請重新登錄\"}");return false;}HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();// 判斷方法上是否有NoAuth注解,如果有則跳過認證if (method.isAnnotationPresent(NoAuth.class)) {return true;}String username = jwtTokenProvider.getUsernameFromJWT(token);User user = userInfoService.getUserInfoByUserName(username);if (user == null) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write("{\"error\": \"用戶不存在\"}");return false;}// 判斷角色權限HasRoles hasRoles = handlerMethod.getMethodAnnotation(HasRoles.class);if (!Objects.isNull(hasRoles)) {// 檢查用戶是否有所需角色String[] roles = hasRoles.value();boolean hasRole = false;// 角色校驗 ...if (!hasRole) {response.setStatus(HttpServletResponse.SC_FORBIDDEN);response.getWriter().write("{\"error\": \"權限不足\"}");return false;}}// 將用戶信息放入請求屬性request.setAttribute("currentUser", user);return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {HandlerInterceptor.super.afterCompletion(request, response, handler, ex);}
}
??在WebMvcConfig配置類中,將JwtHandlerInterceptor注冊到Spring的攔截器鏈中,并設置攔截路徑為所有請求,排除了登錄和注冊接口,因為這兩個接口不需要用戶登錄就可以訪問。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new JwtHandlerInterceptor()).addPathPatterns("/**")// 攔截所有請求.excludePathPatterns("/login","/register");// 排除登錄和注冊接口}}
??通過這樣的配置,就可以實現對用戶登錄狀態的驗證,確保只有登錄用戶才能訪問受保護的資源。
2.日志記錄攔截器
??日志記錄對于系統的運維和故障排查非常重要。日志記錄攔截器可以在請求處理的前后記錄請求的相關信息,比如請求的 URL、請求方法、請求參數、客戶端 IP 等。這些日志信息可以幫助我們了解系統的運行情況,分析用戶行為,在出現問題時快速定位問題。
@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {private static final Logger logger = LoggerFactory.getLogger(RequestLoggingInterceptor.class);@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {logger.info("Request URL: {}, Method: {}, IP: {}", request.getRequestURI(), request.getMethod(), request.getRemoteAddr());// 記錄請求參數Map<String, String[]> paramMap = request.getParameterMap();StringBuilder params = new StringBuilder();if (!paramMap.isEmpty()) {for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {params.append(entry.getKey()).append("=").append(String.join(",", entry.getValue())).append("&");}if (params.length() > 0) {params.deleteCharAt(params.length() - 1);}}// 記錄請求體(僅POST/PUT/PATCH請求)String method = request.getMethod();String requestBody = "";if (HttpMethod.POST.matches(method) ||HttpMethod.PUT.matches(method) ||HttpMethod.PATCH.matches(method)) {// 使用包裝請求對象來多次讀取請求體ContentCachingRequestWrapper wrappedRequest =new ContentCachingRequestWrapper(request);// 為了觸發內容緩存,我們需要獲取一次輸入流if (wrappedRequest.getContentLength() > 0) {wrappedRequest.getInputStream().read();requestBody = new String(wrappedRequest.getContentAsByteArray(),wrappedRequest.getCharacterEncoding());}}logger.info("Request URL: {}, Method: {}, IP: {},params: {},requestBody: {}", request.getRequestURI(), request.getMethod(), request.getRemoteAddr(), params, requestBody);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {logger.info("Request to {} has been completed", request.getRequestURI());if (ex != null) {logger.error("An exception occurred during request handling", ex);}}
}
3.性能監控攔截器
??性能監控對于優化系統性能至關重要。性能監控攔截器可以用來計算和記錄請求的處理時間,通過分析這些時間數據,我們可以找出系統中的性能瓶頸,進而進行針對性的優化。
@Component
public class PerformanceInterceptor implements HandlerInterceptor {private static final ThreadLocal<Long> startTimeThreadLocal = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {long startTime = System.currentTimeMillis();startTimeThreadLocal.set(startTime);return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {// 計算請求處理時間long endTime = System.currentTimeMillis();long startTime = startTimeThreadLocal.get();long executeTime = endTime - startTime;System.out.println("Request URL: " + request.getRequestURL() + ", Execution Time: " + executeTime + "ms");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {startTimeThreadLocal.remove();}
}
4.接口限流攔截器
??在高并發場景下,接口限流是保護系統的重要手段 。接口限流攔截器可以限制單位時間內對某個接口的訪問次數,防止因大量請求導致系統資源耗盡,從而保證系統的穩定性和可用性 。
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {@Autowiredprivate RedisTemplate<String, Integer> redisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (!(handler instanceof HandlerMethod)) {return true;}HandlerMethod handlerMethod = (HandlerMethod) handler;AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class);if (accessLimit == null) {return true;}int seconds = accessLimit.seconds();int maxCount = accessLimit.maxCount();boolean needLogin = accessLimit.needLogin();if (needLogin) {// 檢查用戶登錄狀態,這里省略具體實現// 如果未登錄,返回錯誤信息return false;}String key = request.getRemoteAddr() + request.getRequestURI();Integer count = redisTemplate.opsForValue().get(key);if (count == null) {redisTemplate.opsForValue().set(key, 1, seconds, TimeUnit.SECONDS);} else if (count < maxCount) {redisTemplate.opsForValue().increment(key, 1);} else {render(response, "請求過于頻繁,請稍后再試");return false;}return true;}private void render(HttpServletResponse response, String msg) throws IOException {response.setContentType("application/json;charset=UTF-8");PrintWriter out = response.getWriter();out.write(msg);out.flush();out.close();}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}}
5.數據加密解密攔截器
??在數據傳輸和存儲過程中,保護敏感數據的安全至關重要。數據加密解密攔截器可以在請求到達Controller之前對敏感數據進行加密,在響應返回給客戶端之前對加密數據進行解密,確保數據在傳輸和處理過程中的安全性。
??比如在用戶登錄時,對用戶輸入的密碼進行加密后再傳輸到服務器。在從數據庫中查詢用戶的身份證號、手機號等敏感信息時,對查詢結果進行解密后再返回給前端。
@Component
public class EncryptionInterceptor implements HandlerInterceptor {/*** 密鑰*/private static final String KEY = "secret_key";@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 假設請求參數中有一個名為"sensitiveData"的敏感數據需要加密String sensitiveData = request.getParameter("sensitiveData");if (sensitiveData != null) {SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), "AES");byte[] encryptedData = AESUtil.encrypt(sensitiveData, secretKey);String encryptedDataStr = Base64.getEncoder().encodeToString(encryptedData);// 將加密后的數據重新放回請求參數中request.setAttribute("sensitiveData", encryptedDataStr);}return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 假設響應中有一個名為"sensitiveResponseData"的敏感數據需要解密String sensitiveResponseData = (String) request.getAttribute("sensitiveResponseData");if (sensitiveResponseData != null) {SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), "AES");byte[] decodedData = Base64.getDecoder().decode(sensitiveResponseData);String decryptedData = AESUtil.decrypt(decodedData, secretKey);// 將解密后的數據重新放回請求屬性中,方便后續處理request.setAttribute("sensitiveResponseData", decryptedData);}}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);}
}
五、總結
??在 Spring Boot 開發中,攔截器就像是一把 “萬能鑰匙”,為我們提供了豐富的功能擴展和業務處理的可能性。
??希望大家在實際項目中,能夠根據業務需求,靈活地運用這些攔截器,讓我們的 Spring Boot 應用更加健壯、高效、安全。
??同時,攔截器還有很多值得深入探討的地方,比如如何在攔截器中優雅地處理事務、如何實現更加復雜的動態攔截規則等 ,后續我們可以一起繼續探索。
??如果大家在使用攔截器的過程中有任何問題或心得,歡迎在留言區分享交流。