希望长大对我而言,是可以做更多想做的事,而不是被迫做更多不想做的事...... 首页 spring cloud网关之zuul 丁D 学无止境 2020-03-20 19:40 54906已阅读 spring cloud zuul 网关 摘要本文将介绍网关服务zuul的基本用法,拦截器,基础配置,动态路由,高可用,鉴权。 ### zuul概述 在微服务架构盛行的年代,我们将一个大型的系统,拆解成各个服务,要完成一个业务逻辑,就可能需要,调用不同主机或不同端口的接口,这样的话看似清晰的服务拆分,实则杂乱无章。这样就我们就需要一个面向服务治理,服务编排的组件---微服务网关,于是乎zuul就出现了。 zuul用一句话来就是,网站到后端程序所有请求的前门。zuul本质上是由一系列的filter所构成的责任链。 网关一般需要具备如下功能: - 认证和鉴权 - 动态路由 - 流量管理转发限流 目前spring cloud gateway功能和性能更好,本文介绍zuul。 > 注意:Hystrix和Ribbon的超时时间,较小的值生效,Hystrix超时时间要设置比Ribbon大,不然熔断失效。 ribbon 总超时时间(conn_time+read_time)*(MaxAutoRetries + 1) * (MaxAutoRetriesServer + 1) ### zuul入门案例 1.引入依赖 ``` org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-netflix-zuul ``` 2.启动类 ``` @SpringBootApplication(scanBasePackages = { "com.ding" }) @EnableDiscoveryClient @EnableZuulProxy public class ZuulApplication { public static void main(String[] args) { SpringApplication.run(ZuulApplication.class,args); } } ``` 3.配置文件 ``` zuul: routes: cloud-member: path: /member/** serviceId: cloud-member ``` 这样的话我们使用/member/开头的url都会转发到cloud-member服务 cloud-member项目的配置文件指定名字这样注册到eureka就能被发现 ``` spring: application: name: cloud-member ``` 我们访问 http://zuu-ip:zuul-port/member/list就能转发到cloud-member项目的list接口 ### zuul常见配置 #### 1.路由配置规则 ``` zuul: routes: cloud-member: path: /member/** serviceId: cloud-member ``` 上例中我们这样配置,还有比较简单的配置 ``` zuul: routes: cloud-member: /member/** 默认映射到cloud-member zuul: routes: cloud-member: zuul: routes: cloud-member: path: /member/** url: http://cloud-member-ip:cloud-member-port ``` 配置多个项目 ``` zuul: routes: cloud-order: path: /order/** serviceId: cloud-order cloud-member: path: /member/** serviceId: cloud-member ``` #### 路由本地转发 有时候我们需要zuul项目中转发到zuul本身的接口 ``` @RequestMapping("/callBack") @ResponseBody public String callBack(String code,String state) throws Exception{ System.out.println("code:::::"+code); return "dddd"; } ``` 配置文件 ``` zuul: routes: cloud-zuul: path: /zuul/** url: forward:/callback ``` 这样的话我们访问zuul接口的时候都会跳转到callback处理 #### 通配符 ![](/upload/微信图片_20200318172153.png) #### 路由重定向ip变化 我们经常会这样客户通过zuul访问认证服务,认证服务认证成功后重定向到欢迎页面,这个时候发现浏览器的地址变成了认证服务的ip,这样认证服务就暴露了。我们可以通过配置`add-host-header`来处理这样的问题 ``` zuul: add-host-header: true routes: cloud-order: path: /order/** serviceId: cloud-order ``` #### 重试机制 由于各种各样的原因我们可能会出现请求偶然失败,这时候我们就可能需要使用ribbon的重试功能,zuul默认结合ribbon ``` zuul: retryable: true #开启重试 ribbon: MaxAutoRetries: 1 #同一个服务重试次数(不算第一次) MaxAutoRetriesServer: 1 #切换相同服务数量 spring: cloud: loadbalancer: retry: enabled: true #默认就是开启的 ``` 根据上面配置,当第一次请求失败的时候,会就同一个服务重试1次(MaxAutoRetries),如果还是失败就切换到另一服务重试(MaxAutoRetriesServer决定切换次数) 上面配置一共会调用4次(原本主机1次,失败重试,切换主机调用一次 失败重试1次) ribbon 总超时时间(conn_time+read_time)*(MaxAutoRetries + 1) * (MaxAutoRetriesServer + 1) > 重试要保证接口的幂等性 #### 路由前缀 ``` zuul: add-host-header: true prefix: /pre #前缀 routes: cloud-member: path: /member/** serviceId: cloud-member ``` 我们访问 http://zuu-ip:zuul-port/pre/member/list就能转发到cloud-member项目的list接口 #### 敏感头问题 我们这样将认证信息放在请求header中,cookie就是其中一种,或者将信息加密放在Authorization中,就会暴露给下层服务,zuul提供了配置`sensitiveHeaders`来切断zuul与下层服务的交互 sensitiveHeaders会过滤客户端附带的headers 例如:sensitiveHeaders: X-ABC 如果客户端在发请求是带了X-ABC,那么X-ABC不会传递给下游服务 ignoredHeaders会过滤服务之间通信附带的headers 例如:ignoredHeaders: X-ABC 如果客户端在发请求是带了X-ABC,那么X-ABC依然会传递给下游服务。但是如果下游服务再转发就会被过滤 还有一种情况就是客户端带了X-ABC,在ZUUL的Filter中又addZuulRequestHeader("X-ABC", "new"), 那么客户端的X-ABC将会被覆盖,此时不需要sensitiveHeaders。如果设置了sensitiveHeaders: X-ABC,那么Filter中设置的X-ABC依然不会被过滤。 ZUUL服务都是可以接受到敏感信息的。至于传不传给后台服务就看配置了。 ``` zuul: add-host-header: true routes: #cloud-order: #path: /order/** #serviceId: cloud-order cloud-member: path: /member/** sensitiveHeaders: Cookie,Set-Cookie,Authorization serviceId: cloud-member ``` #### 服务/接口屏蔽 有时候我们不需要将某些服务或者接口暴露出去我们可以通过配置来实现屏蔽 ``` zuul: ignored-services: cloud-order #屏蔽服务 ignored-patterns: /**/div/** #屏蔽接口 add-host-header: true routes: #cloud-order: #path: /order/** #serviceId: cloud-order cloud-member: path: /member/** sensitiveHeaders: Cookie,Set-Cookie,Authorization serviceId: cloud-member ``` #### 饥饿加载 zuul内部是默认是ribbon调用远程服务的,由于ribbon的原因,第一次经过zuul调用回去注册中心查询服务注册表,初始化ribbon负载均衡信息,这是一种懒加载策略,但是这个过程会耗时比较久,所以我们可以设置启动zuul的时候就去加载。 ```yaml zuul: ribbon: eager-load: enable: true ``` #### 使用 OKHttp 替换 HttpClient 在Java平台上,Java 标准库提供了 HttpURLConnection 类来支持 HTTP 通讯。不过 HttpURLConnection 本身的 API 不够友好,所提供的功能也有限。大部分 Java 程序都选择使用 Apache 的开源项目 HttpClient 作为 HTTP 客户端。Apache HttpClient 库的功能强大,使用率也很高,基本上是 Java 平台中事实上的标准 HTTP 客户端。 zuul默认是使用:Apache HttpClient,但是HttpClient由于难于扩展等原因,慢慢的被弃用了。我们可以使用okhttp来替换 okhttp优点: 1.对同一个主机发出的所有请求都可以共享相同的套接字连接。(对HTTP/2 、spdy支持,对同一个host的请求进行合并) 2.OkHttp 会使用连接池来复用连接以提高效率。 3.OkHttp 提供了对 GZIP 的默认支持来降低传输内容的大小。 4.OkHttp 也提供了对 HTTP 响应的缓存机制,可以避免不必要的网络请求。 5.当网络出现问题时,OkHttp 会自动重试一个主机的多个 IP 地址。 1.引入依赖 ```xml com.squareup.okhttp3 okhttp ``` 2.修改配置文件 ### Ribbon 配置 ```yaml ribbon: httpclient: # 关闭 httpclient 支持 enabled: false okhttp: # 开启 okhttp 支持 enabled: true ``` ### zuul拦截器 zuul本质上是一系列拦截器构成的责任链。 zuul拦截器之前是不能直接通信的,zuul提供RequestContext来实现拦截器之前共享状态,通信,本质上来说RequestContext是底层使用了ThreadLocal下面会有实例。 #### 拦截器类型 - pre filters 在zuul路由带下级服务之前执行,一般用来鉴权、限流 - routing filters zuul路由动作的执行者,是发送构建和发送http请求的地方 - post filters 源服务返回结果或者异常信息发生后执行的,可以对返回结果加工处理 - error filters 在生命周期内如果执行出现异常,则会进入这个类型,这里可以做全局异常处理 ![](/upload/微信图片_20200318182516.png) 从上图可以看出 pre或者routing类型执行报错post类型的也会执行 #### 拦截器执行顺序 相同类型的拦截器,数字越低,优先级也高 使用filterOrder()来定义,后面会有例子。 不同类型的拦截器,优先级高到低 pre > routing > post **官网推荐的一些拦截器的执行顺序定义** 当我们需要做限流的时候,可以在pre拦截器类型做,并且优先级要最高,比鉴权都要高,可以使用 ```java @Override public int filterOrder() { return FilterConstants.SERVLET_DETECTION_FILTER_ORDER -1 ; } ``` 如果是普通pre类型拦截器order可以使用 ```java @Override public int filterOrder() { return FilterConstants.PRE_DECORATION_FILTER_ORDER -1 ; } ``` 如果是post类型的拦截器order可以使用 ```java @Override public int filterOrder() { return FilterConstants.SEND_RESPONSE_FILTER_ORDER -1 ; } ``` #### 拦截器生命周期 ![](/upload/微信图片_20200318182527.png) #### 自定义拦截器实例 1.继承ZuulFilter类 2.实现shouldFilter、filterOrder、filterType、run方法 - shouldFilter:表示当前拦截器执不执行,可以作为开关,返回true执行,false不执行 - filterOrder:返回数字,越小优先级越高 - filterType:指定拦截器的类型 pre、route、post、error - run:具体的逻辑,业务处理 ```yaml @Component public class FirstFilter extends ZuulFilter{ /** * 表示当前的Filter执行不执行 * 返回true 执行run方法 false则不执行 */ @Override public boolean shouldFilter() { return true; } /** * 定义同类型的Filter的执行顺序 * 值越小 越早执行 */ @Override public int filterOrder() { return 0; } /** * 表示具体的Filter类型 */ @Override public String filterType() { return "pre"; } /** * 具体的Filter逻辑 */ @Override public Object run() throws ZuulException { System.out.println("FirstFilter run....................pre................."); //RequestContext使用这个来共享状态 RequestContext rc = RequestContext.getCurrentContext(); HttpServletRequest req = rc.getRequest(); String orderNo = req.getParameter("orderNo"); if(null == orderNo || "".equals(orderNo)){ rc.setResponseBody("{\"message\":\"order is not null\"}"); //禁止路由到下游服务 rc.setSendZuulResponse(false); //false表示不执行同类型的 下面的Filter 但是post的还会执行 rc.set("isExecFilter",false); } rc.set("isExecFilter",true); return null; } } ``` ```yaml @Component public class SecondFilter extends ZuulFilter{ /** * 表示当前的Filter执行不执行 * 返回true 执行run方法 false则不执行 */ @Override public boolean shouldFilter() { RequestContext rc = RequestContext.getCurrentContext(); System.out.println("SecondFilter======================="+rc.get("isExecFilter")); return (boolean)rc.get("isExecFilter"); } /** * 定义同类型的Filter的执行顺序 * 值越小 越早执行 */ @Override public int filterOrder() { // TODO Auto-generated method stub return 1; } /** * 表示具体的Filter类型 */ @Override public String filterType() { return "pre"; } /** * 具体的Filter逻辑 */ @Override public Object run() throws ZuulException { System.out.println("SecondFilter run ..............pre............"); return null; } } ``` ### zuul高可用 我们知道网关是一切请求的入口,如果网关挂了,所有的请求都挂了。所以我们要保证网关的高可用。 ![](/upload/微信图片_20200320101018.png) 如果我们使用了zuul作为网关的话,实现高可用是很简单的,这里我分为两种: 一种是服务之间(eureka的客户端) 我们可以将多个zuul注册到注册中心eureka中,那么service A,B,C也注册到注册中心,Zuul客户端会自动从Eureka Server中查询Zuul Server的列表,并使用Ribbon负责均衡地请求Zuul集群。 另个一种,是用户手机电脑等不是eureka的客户端,即没有注册到注册中心 这种我们多个zuul,不知道要访问那个zuul我们可以使用nginx来对zuul进行负载均衡,nginx在使用keeplived来实现高可用。 当我们使用nginx作为负载的时候,nginx跟zuul是没有感知功能的,当zuul挂掉的时候,nginx是不会将挂掉zuul踢出去的,这样子就会出现分配到这个zuul的请求失败。 ![](/upload/微信图片_20200320183357.png) 所以我们可以结合nginx+lua+zuul来完美处理这个问题 ![](/upload/微信图片_20200320183640.png) 可以写lua脚本定时去注册中心获取 up状态的zuul服务集群进行负载均衡 参考 https://www.ctolib.com/SpringCloud-nginx-zuul-dynamic-lb.html ### zuul限流 网关是一切请求的入口,我们可以在网关使用pre拦截器,对请求进行限流。并且优先级要最高,比鉴权都要高,可以使用来定义order ```java @Override public int filterOrder() { return FilterConstants.SERVLET_DETECTION_FILTER_ORDER -1 ; } ``` 我们可以使用这个开源的工具对api进行限流 https://www.cnblogs.com/lihaoyang/p/12127768.html https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit > 注意:我们不能将所有的限流都放在网关上来做,网关一般微服务外的请求进行限流,而服务之间一般不会经过网关,都是服务间之间调用,所以这个时候网关的限流没用,服务之间一般使用熔断。 网关主要为服务器硬件设备的并发处理能力做限流。细粒度的限流还是交给专门的熔断限流微服务去处理,这样利于各微服务之间的解构和各团队的协同开发。 ### zuul动态路由 #### 动态路由原理 zuul在启动的时候会将配置文件中的映射规则加载到内存中,但是当我们需要修改映射规则,我们就需要重启zuul网关,让其生效。那么有没有一种方法来实现不重启zuul服务,直接让配置文件实时生效----这就是动态路由 要实现动态路由有两种方案 一、使用配置中心spring-clould-config。二、将配置信息放在mysql中,手动触发更新或者等待心跳租约触发。通常都是使用配置中心。 现在我们来了解一下zuul实现动态路由的原理、 ![](/upload/微信图片_20200320115720.png) zuul提供了SimpleRouteLocator这个类可以用来加载配置文件的路由和刷新路由。从下图我们可以看出doRefresh()用来刷新路由,如果实现RefreshableRouteLocator接口重写refresh()并调用doRefresh()可以实现动态路由,locateRoutes()方法只会从配置文件加载静态的配置,如要动态路由子类要重写这个方法。 SimpleRouteLocator 提供了静态路由加载,没有动态刷新的功能 RefreshableRouteLocator 提供了动态刷新功能。实现refresh(),并调用doRefresh(),从而调用locateRoutes()来重新加载路由 DiscoveryClientRouteLocator zuul提供的,继承SimpleRouteLocator并实现RefreshableRouteLocator接口,能加载配置文件路由,并动态感知注册中心是不是新增注册服务,实现注册中心的动态路由。 所以我们要动态路由必须重新继承SimpleRouteLocator并实现RefreshableRouteLocator接口。 ![](/upload/微信图片_20200320134901.png) zuul是使用事件刷新机制的。 ![](/upload/微信图片_20200320135632.png) zuul会注册一个监听器,当接收到特定的事件HeartbeatEvent、RoutesRefreshedEvent等就会将 this.zuulHandlerMapping.setDirty(true);设置为脏数据,这样映射路由转发的时候会判断是不是脏数据是的话,就会重新加载一次路由配置。 所以我们要手动触发路由更新的时候我们可以自己一个controller调用service,service用来发布事件。 或者等待心跳租约触发。 #### 基于mysql动态路由 0.表结构定义参考zuul的ZuulRoute类 ![](/upload/微信图片_20200320142340.png) 1.定义一个路由加载 ```java public class DynamicZuulRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator { @Autowired private ZuulProperties properties; @Autowired private IZuulRouteService zuulRouteService; public DynamicZuulRouteLocator(String servletPath, ZuulProperties properties) { super(servletPath, properties); this.properties = properties; } //实现RefreshableRouteLocator的refresh从而能实现动态路由 @Override public void refresh() { doRefresh(); } //重写locateRoutes方法从数据库加载路由配置 @Override protected Map locateRoutes() { LinkedHashMap routesMap = new LinkedHashMap<>(); routesMap.putAll(super.locateRoutes());//配置文件的静态路由 routesMap.putAll(zuulRouteService.getZuulRoutes());//从数据库加载 LinkedHashMap values = new LinkedHashMap<>(); routesMap.forEach((key, value) -> {//前缀处理 String path = key; if (!path.startsWith("/")) { path = "/" + path; } if (StringUtils.hasText(this.properties.getPrefix())) { path = this.properties.getPrefix() + path; if (!path.startsWith("/")) { path = "/" + path; } } values.put(path, value); }); return values; } } ``` 2.将我们定义的路由加载器交给spring管理 ```java @Configuration public class DynamicZuulConfig { @Autowired private ZuulProperties zuulProperties; @Autowired private ServerProperties serverProperties; @Bean public DynamicZuulRouteLocator routeLocator() { DynamicZuulRouteLocator routeLocator = new DynamicZuulRouteLocator( serverProperties.getServlet().getServletPrefix(), zuulProperties); return routeLocator; } } ``` 以上这样就可以实现动态路由了(心跳租约触发) zuul提供了CompositeRouteLocator,这个类可以整合多个RouteLocator,在项目启动的时候会将RouteLocator类型的bean注入CompositeRouteLocator,CompositeRouteLocator会调用getRoutes()、getMatchingRoute()、refresh() 时都会逐一调用每个RouteLocator相应的方法。 最后我们还有一个步骤就是要手动触发,我们通过手动发布事件来触发 ```java @Service public class RefreshRouteService { @Autowired ApplicationEventPublisher publisher; @Autowired RouteLocator routeLocator; public void refreshRoute() { RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator); publisher.publishEvent(routesRefreshedEvent); } } @RestController public class RefreshController { @Autowired RefreshRouteService refreshRouteService; @GetMapping("/refreshRoute") public String refresh() { refreshRouteService.refreshRoute(); return "refresh success"; } } ``` 参考 https://blog.51cto.com/4925054/2136267 https://www.jianshu.com/p/5e5197e00d65 ### zuul鉴权 我们结合Spring Security OAuth2 1.token可以使用和JWT 2.也可以保存在数据库或redis OAuth2是一种协议我们简单介绍一下 OAuth2有4个角色 1. 资源所有者:指的是用户, 2.认证、授权服务器:用于发放访问令牌给客户端 cloud-auth 3.资源服务器:资源服务器存放受保护资源,要访问这些资源,需要获得访问令牌 这里可以是cloud-order、cloud-member 4.客户端:客户端代表请求资源服务器资源的第三方程序,这里是cloud-zuul 一般来说我们可能有多个资源服务器(cloud-order、cloud-member里面的接口就是我们的资源),一个统一认证授权服务器。 >客户端需要对配置资源服务器放行 需要在HttpSecurity放行资源服务器和授权服务器 资源服务器需要对自己的接口权限校验 需要在HttpSecurity配置 工作流程 >下面流程如果出现401的情况,可以切换一下get或post分别试一下。或者检查参数的完整性,缺少参数也会401 1.客户端cloud-zuul 将自己注册到认证服务器cloud-auth,一般会有client_id和client_secret,这里使用了password模式,和 refresh_token ``` INSERT INTO `token`.`oauth_client_details` (`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('cloud-zuul', 'cloud-order', '123456', '1234561', 'password,refresh_token', NULL, '123456', '300', '7200', NULL, NULL); ``` 2.用户将账号密码给zuul,zuul拿着账号密码发一个请求给auth申请token(使用password),auth返回token给zuul,zuul写入cookie,如果是支持refresh_token还会返回 refresh_token(不能给用户,保存在zuul中数据库,redis中) ``` post http://localhost:10090/auth/oauth/token?username=admin88&password=123456&grant_type=password&scope=1234561&client_id=cloud-zuul&client_secret=123456 get不支持 { "access_token": "bf6ee0a0-6680-4055-a86a-60b193cc460d", "token_type": "bearer", "refresh_token": "39b836bb-2c76-4e95-81f6-1c566752f157", "expires_in": 299, "scope": "1234561" } ``` 3.之后用户就可以拿token来请求资源服务器了 ``` 1。直接访问 资源服务器 get http://localhost:10092/orderInfo/getMemberByOrderNo?orderNo=123456 没有带token的情况 { "error": "unauthorized", "error_description": "Full authentication is required to access this resource" } 在header中放入token key:Authorization value:bearer 3b78f74e-f87e-4c58-807c-6f54890b9748 注意有个空格 成功访问到资源服务器 2.通过zuul网关去访问资源服务器 get http://localhost:10086/order/orderInfo/getMemberByOrderNo?orderNo=123456 在zuul的WebSecurityConfig中要对资源服务器放行 @Override protected void configure(HttpSecurity http) throws Exception { //错误写法 放行不了会报401 不知道为什么放行不了 不知道为什么不放行会401,只能放行让资源服务器校验toekn //http //.authorizeRequests().anyRequest() .authenticated() //.antMatchers("/login/**","/callBack/**","/dynamicZuul/**","/callBack/login/**","/order/**") .permitAll() //.and() .csrf() .disable(); //正确写法,不知道为什么上面不行 http .authorizeRequests() .antMatchers("/callBack/**", "/order11/**", "/about").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") .anyRequest().authenticated(); } 即使这样做了还不行。还是会401,因为敏感头的问题 zuul: sensitiveHeaders: 这样就可以访问 ``` 4.上面说的token都是指access_token:一般过期时间较短(试了默认12小时),refresh_token的过期时间较长 当access_token过期了,用户就要重新登录授权。。一般来说我们为了安全会将access_token设置过期时间较短,这样的话用户就要经常重新登录授权比较麻烦,所以我们可以使用refresh_token进行刷新access_token。 ``` post http://localhost:10090/auth/oauth/token?grant_type=refresh_token&refresh_token=39b836bb-2c76-4e95-81f6-1c566752f157&client_id=cloud-zuul&client_secret=123456 注意带上client_id=cloud-zuul&client_secret=123456不然会401 https://blog.csdn.net/u012040869/article/details/80140515 auth会报错 refresh_token UserDetailsService is required @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //endpoints.tokenStore(jwtTokenStore()); //endpoints.tokenEnhancer(jwtTokenConverter()); endpoints.tokenStore(new JdbcTokenStore(dataSource)); endpoints.authenticationManager(authenticationManager); //refresh_token UserDetailsService is required https://www.jianshu.com/p/ea5ccaddaef7 endpoints.userDetailsService(baseUserDetailService); } ``` #### 退出 ``` @Autowired private ConsumerTokenServices consumerTokenServices; /** *注销access_token,注销access_token的同时refresh_token也会被注销掉 * @param access_token * @return */ @RequestMapping(value = "/member/exit",method=RequestMethod.DELETE) @ResponseBody public Result revokeToken(String access_token) { if (access_token!=null && !"".equals(access_token) && consumerTokenServices.revokeToken(access_token)) { return Result.ok("成功注销"); } else { return Result.error("注销失败"); } } delete http://localhost:10090/auth/member/exit?access_token=cc6312ca-f3a8-4711-bd95-6dc0adb1e3aa 注意 退出登录是写在授权服务器中,所以授权服务器也是资源服务器。需要对访问的接口放行 授权服务器的HttpSecurity配置 @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/member/**").permitAll().and().csrf().disable(); } ``` demo git地址 git@github.com:348786639/cloud-parent.git cloud-parent项目中的拦截器优先于 oauth2 拦截器执行 参考 https://www.jianshu.com/p/68f22f9a00ee 很赞哦! (3) 上一篇:MySQL优化看这篇就对了 下一篇:spring cloud服务间调用之feign 目录 点击排行 Elasticsearch6.3.2之x-pack redis哨兵 2019-07-09 22:05 Redis+Twemproxy+HAProxy+Keepalived 2019-07-12 17:20 GC优化策略和相关实践案例 2019-10-10 10:54 JVM垃圾回收器 2019-10-10 10:23 标签云 Java Spring MVC Mybatis Ansible Elasticsearch Redis Hive Docker Kubernetes RocketMQ Jenkins Nginx 友情链接 郑晓博客 佛布朗斯基 凉风有信 MarkHoo's Blog 冰洛博客 南实博客 Rui | 丁D Java研发工程师 生活可以用「没办法」三个字概括。但别人的没办法是「腿长,没办法」、「长得好看,没办法」、「有才华,没办法」。而你的没办法,是真的没办法。 请作者喝咖啡