1、認識攔截器
SpringMVC的攔截器(Interceptor)不是Filer,同樣可以實現請求的預處理、后處理。使用攔截器僅需要兩個步驟
實現攔截器
注冊攔截器
1.1實現攔截器
實現攔截器可以自定義實現HandleInterceptor接口,也可以繼承HandleInterceptorAdatper類,后者是前者的實現類。
下面是攔截器實現的一個例子,目的是判斷用戶是否登錄。如果preHandle方法return true ,則后續方法繼續執行。
1 package zsjmsdemo.interceptor; 2 3 import java.io.PrintWriter; 4 import java.util.Set; 5 import java.util.concurrent.TimeUnit; 6 7 import javax.servlet.http.Cookie; 8 import javax.servlet.http.HttpServletRequest; 9 import javax.servlet.http.HttpServletResponse; 10 11 import org.springframework.data.redis.core.RedisTemplate; 12 import org.springframework.util.ObjectUtils; 13 import org.springframework.web.servlet.ModelAndView; 14 import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 15 16 import com.alibaba.fastjson.JSON; 17 import com.ezhiyang.xxxb.common.DataResult; 18 import com.ezhiyang.xxxb.utils.MobileUtil; 19 20 public class LoginInterceptor extends HandlerInterceptorAdapter { 21 22 private RedisTemplate<String, Object> redisTemplate; 23 24 /** 25 * 預處理回調方法,實現處理器的預處理(如登陸檢查/判斷同一對象短時間內是否重復調用接口等) 第三個參數為相應的處理器即controller 26 * f返回true表示流程繼續,調用下一個攔截器或者處理器,返回false表示流程中斷,通過response產生響應 27 */ 28 @Override 29 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 30 throws Exception { 31 String token = getToken(request); 32 if (!ObjectUtils.isEmpty(token)) { 33 //判斷同一用戶短時間內是否重復請求接口 34 String method = request.getMethod(); 35 if (method!=null&&("POST".equalsIgnoreCase(method)||"GET".equalsIgnoreCase(method))) { 36 String requestUri = request.getRequestURI(); 37 String url = requestUri.substring(request.getContextPath().length());//獲取此次請求訪問的是哪個接口 38 String ip=MobileUtil.getIpAddr(request);//獲取當前的ip地址值 39 String requestKey=ip+"_"+token+"_"+url; 40 Object object = redisTemplate.opsForValue().get("requestKey"); 41 //若重復請求則提示 42 if (!ObjectUtils.isEmpty(object)) { 43 DataResult dataResult=new DataResult(); 44 dataResult.setCode("4001"); 45 dataResult.setMsg("請求太頻繁,請稍后再試"); 46 response.setCharacterEncoding("utf-8"); 47 response.setContentType("text/html; charset=utf-8"); 48 PrintWriter writer=response.getWriter(); 49 writer.print(JSON.toJSON(dataResult)); 50 writer.close(); 51 response.flushBuffer(); 52 return false; 53 } 54 //若是第一次請求則記錄請求標記,在處理器執行完成后刪除標記 55 redisTemplate.opsForValue().set(requestKey, url, 5, TimeUnit.SECONDS);//設置5秒,只是為了防止攔截器的后置處理方法沒執行到(比如突然斷電),導致后續的同類請求都不能執行 56 //如果請求允許,就記住key,請求處理完后,還要刪除標識 57 request.setAttribute("ACCESS_KEY", requestKey); 58 } 59 60 //根據token從redis中獲取登錄信息 61 Set<String> keys = redisTemplate.keys(token); 62 if (!ObjectUtils.isEmpty(keys) && keys.size() == 1) { 63 Object nimitokenvalue = redisTemplate.opsForValue().get(keys.iterator().next()); 64 if (ObjectUtils.isEmpty(nimitokenvalue)) { 65 return true; 66 } 67 } else { 68 Object loginInfo = redisTemplate.boundValueOps(token) 69 .get(); 70 if (loginInfo != null) { 71 return true; 72 } 73 } 74 }else{ 75 response.setContentType("text/html; charset=utf-8"); 76 PrintWriter writer = response.getWriter(); 77 writer.print(new DataResult("4001", "沒登陸", new Object())); 78 writer.close(); 79 response.flushBuffer(); 80 return false; 81 } 82 return super.preHandle(request, response, handler); 83 } 84 85 86 /** 87 * 當請求進行處理之后,也就是controller方法調用之后執行,但是他會在DispatcherServlet進行視圖渲染之前被調用 88 * 此時我們可以通過modelAndView對模型數據進行處理或對視圖進行處理 89 */ 90 @Override 91 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, 92 ModelAndView modelAndView) throws Exception { 93 System.out.println("-------------------postHandle"); 94 String requestKey = (String)request.getAttribute("ACCESS_KEY"); 95 if (requestKey!=null) { 96 redisTemplate.delete(requestKey); 97 } 98 } 99 100 /** 101 * 方法將在整個請求結束之后,也就是DispatcheServlet進行視圖渲染之后執行,這個方法的主要作用是對資源的清理工作 102 */ 103 @Override 104 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 105 throws Exception { 106 System.out.println("-------------------afterCompletion"); 107 } 108 109 @Override 110 public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) 111 throws Exception { 112 } 113 114 115 /** 116 * 獲取token 117 * @param req 118 * @return 119 */ 120 public static String getToken(HttpServletRequest req) { 121 String token = req.getParameter("token"); 122 if (!ObjectUtils.isEmpty(token)) { 123 return token; 124 } else { 125 Cookie[] cks = req.getCookies(); 126 if (cks != null) { 127 for (Cookie ck : cks) { 128 if (ck.getName().equals("token")) { 129 return ck.getValue(); 130 } 131 } 132 } 133 return req.getHeader("token"); 134 } 135 } 136 137 }
1.2注冊攔截器
1 package com.cxs.allmodel.interceptor; 2 3 import javax.annotation.Resource; 4 5 import org.springframework.context.annotation.Configuration; 6 import org.springframework.data.redis.core.RedisTemplate; 7 import org.springframework.web.servlet.config.annotation.InterceptorRegistration; 8 import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 9 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 10 11 /** 12 * 為了使自定義的攔截器生效,需要注冊攔截器到spring容器中,具體的做法是繼承WebMvcConfigurerAdapter類, 13 * 覆蓋其addInterceptors(InterceptorRegistry registry)方法。最后別忘了把Bean注冊到Spring容器中, 14 * 可以選擇@Component 或者 @Configuration。 15 * 16 */ 17 @Configuration 18 public class InterceptorConfig implements WebMvcConfigurer{ 19 //在攔截器執行時實例化redisTemplate 20 @Resource 21 private RedisTemplate<String, Object> redisTemplate; 22 23 @Override 24 public void addInterceptors(InterceptorRegistry registry) { 25 // 注冊攔截器 26 InterceptorRegistration ir = registry.addInterceptor(new LoginInterceptor(redisTemplate)); 27 // 配置攔截的路徑 28 ir.addPathPatterns("/**"); 29 // 配置不攔截的路徑 30 ir.excludePathPatterns("/user/info","/user/add"); 31 32 // 還可以在這里注冊其它的攔截器 33 //registry.addInterceptor(new OtherInterceptor()).addPathPatterns("/**"); 34 } 35 }
也可以直接在SpringBoot的啟動類中繼承WebMvcConfigurerAdaoter類
1 package cn.wowkai.mall; 2 3 import java.nio.charset.Charset; 4 import java.util.Arrays; 5 6 import javax.annotation.PostConstruct; 7 import javax.annotation.Resource; 8 import javax.sql.DataSource; 9 10 import org.springframework.boot.SpringApplication; 11 import org.springframework.boot.autoconfigure.SpringBootApplication; 12 import org.springframework.context.annotation.Bean; 13 import org.springframework.data.redis.core.RedisTemplate; 14 import org.springframework.data.redis.serializer.StringRedisSerializer; 15 import org.springframework.http.HttpHeaders; 16 import org.springframework.scheduling.annotation.EnableScheduling; 17 import org.springframework.web.cors.CorsConfiguration; 18 import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 19 import org.springframework.web.filter.CorsFilter; 20 import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 21 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 22 23 import com.fd.myshardingfordata.helper.ConnectionManager; 24 import com.fd.myshardingfordata.helper.TransManager; 25 26 import cn.wowkai.mall.web.interceptor.AuthInterceptor; 27 28 @EnableScheduling 29 @SpringBootApplication 30 public class ShopSaasMallApplication implements WebMvcConfigurer { 31 32 public static void main(String[] args) { 33 SpringApplication.run(ShopSaasMallApplication.class, args); 34 } 35 36 @Resource 37 protected RedisTemplate<String, Object> redisTemplate; 38 39 @PostConstruct 40 private void init() { 41 redisTemplate.setKeySerializer(new StringRedisSerializer(Charset.forName("UTF8"))); 42 redisTemplate.setValueSerializer(new StringRedisSerializer(Charset.forName("UTF8"))); 43 } 44 45 @Override 46 public void addInterceptors(InterceptorRegistry registry) { 47 registry.addInterceptor(new AuthInterceptor(redisTemplate)).excludePathPatterns("/mall/mp/*", 48 "/mall/notify/panganNotify", "/mall/notify/panganRechargeNotify", "/mall/notify/panganbillNotify", 49 "/mall/notify/wxpayrechargenotify", "/mall/notify/wxpaybillnotify", "/mall/notify/wxpaynotify", 50 "/mall/api/getToken", "/mall/store/getStoreListWithLongitudeLatitude", 51 "/mall/sett/getIndexTemplateListWithXcx", "/mall/product/productListForXcxSelect", 52 "/mall/product/custFindProduct", "/mall/product/custFindProductSpeceInventoryAndPrice", 53 "/mall/sett/xcx/getIndexTemplateMastList"); 54 } 55 56 @Resource 57 private DataSource dataSource; 58 59 @Bean 60 public TransManager transManager() { 61 TransManager trans = new TransManager(); 62 trans.setConnectionManager(connectionManager()); 63 return trans; 64 } 65 66 @Bean 67 public ConnectionManager connectionManager() { 68 ConnectionManager conm = new ConnectionManager(); 69 conm.setGenerateDdl(true); 70 conm.setShowSql(false); 71 conm.setInitConnect("set names utf8mb4"); 72 conm.setDataSource(dataSource); 73 conm.setReadDataSources(Arrays.asList(dataSource)); 74 75 return conm; 76 } 77 78 @Bean 79 public CorsFilter corsFilter() { 80 // 1.添加CORS配置信息 81 CorsConfiguration config = new CorsConfiguration(); 82 // 放行哪些原始域 83 config.addAllowedOrigin("*"); 84 // 是否發送Cookie信息 85 config.setAllowCredentials(true); 86 // 放行哪些原始域(請求方式) 87 config.addAllowedMethod("*"); 88 // 放行哪些原始域(頭部信息) 89 config.addAllowedHeader("*"); 90 // 暴露哪些頭部信息(因為跨域訪問默認不能獲取全部頭部信息) 91 config.addExposedHeader(HttpHeaders.LOCATION); 92 config.setExposedHeaders(Arrays.asList("JSESSIONID", "SESSION", "token", HttpHeaders.LOCATION, 93 HttpHeaders.ACCEPT, HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, 94 HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, HttpHeaders.COOKIE, HttpHeaders.SET_COOKIE, 95 HttpHeaders.SET_COOKIE2)); 96 // 2.添加映射路徑 97 UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); 98 configSource.registerCorsConfiguration("/**", config); 99 100 // 3.返回新的CorsFilter. 101 return new CorsFilter(configSource); 102 } 103 104 }
1.3攔截器的應用場景
攔截器的本質是面向切面編程(AOP),符合橫切關注點的功能都可以放在攔截器中來實現,主要的應用場景包括:
1、登錄驗證,判斷用戶是否登錄。
2、權限驗證,判斷用戶是否具有訪問權限。
3、日志記錄,記錄請求日志,以便統計請求訪問量。
4、處理cookie、本地化、國際化、主題等。
5、性能監控,監控請求處理時長等。
6、防止同一用戶在短時間內重復請求接口
2、原理
2.1、工作原理
攔截器不是Filter,卻實現了filter的功能,其原理在於:
所有的攔截器(Interceptor)和處理器(Handler)都注冊在HandlerMapping中。
Spring MVC中所有的請求都是由DispatcherServlet
分發的。
DispatcherServlet.doDispatch()
時候,首先會得到處理該請求的Handler(即Controller中對應的方法)以及所有攔截該請求的攔截器。攔截器就是在這里被調用開始工作的。
一個攔截器,只有preHandle
方法返回true,postHandle
、afterCompletion
才有可能被執行;如果preHandle
方法返回false,則該攔截器的postHandle
、afterCompletion
必然不會被執行。
假設我們有兩個攔截器,例如叫Interceptor1和Interceptor2,當一個請求過來,正常的流程和中斷的流程分別如下。
2.2.1正常流程
注意兩個攔截器在執行preHandle
方法和執行postHandle
、afterCompletion
方法時,順序是顛倒的。
1 Interceptor1.preHandle 2 3 Interceptor2.preHandle 4 5 //Controller處理請求 6 7 Interceptor2.postHandle 8 9 Interceptor1.postHandle 10 11 //渲染視圖 12 13 Interceptor2.afterCompletion 14 15 Interceptor1.afterCompletion
2.2.2中斷流程
Interceptor2.preHandle
中報錯,那么流程被中斷,之前被執行過的攔截器的afterCompletion仍然會執行。在本例中,即執行了
Interceptor1.afterCompletion
。
1 Interceptor1.preHandle 2 3 Interceptor2.preHandle 4 5 //中間流程被中斷,不再執行 6 7 Interceptor1.afterCompletion
2.3和Filter共存時的執行順序
攔截器是在DispatcherServlet
這個servlet中執行的,因此所有的請求最先進入Filter,最后離開Filter。其順序如下。
1 Filter 2 3 Interceptor.preHandle 4 5 Handler 6 7 Interceptor.postHandle 8 9 Interceptor.afterCompletion 10 11 Filter