一、版本說明
SpringBoot:2.1.6.RELEASE
SpringCloud:Greenwich.RELEASE
二、功能說明
上一章節我們介紹了“注冊中心”,下面我們將開始配置網關路由,個人理解網關路由是對“注冊中心”的所有組件進行分發調用,在這里可以進行權限的驗證、訪問的分流與限流等操作;
Zuul大部分功能都是通過過濾器來實現的。Zuul中定義了四種標准過濾器類型,這些過濾器類型對應於請求的典型生命周期。
(1) PRE:這種過濾器在請求被路由之前調用。我們可利用這種過濾器實現身份驗證、在集群中選擇請求的微服務、記錄調試信息等。
(2) ROUTING:這種過濾器將請求路由到微服務。這種過濾器用於構建發送給微服務的請求,並使用Apache HttpClient或Netfilx Ribbon請求微服務。
(3) POST:這種過濾器在路由到微服務以后執行。這種過濾器可用來為響應添加標准的HTTP Header、收集統計信息和指標、將響應從微服務發送給客戶端等。
(4) ERROR:在其他階段發生錯誤時執行該過濾器。
三、Maven主要配置
1 <modelVersion>4.0.0</modelVersion> 2 <parent> 3 <groupId>org.springframework.boot</groupId> 4 <artifactId>spring-boot-starter-parent</artifactId> 5 <version>2.1.6.RELEASE</version> 6 <relativePath/> <!-- lookup parent from repository --> 7 </parent> 8 <groupId>【項目自定義】</groupId> 9 <artifactId>【項目自定義】</artifactId> 10 <version>【版本自定義】</version> 11 <name>【名稱自定義】</name> 12 <packaging>jar</packaging> 13 <description>路由控制中心——獨立運行的服務</description> 14 15 <properties> 16 <java.version>1.8</java.version> 17 </properties> 18 19 <dependencies> 20 <!--eureka客戶端--> 21 <dependency> 22 <groupId>org.springframework.cloud</groupId> 23 <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> 24 </dependency> 25 <!--流量限流--> 26 <dependency> 27 <groupId>com.marcosbarbero.cloud</groupId> 28 <artifactId>spring-cloud-zuul-ratelimit</artifactId> 29 <version>1.5.0.RELEASE</version> 30 </dependency> 31 <!--網關路由--> 32 <dependency> 33 <groupId>org.springframework.cloud</groupId> 34 <artifactId>spring-cloud-starter-netflix-zuul</artifactId> 35 </dependency> 36 <dependency> 37 <groupId>net.sf.json-lib</groupId> 38 <artifactId>json-lib</artifactId> 39 <version>2.4</version> 40 <classifier>jdk15</classifier> 41 </dependency> 42 <dependency> 43 <groupId>org.projectlombok</groupId> 44 <artifactId>lombok</artifactId> 45 <optional>true</optional> 46 </dependency> 47 </dependencies> 48 49 <!--spring cloud版本--> 50 <dependencyManagement> 51 <dependencies> 52 <dependency> 53 <groupId>org.springframework.cloud</groupId> 54 <artifactId>spring-cloud-dependencies</artifactId> 55 <version>Greenwich.RELEASE</version> 56 <type>pom</type> 57 <scope>import</scope> 58 </dependency> 59 </dependencies> 60 </dependencyManagement> 61 62 <!-- 打包spring boot應用 --> 63 <build> 64 <finalName>gateway-center-independent-service</finalName> 65 <plugins> 66 <plugin> 67 <groupId>org.springframework.boot</groupId> 68 <artifactId>spring-boot-maven-plugin</artifactId> 69 <configuration> 70 <fork>true</fork> 71 </configuration> 72 </plugin> 73 </plugins> 74 <resources> 75 <resource> 76 <directory>src/main/java</directory> 77 <includes> 78 <include>**/*.xml</include> 79 </includes> 80 <filtering>true</filtering> 81 </resource> 82 </resources> 83 </build>
四、配置文件(application.properties)
server.port=【服務端口號】
#服務名
spring.application.name=【服務名稱】
#服務介紹
xinyan.server.message=這個是zuul組件
#遠程SpringCloud配置
#eureka主機名,會在控制頁面中顯示
eureka.instance.hostname=【IP】
#eureka服務器頁面中status的請求路徑
eureka.instance.status-page-url=http://【IP】:【服務端口號】/index
eureka.instance.preferIpAddress=false
#eureka.instance.instance-id=${spring.application.name}:${spring.application.instance_id:${server.port}}
eureka.instance.appname=zuul-gateway
#eureka注冊中心服務器地址
eureka.client.serviceUrl.defaultZone=http://【注冊中心設置的用戶名】:【注冊中心設置的密碼】@【注冊中心IP】:【注冊中心端口號】/eureka
#eureka.instance.instance-id=${spring.application.name}:${spring.application.instance_id:${server.port}}
eureka.instance.instance-id=${eureka.instance.hostname}:${server.port}
#新版配置,否則后面dashboard無法找到hystrix.stream
management.endpoints.web.exposure.include=*
ribbon.eureka.enabled=true
eureka.client.registerWithEureka=true
eureka.client.fetchRegistry=true
#忽略所有未配置的service,每一個微服務的路由配置都需要配置
zuul.ignored-services="*"
#需要忽略的頭部信息,不在傳播到其他服務
zuul.sensitive-headers=Access-Control-Allow-Origin
zuul.ignored-headers=Access-Control-Allow-Origin,H-APP-Id,Token,APPToken
zuul.routes.xinyan-test2-demo.path=/xinyan-test2-demo/**
zuul.routes.xinyan-test2-demo.serviceId=xinyan-test2-demo
zuul.routes.xinyan-test2-demo.sensitiveHeaders=true
zuul.routes.xinyan-test1-demo.path=/xinyan-test1-demo/**
zuul.routes.xinyan-test1-demo.serviceId=xinyan-test1-demo
zuul.routes.xinyan-test1-demo.sensitiveHeaders=true
zuul.ratelimit.enabled=true
zuul.ratelimit.repository=IN_MEMORY
zuul.ratelimit.behind-proxy=true
#每個刷新時間窗口對應的請求數量限制
zuul.ratelimit.policies.xinyan-test2-demo.limit=1
#每個刷新時間窗口對應的請求時間限制(秒)
zuul.ratelimit.policies.xinyan-test2-demo.quota=1
#刷新時間窗口的時間,默認值 (秒)
zuul.ratelimit.policies.【需要限制的組件名稱】.refresh-interval=3
zuul.ratelimit.policies.【需要限制的組件名稱】.type[0]=url
zuul.ratelimit.policies.【需要限制的組件名稱】.type[1]=origin
zuul.ratelimit.policies.【需要限制的組件名稱】.type[2]=user
這里特別說明:這里有個天坑,所有注冊到“注冊中心”的組件名稱不能包含下划線,否則無法識別;
五、代碼部分
5.1、重寫異常交互
在調用網關發生異常或者限流異常等返回的信息不是非常友好,所以這里要對異常的交互Controller進行重寫;
1 /** 2 * 類描述: 重寫系統/error接口 3 * 4 * @author xxsd 5 * @version 1.0.0 6 * @date 2019/6/20 0020 下午 8:56 7 */ 8 @RestController 9 @Slf4j 10 public class ErrorController extends AbstractErrorController { 11 12 public ErrorController(ErrorAttributes errorAttributes) { 13 super(new GlobalExceptionHandler()); 14 } 15 16 @Override 17 public String getErrorPath() { 18 return null; 19 } 20 21 /** 22 * 屬性描述:路徑 23 * 24 * @date : 2019/6/20 0020 下午 9:09 25 */ 26 private static final String ERROR_PATH = "/error"; 27 /** 28 * 屬性描述:正常參數 29 * 30 * @date : 2019/6/20 0020 下午 9:09 31 */ 32 private static final int OK = 200; 33 // private static final String ERROR_MESSAGE = "系統內部錯誤,請聯系管理員!"; 34 35 @RequestMapping(ERROR_PATH) 36 public void exceptionHandler(HttpServletRequest request, HttpServletResponse response) { 37 // 返回成功狀態 38 response.setStatus(OK); 39 40 Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(request, false)); 41 int status = Integer.valueOf(model.get("zuul_code").toString()); 42 String message = (String) model.get("zuul_manageMessage"); 43 44 // 判斷請求類型 45 String header = request.getHeader("X-Requested-With"); 46 //是否是Ajax提交 47 boolean isAjax = "XMLHttpRequest".equalsIgnoreCase(header); 48 49 Map<String, Object> error = new HashMap<>(2); 50 error.put("code", status); 51 error.put("manageMessage", message); 52 53 if (isAjax) { 54 log.info("Ajax交互"); 55 } else { 56 log.info("非Ajax交互"); 57 } 58 writeJson(response, error); 59 } 60 61 /** 62 * 功能描述:以Json的格式輸出信息 63 * @author : xxsd 64 * @date : 2019/6/21 0021 下午 12:07 65 */ 66 private void writeJson(HttpServletResponse response, Map<String, Object> error) { 67 try { 68 response.setContentType("text/html;charset=UTF-8"); 69 response.getWriter().write(JSONObject.fromObject(error).toString()); 70 } catch (IOException e) { 71 e.printStackTrace(); 72 } 73 } 74 75 }
全局異常攔截器:
1 /** 2 * 類描述: 全局異常攔截器 3 * 4 * @author xxsd 5 * @version 1.0.0 6 * @date 2019/6/20 0020 下午 5:53 7 */ 8 public class GlobalExceptionHandler extends DefaultErrorAttributes { 9 10 @Override 11 public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { 12 Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace); 13 14 try { 15 final Integer status = (Integer) errorAttributes.get("status"); 16 //TODO 下面還可以直接添加系統級別的異常碼及說明 17 ErrorCodeEntity errorCodeEntity = ErrorCodeEntity.valueOf("Code_" + status); 18 assert errorCodeEntity != null; 19 errorAttributes.put("zuul_code", errorCodeEntity.giveCodeNumber()); 20 errorAttributes.put("zuul_manageMessage", errorCodeEntity.giveCodeMessage()); 21 } catch (Exception e) { 22 errorAttributes.put("zuul_code", 0); 23 errorAttributes.put("zuul_manageMessage", "未知狀態碼"); 24 } 25 return errorAttributes; 26 } 27 }
5.2、定義Zuul過濾器抽象基類
1 /** 2 * 類描述: 自定義過濾器 3 * 4 * @author xxsd 5 * @version 1.0.0 6 * @date 2019/6/19 0019 下午 3:59 7 */ 8 @Slf4j 9 public abstract class AbstractZuulFilter extends ZuulFilter { 10 protected RequestContext context; 11 12 @Override 13 public boolean shouldFilter() { 14 RequestContext ctx = RequestContext.getCurrentContext(); 15 return (boolean) (ctx.getOrDefault(ContantValue.NEXT_FILTER, true)); 16 } 17 18 @Override 19 public Object run() { 20 context = RequestContext.getCurrentContext(); 22 return doRun(); 23 } 24 25 public abstract Object doRun(); 26 27 /** 28 * 功能描述:異常信息輸出 29 * 30 * @param code 錯誤編碼 31 * @param message 錯誤信息 32 * @return : java.lang.Object 33 * @author : xxsd 34 * @date : 2019/6/21 0021 上午 11:35 35 */ 36 protected Object fail(Integer code, String message) { 37 context.set(ContantValue.NEXT_FILTER, false); 38 context.setSendZuulResponse(false); 39 context.getResponse().setContentType("text/html;charset=UTF-8"); 40 context.setResponseStatusCode(code); 41 context.setResponseBody(String.format("{\"result\":\"%s!\"}", message)); 42 return null; 43 } 44 45 /** 46 * 功能描述:成功輸出 47 * @author : xxsd 48 * @date : 2019/6/21 0021 上午 11:36 49 */ 50 protected Object success() { 51 context.set(ContantValue.NEXT_FILTER, true); 52 return null; 53 } 54 55 /** 56 * 功能描述:獲取提交的數據內容及類型 57 * @author : xxsd 58 * @date : 2019/6/27 0027 下午 7:13 59 */ 60 protected Map<String, Object> giveSubDataOrType(){ 61 62 Map<String, Object> returnMap = new HashMap<>(2); 63 64 HttpServletRequest req = RequestContext.getCurrentContext().getRequest(); 65 System.err.println("REQUEST:: " + req.getScheme() + " " + req.getRemoteAddr() + ":" + req.getRemotePort()); 66 System.err.println("REQUEST:: " + req.getScheme() + " " + req.getRemoteAddr() + ":" + req.getRemotePort()); 67 StringBuilder params = new StringBuilder("?"); 68 Enumeration<String> names = req.getParameterNames(); 69 if(ContantValue.SUP_TYPE.equals(req.getMethod())) { 70 while (names.hasMoreElements()) { 71 String name = names.nextElement(); 72 params.append(name); 73 params.append("="); 74 params.append(req.getParameter(name)); 75 params.append("&"); 76 } 77 } 78 79 if (params.length() > 0) { 80 params.delete(params.length()-1, params.length()); 81 } 82 83 returnMap.put("method", req.getMethod()); 84 85 System.err.println("REQUEST:: > " + req.getMethod() + " " + req.getRequestURI() + params + " " + req.getProtocol()); 86 87 Enumeration<String> headers = req.getHeaderNames(); 88 89 while (headers.hasMoreElements()) { 90 String name = headers.nextElement(); 91 String value = req.getHeader(name); 92 System.err.println("REQUEST:: > " + name + ":" + value); 93 } 94 95 final RequestContext ctx = RequestContext.getCurrentContext(); 96 if (!ctx.isChunkedRequestBody()) { 97 ServletInputStream inp; 98 try { 99 inp = ctx.getRequest().getInputStream(); 100 String body; 101 if (inp != null) { 102 body = IOUtils.toString(inp); 103 System.err.println("REQUEST:: > " + body); 104 returnMap.put("inputStream", JSONObject.fromObject(body)); 105 } 106 } catch (IOException e) { 107 e.printStackTrace(); 108 } 109 } 110 111 return returnMap; 112 } 113 }
5.3、自定義Zuul異常攔截器
1 /** 2 * 類描述: 自定義異常攔截器 3 * @author : xxsd 4 * @date : 2019/6/21 0021 上午 11:38 5 */ 6 @Component 7 public class ErrorHandlerFilter extends AbstractZuulFilter { 8 @Override 9 public String filterType() { 10 return ERROR_TYPE; 11 } 12 13 @Override 14 public int filterOrder() { 15 return SEND_RESPONSE_FILTER_ORDER + 1; 16 } 17 18 @Override 19 public Object doRun() { 20 return success(); 21 } 22 23 }
5.4、自定義請求PRE攔截器
1 /** 2 * 類描述: 自定義PRE過濾器 3 * 4 * @author xxsd 5 * @version 1.0.0 6 * @date 2019/6/19 0019 下午 4:23 7 */ 8 @Slf4j 9 @Component 10 public class PreHandlerFilter extends AbstractZuulFilter { 11 12 @Override 13 public String filterType() { 14 return PRE_TYPE; 15 } 16 17 /** 18 * 每秒允許處理的量是50 19 */ 20 RateLimiter rateLimiter = RateLimiter.create(50); 21 22 @Override 23 public int filterOrder() { 24 return FilterOrder.RATE_LIMITER_ORDER; 25 } 26 27 private static String INPUT_STREAM = "inputStream"; 28 29 @Override 30 public Object doRun() { 31 HttpServletRequest request = context.getRequest(); 32 String url = request.getRequestURI(); 33 if (rateLimiter.tryAcquire()) { 34 Map<String, Object> stringObjectMap = giveSubDataOrType(); 35 36 System.out.println(SystemAttribute.getDebugType()); 37 System.out.println(SystemAttribute.getSsoCodeId()); 38 if (SystemAttribute.getDebugType()) { 39 System.out.println(stringObjectMap.get("method").toString()); 40 System.out.println(stringObjectMap.get("inputStream").toString()); 41 } 42 43 if (stringObjectMap.get(INPUT_STREAM) != null && !"".equals(stringObjectMap.get(INPUT_STREAM).toString().trim())) { 44 if (SystemAttribute.getSsoCodeId() != null && !"".equals(SystemAttribute.getSsoCodeId())) { 45 //TODO 這里開始驗證邏輯 46 } 47 } 48 49 return success(); 50 } else { 51 log.info("rate limit:{}", url); 52 return fail(401, String.format("rate limit:{}", url)); 53 } 54 } 55 }
5.4、啟動入口
/** * 類描述: 路由控制中心啟動入口 * @author : xxsd * @date : 2019/6/27 0027 下午 6:19 */ @SpringBootApplication @EnableDiscoveryClient @EnableZuulProxy public class GatewayCenterIndependentServiceApplication { public static void main(String[] args) { SpringApplication.run(GatewayCenterIndependentServiceApplication.class, args); } }
六、跨域處理
目前開發模式已經從原來的“一鍋端”發展到現在的前后端分離,那么很多人都在問我跨域如何處理,目前我使用的方式沒有發現有其他的問題出現,現在將這個代碼發出來,大家一起看看;
1 /** 2 * 類描述: 跨域配置 3 * 4 * @author xxsd 5 * @version 1.0.0 6 * @date 2019/6/19 0019 下午 5:08 7 */ 8 @Configuration 9 public class CorsConfig { 10 @Bean 11 public FilterRegistrationBean corsFilter() { 12 final ResourceHttpRequestHandler resourceHttpRequestHandler = new ResourceHttpRequestHandler(); 13 final CorsConfiguration corsConfiguration = new CorsConfiguration(); 14 corsConfiguration.setAllowCredentials(true); 15 corsConfiguration.addAllowedOrigin("*"); 16 corsConfiguration.addAllowedHeader("*"); 17 corsConfiguration.addAllowedMethod("*"); 18 //corsConfiguration.addExposedHeader("X-forwared-port, X-forwarded-host"); 19 resourceHttpRequestHandler.setCorsConfiguration(corsConfiguration); 20 FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(resourceHttpRequestHandler)); 21 bean.setOrder(Ordered.HIGHEST_PRECEDENCE); 22 return bean; 23 } 24 }
七、項目開源地址
目前項目正在進行大規模調整,后期再進行開放;