一、版本说明
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 }
七、项目开源地址
目前项目正在进行大规模调整,后期再进行开放;