Spring Cloud Zuul-“网关“解决方案组件

分享组件版本为1.x版本,2.x版本‘码’同志自己去瞅瞅~

–Zuul-‘网关组件’ 在动态路由、监控、弹性、服务治理以及安全方面举足轻重的作用。
– Zuul 它的诞生场景,是因在微服务框架拆分多个服务后,多个时候为了完成一个业务逻辑,需要在不同主机和端口
–上面调取接口,于是一个面向服务治理、服务编排的组件Zuul出现了。
– Zuul是从设备和网站请求/访问,后端服务都必须经过网关,它为内部服务提供了可配置的对外URL到
– 服务的的映射关系,基于JVM的后端路由器。
– Zuul组件具备:

–1-认证与鉴权 -2-压力控制 -3-金丝雀测试 -4-动态路由 -5-负载消减 -6-静态响应处理 -7-主动流量管理
‘其底层基于Servlert,本质组件是一系列Filter(过滤器)所构成的责任链,并且Zuul逻辑引擎与Filter(过滤器)
可用基于JVM的语言编写,比如:Groovy(一种基于JVM(Java虚拟机)的敏捷开发语言)’.

-上代码欧

<!-- zuul-server 服务所有代码-->
------------------------------------------------------------------------------------
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
		</dependency>
------------------------------------------------------------------------------------
spring:
  application:
    name: zuul-server
server:
  port: 5555 '这里的设置的Zuul组件的端口为5555,是指定注册中心端口'
eureka:
  client:
    serviceUrl:
      defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8888}/eureka/
  instance:
    prefer-ip-address: true
'下面👇五行配置是指,将所有/client开头的URL映射到Client-R这个服务中去,'
'这样在请求的时候就可以不用请求实际的服务,而转请求这个5555端口的Zuul服务组件,'
'/client即一次服务路由的规则'
zuul:
  routes:
    client-a:
      path: /client/**/
      serviceId: client-R
------------------------------------------------------------------------------------
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class ZuulServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulServerApplication.class, args);
    }
}
------------------------------------------------------------------------------------
<!-- 创建普通下游服务 client-R 服务所有代码-->
'该服务用来测试Zuul‘ Server路由的路由功能'
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
------------------------------------------------------------------------------------
server:
  port: 7070
spring:
  application:
    name: client-R
eureka:
  client:
    serviceUrl:
      defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8888}/eureka/
  instance:
    prefer-ip-address: true
------------------------------------------------------------------------------------
@SpringBootApplication
@EnableDiscoveryClient
public class ClientAApplication {
    public static void main(String[] args) {
        SpringApplication.run(ClientAApplication.class, args);
    }
}
------------------------------------------------------------------------------------
'创建一个测试接口'
@RestController
public class TestController {
	@GetMapping("/add")
	public Integer add(Integer a, Integer b){
		return a + b;
	}
}
------------------------------------------------------------------------------------
"--然后,启动各服务,使用postman分两次调取接口,调用运行结果可知,在调取"
"--http://localhost:5555/client/add?a=100&b=400接口的时候,"
"--实际上是调取http://localhost:7070/add?a=100&b=400接口,"
"--因为Zuul配置文件指定了路由规则,当Zuul Server发起请求的时候,它会去Eureka注册中心拉取服务列表,"
"--如果发现有指定的路由映射规则,就会按照规则路由到相应的服务接口上去。"
------------------------------------------------------------------------------------
---------------------
# ** Zuul-路由配置: **
---------------------
========
'(1)单实例ServiceId映射:'
========
--'前例子中:'
zuul:
  routes:
    client-a:
      path: /client/**/
      serviceId: client-R
--'是一个从/client/**到client-R服务的一个映射规则,下面在把它简化:'
######## 简化配置 ########
zuul:
  routes:
    client-R: /client/**/
---------------------------
--'更简化的默认配置:'
######## 默认配置 ########
zuul:
  routes:
    client-R: 
--'这个使用Zuul会给Client-R 服务添加一个默认的映射规则/client-r/**'
--------------------------- 
========
'(2)单实例URL映射:'
========
--'除了路由到服务外,还能路由到物理地址,将ServiceId替换为url即可:'
######## 使用url替代serviceId路由 ########
zuul:
  routes:
    client-r:
      path: /client/**/
      url: http://localhost:7070 #client-r的地址
--------------------------- 
========
'(3)多实例路由:'
========
--'在默认情况下,Zull会使用Eureka中集成的基本负载均衡功能,如果想要使用Ribbon的负载均衡,'
--'就需要指定一个ServceId,此操作需要禁止Ribbon使用Eureka,负载均衡策略的配置如下:'
##############  脱离eureka让zuul结合ribbon实现路由负载均衡  ################# 
zuul:
  routes:
    ribbon-route:
      path: /ribbon/**/
      serviceId: ribbon-route
ribbon:
  eureka:
    enabled: false  #禁止Ribbon使用Eureka
ribbon-route:
  ribbon:
    NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #Ribbon LB Strategy
    listOfServers: localhost:7070,localhost:7071 #client services for Ribbon LB
--------------------------- 
========
'(4)forward(转发)本地跳转:'
========
--'有时候我们在Zuul中会做一些逻辑处理,在网关中写好一个接口:'
@RestController
public class TestController {
	@GetMapping("/client")
	public String add(Integer a, Integer b){
		return "本地跳转:" + (a + b);
	}
}
--'如果希望在访问/client接口的时候跳转到这个方法上来处理,就需要用到ZUul的本地跳转,配置如下:'
########################## foward跳转本地url  ##########################
zuul:
  routes:
    client-r:
      path: /client/**/
      url: forward:/client
--------------------------- 
========
'(5)相同路径的加载规则:'
========
--'有一种特殊的情况,为一个映射路径指定多个ServiceID,怎么加载服务顺序/即生效;'
########################## 映射覆盖情况 ##########################
zuul:
  routes:
    client-b:
      path: /client/**/
      serviceId: client-a
    client-a:
      path: /client/**/
      serviceId: client-r
--'测试发现,它总是会路由到yml配置文件后面的那个服务,这里是client-r服务,也就是末尾那个服务。'
--'在yml解释器工作的时候,如果用一个映射路径对应多个服务,按照加载顺序,最末加载的映射规则会把之前覆盖掉。'
> 路由通配符:
规则释义示例
/**匹配任意数量的路径与字符/client/add, /client/mul, /client/r
/*匹配任意数量的字符client/add, /client/mul, /client/r
/?匹配单个字符/client/r
---------------------
# ** Zuul-功能配置: **
---------------------
========
'(1)路由前缀:'
========
########################## 前缀 ##########################
zuul:
  prefix: /pre       #前缀(统一的代理前缀)
  routes:
    client-r: /client/**/
    stripPrefix: false #该false设置状态,是把代理路径前缀从URL请求中移除的关闭功能
========
'(2)服务屏蔽与路径屏蔽:'
========
‘有时候为了避免某些服务或者路径的入侵,可以将它们屏蔽掉 如下:’
########################## 服务忽略、路径忽略、前缀 ##########################
zuul:
  ignored-services: client-a    #忽略的服务,防服务侵入
  ignored-patterns: /**/div/**  #忽略的接口,屏蔽接口
  prefix: /pre                  #前缀
  routes:
    client-r: /client/**‘
========
'(3)敏感头信息:'
========
########################## 敏感头设置 ##########################
zuul:
  prefix: /pre
  routes:
    client-r:
      path: /client/**/
      sensitiveHeaders: Cookie,Set-Cookie,Authorization
      serviceId: client-r
      -------------------------------------------------------------------------在构建系统的时候,使用HTTP的header传值是十分方便的,协议的一些认证信息默认也在Header,
      比如:Cookie 或者习惯把基本认证通过BASE64加密后放在Authorization里面。与外部系统打交道时,
      会出现这些信息泄露,所有在zuul网关中配置指定敏感头,切断它和下层服务之间的交互。如上👆’
========
'(4)重定向问题:'
========
zuul:
  add-host-header: true         #重定向header问题
  routes:
    client-r: /client/**
    -----------------------------------------------------------------------
    ‘客户端通过Zuul请求认证服务,认证成功后重定向到一个欢迎页,但不指定上面设置,重定向的欢迎页面
    Host 变成来认证的Host,而不是zuul的Host,直接造成暴露来认证服务的地址,所有需要设置上面ture状态.’
========
'(5)重试机制:'
========
     ########################## 重试机制 ##########################
zuul: 
  retryable: true #开启重试
ribbon:
  MaxAutoRetries: 1 #同一个服务重试的次数(除去首次)‘Zuul可以配合Ribbon(默认集成)来做重试’
  MaxAutoRetriesNextServer: 1  #切换相同服务数量

–Spring Cloud Zuul 工作原理:
–1-Zuul的核心逻辑是由一系列紧密配合工作的Filter(过滤器)来实现的,
–2-Zuul中的Filter(过滤器)责任链是它的核心功能。

–Filter(过滤器)的类型:Filter的类型决定了此Filter在Filter链中的执行顺序(可能是路由动作发生前、中 后及异常时)。
–Filter(过滤器)的执行顺序:同一类型的Filter可以通过filterOrder()方法来设定执行顺序。
—Filter(过滤器)的执行条件:Filter运行所需要的标准或条件。
–Filter(过滤器)的执行效果:符合某个Filter执行条件,产生的执行效果。

–Zuul 内部提供来一个动态读取、编译和运行这些Filter(过滤器)的机制。
– Filter(过滤器)之间不直接通信,在请求线程中会通过RequestContext来共享状态,
–它的内部是用ThreadLocal实现的,当然也可以在Filter之间使用ThreadLocal来收集自己需要的状态或数据。
– Zuul中不同类型Filter的执行逻辑核心在:com.netflix.zuul.http.ZuulServlet 类中它继承了HttpServlet类。

> Zuul请求生命周期:

在这里插入图片描述

Zuul请求生命周期
.pre(格式化文本,前置/之前) Filters(过滤器)
.route(v.按特定路线发送) Filters(过滤器)

–‘解说下这张图:
–1.从OriginServer(源服务)发出请求,’.pre(格式化文本,前置/之前) Filters(每个过滤器)’/路由过滤器,’.route(v.按特定路线发送)Filters’
–2.其中’.post Filter’请求过滤器抛错之后进入.‘error Filte’r,
–3.然后在进入post Filter是有失偏颇的,
–4.在’.post filterc’抛错分为两种情况:
–4-1:1)在’.post Filter’抛错之前,’.pre’、’.route Filter’没有抛错,此时会进入ZuulException的逻辑,
------打印堆栈信息,然后再返回states=500的Error信息。
–4-2:2)在post Filter抛错之前,’.pre’、’.route Filter’已有抛错,此时不会在打印堆栈信息,直接返回status=500的error信息。


—也就说,整个责任链流程终点不只是 ‘.post Filter’,还可能是’.error Filter’。

-- Zuul中一共有四种不同的生命周期的Filter(过滤器),分别是:
	--'.pre:'在Zuul按照规则路由到下级服务之前执行。如果需要对请求进行预处理,
			 比如:鉴权、限流等,都应考虑💭在此类Filter实现。
	--'.route:'这类Filter是Zuul路由动作的执行者,是Apache HttpClient或Netflix Ribbon构建和发送
			   原始HTTP请求的地方,目前已支持OkHttp'
 	--'.post:'这类Filter是在源服务返回结果或者异常信息发生后执行的,如果需要对返回信息做一额处理,
 			  则在此类Filter进行处理。
 	--'.error:'在整个生命周期内如果发生异常,则会进入 error Filter 可做全局异常处理。
 	
	--在Filter之间,通过com.netflix.zuul.context.RequestContext类来进行通信,
	---内部采用ThreadLocal保存每个请求的一些信息,包括请求路由、错误信息、HttpServletRequest、
HttpServletResponse,这使得一些操作是十分可靠的,它还扩展来ConcurrentHashMap,目的是为来在处理过程中
	---保存任何形式的信息。
> Zuul原生Filter(过滤器):

Zuul Server 如果使用@Enable-ZuulProxy注解搭配Spring Boot Actuator(执行器),1会多两个管控端点。
1)/routes: 返回当前Zuul Server中的所有已生成的映射规则,加上/details可查看明细。
2) /filters:返回当前Zuul Server中所有已注册生效的Filter。

> Zuul内置Filter(过滤器)与生命周期的组合流程图:

在这里插入图片描述

> Zuul内置Filter(过滤器)信息表:

在这里插入图片描述
在这里插入图片描述

--'RequestContext.getThrowable()不为空,则转发到error.path配置的路径。'
--'以上是使用@EnableZuulProxy注解后安装的Filter,如果使用@EnableZuulServer将缺少'
'PreDecorationFilter、RibbonRoutingFilter、SimpleHostRouthingFilter.'
-- '原始的也许不好用呢,可以采取替代实现的方式,覆盖掉其原生代码,也可以采用禁用策略,语法如下:'
'zuul.<SimpleClassName>.<filterType>.disable=true.'
--'比如要禁用SendErrorFilter,在配置文件中添加zuul.SendErrorFilter.error.disable=true即可.'
> Zuul网关类应用的逻辑处理模型:

–在Zuul的Filter链体系中,可以把一系列业务逻辑细分,然后封装到一个个紧密结合的Filter中,
—设置处理顺序,组成一组Filter链。
–1--实现自定义Filter(过滤器)
‘在Zuul中实现自定义Filter,继承ZuulFilter类即可,ZuulFilte是一个抽象类,需要实现它的方法:’
---- .String filterType():使用返回值设定Filter类型,可以设置为.pre、.route.、.post 、.error类型。
----.int filterOrder(): 使用返回值设定Filter执行次序。
----.boolean shouIdFilter():使用返回值设置该Filter是否执行,可以作为开关来使用。
----.Object run(): Filter里面的核心执行逻辑,业务处理在此编写.

public class FirstPreFilter extends ZuulFilter {
	@Override
	public String filterType() {return PRE_TYPE;}
	@Override
	public int filterOrder() {return 0;}
	@Override
	public boolean shouldFilter() {return true;}
	@Override
	public Object run() throws ZuulException {
		System.out.println("这是第一个自定义Zuul Filter!");
		return null;}}
"上面👆的代码,在应用启动时,会把自实现的Filter加载进内存,还需要将它注入Spring Bean容器,"
--"需要在启动类中:"
---------------------------------------------------------------------------
    @Bean
    public FirstPreFilter firstPreFilter(){eturn new FirstPreFilter();}
 "启动所有组件程序,访问:http://localhost:5555/client/nul?a=100&b=300,即可在控制台看到打印特定信息内容."
    ---------------------------------------------------------------------------
========
'(2)业务处理实战:'
========
--"针对于Zuul网关,官方预留类API,以使得使用者更好的实现自定义业务处理,加入Zuul的逻辑流程。"
--"模拟一个业务需求:使用SecondPreFilter(第二)来验证是否传入A参数,使用ThirdPreFilter(第三)来验证是否传入B参数"
--"最后在PostFilter里边统一处理返回内容。"
> 业务处理实战模拟流程图:

在这里插入图片描述
== ==砍代码 == ==

public class SecondPreFilter extends ZuulFilter {
	@Override
	public String filterType() {return PRE_TYPE;}
	@Override
	public int filterOrder() {return 2;}
	@Override
	public boolean shouldFilter() {return true;}
	@Override
	public Object run() throws ZuulException {
		System.out.println("这是SecondPreFilter!");
		//从RequestContext获取上下文
		RequestContext ctx = RequestContext.getCurrentContext();
		//从上下文获取HttpServletRequest
		HttpServletRequest request = ctx.getRequest();
		//从request尝试获取a参数值
		String a = request.getParameter("a");
		//如果a参数值为空则进入此逻辑
		if (null == a) {
			//对该请求禁止路由,也就是禁止访问下游服务
			ctx.setSendZuulResponse(false);
			//设定responseBody供PostFilter使用
			ctx.setResponseBody("{\"status\":500,\"message\":\"a参数为空!\"}");
			//logic-is-success保存于上下文,作为同类型下游Filter的执行开关
			ctx.set("logic-is-success", false);
			//到这里此Filter逻辑结束
			return null;
		}
		//设置避免报空
		ctx.set("logic-is-success", true);return null;}}
--"SecondPreFilter的执行次序设为2,主要验证请求是否传入a参数。若没有传,则通过"
---"ctx.setSendZuulResponse(false)来禁止route Filter路由此次请求;通过"
----"ctx.setResponseBody("{\"status\":500,\"message\":\"a参数为空!\"}")"
-----"来定制返回结果;使用ctx.set("logic-is-success", false)来关联ThirdPreFilter,"
------"若是没有传入a参数,ThirdPreFilter也就没有执行的必要了,代码如下:"
public class ThirdPreFilter extends ZuulFilter {
	@Override
	public String filterType() {return PRE_TYPE;}
	@Override
	public int filterOrder() {return 3;}
	@Override
	public boolean shouldFilter() {
		RequestContext ctx = RequestContext.getCurrentContext();
		//从上下文获取logic-is-success值,用于判断此Filter是否执行
		return (boolean)ctx.get("logic-is-success");}
	@Override
	public Object run() throws ZuulException {
		System.out.println("这是ThirdPreFilter!");
		//从RequestContext获取上下文
		RequestContext ctx = RequestContext.getCurrentContext();
		//从上下文获取HttpServletRequest
		HttpServletRequest request = ctx.getRequest();
		//从request尝试获取b参数值
		String b = request.getParameter("b");
		//如果b参数值为空则进入此逻辑
		if (null == b) {
			//对该请求禁止路由,也就是禁止访问下游服务
			ctx.setSendZuulResponse(false);
			//设定responseBody供PostFilter使用
			ctx.setResponseBody("{\"status\":500,\"message\":\"b参数为空!\"}");
			//logic-is-success保存于上下文,作为同类型下游Filter的执行开关,
			//假定后续还有自定义Filter当设置此值
			ctx.set("logic-is-success", false);
			//到这里此Filter逻辑结束
			return null;}return null;}}
--"ThirdPreFilter的执行次序设为3,主要验证请求是否传入B参数。若没有传入,则通过"
---"ctx.setSendZuulResponse(false)来禁用route Filter路由此次请求,通过"
----"ctx.setResponseBody({\"status\":500,\"message\":\"b参数为空!\"})"
-----"来定制返回结果;使用ctx.set("logic-is-success", false))"
------"来关联下一级Filter 代码如下:"
public class PostFilter extends ZuulFilter {
	@Override
	public String filterType() {return POST_TYPE;}
	@Override
	public int filterOrder() {return 0;}
	@Override
	public boolean shouldFilter() {return true;}
	@Override
	public Object run() throws ZuulException {
		System.out.println("这是PostFilter!");
		//从RequestContext获取上下文
		RequestContext ctx = RequestContext.getCurrentContext();
		//处理返回中文乱码
		ctx.getResponse().setCharacterEncoding("UTF-8");
		//获取上下文中保存的responseBody
		String responseBody = ctx.getResponseBody();
		//如果responseBody不为空,则说明流程有异常发生
		if (null != responseBody) {
			//设定返回状态码
			ctx.setResponseStatusCode(500);
			//替换响应报文
	        ctx.setResponseBody(responseBody);
		}return null;}}
--"PostFilter 主要是用于检查有无定制ResponseBody,以及设置响应字符集,避免中文乱码,此外还设定来HTTP状态码 "
---------------------------------------------------------------------------------------
-- 最后测试,依次启动工程,调用:http://localhost:5555/client/mul?b=300. 控制台查看结果
--- 调用:http://localhost:5555/client/mul?a=100.控制台查看结果
---调用:http://localhost:5555/client/mul?a=100&b=300.控制台查看结果
> 使用Groovy编写Filter:

Groovy语言是基于JVM的一门动态语言,它结合来Python、Ruby和Smalltalk的许多强大特性,
能无缝引入Java代码与Java库,可以用来作Java的扩展语言来使用,它与Java语法类似。
Zuul中提供Groovy的编译类:com.netflix.zuul.groovy.GroovyCompiler, 结合com.netflix.zuul.groovy.GroovyFileGilter类,使用Groovy来编写自定义Filter, Zuul,它可以不用编译(不用打进工程包),可以放在服务器上任意位置,可以任何时候修改由它 编写的Filter,且修改够还不用重启服务,看来它很实用。

1)为工程添加Groovy依赖

<dependency>  
     <groupId>org.codehaus.groovy</groupId>  
      <artifactId>groovy-all</artifactId>  
      <version>2.5.0-beta-2</version>  
 </dependency>  

2)使用Groovy编写自定义Filter:GroovyFilter.groovy

class GroovyFilter extends ZuulFilter {
	@Override
	public String filterType() {return PRE_TYPE}
	@Override
	public int filterOrder() {return 10}
	@Override
	public boolean shouldFilter() {return true}
	@Override
	public Object run() throws ZuulException {
		HttpServletRequest request = RequestContext.currentContext.request as 
		HttpServletRequest
		Iterator headerIt = request.getHeaderNames().iterator()
		while (headerIt.hasNext()) {
			String name = (String) headerIt.next()
			String value = request.getHeader(name)
			println("header: " + name + ":" + value)}
		println("This is Groovy Filter!")
		return null}}
--'这里把自定义Filter设定为PRE类型的Filter,执行次序为10,shouIDFilter设置为true,'
---"核心逻辑就是遍历请求的Header并打印出来,打印完之后在打印This is Groovy Filter!."

3)注册Groovy Filter:GroovyFilter.groovy

这项目的启动类Main()方法类中,添加Groovy的加载器
...........
    /**
     * Groovy加载方法配置,20秒自动刷新
     */
    @Component
    public static class GroovyRunner implements CommandLineRunner {
        @Override
        public void run(String... args) throws Exception {
            MonitoringHelper.initMocks();
            FilterLoader.getInstance().setCompiler(new GroovyCompiler());
            try {
                FilterFileManager.setFilenameFilter(new GroovyFileFilter());
                FilterFileManager.init(20, "/Users/Administrator/workspace-scbook/
                ch8-1/ch8-1-zuul-server/src/main/java/cn/springcloud/book/groovy");
            } catch (Exception e) {
                throw new RuntimeException(e);}}}
                .......................
 --------------------------------------------------------------------------------
 --"开始测试: 依次启动工程,访问:http://localHost;5555/client/mul?a=100&b=300, 观察到控制台打印结果"
 ---"若不启动服务,将GroovyFilter.groovy文件中的println("This is Groovy Filter!")改为:"
 ---:"println("THis is Groovy Filter!Modify!"),等待大约20秒,重复刚才操作,接着观察控制台结果-"   
 --"的确结果让使用者感受到,不启动服务更新代码,没有用到字节码生成库,就可以实践运行."            
> Spring Cloud Zuul权限集成:
-- 1)Zuul内自定义权限认证Filter(过滤器)
	--'由于Zuul对请求转发全过程的可控性,可以在RequestContext基础上做任何事情,'
	---'设置差一个执行顺序靠前的Filter,可专门用于对请求特定内容做权限认证'
> 基于Zuul Filter的权限模型:

基于Zuul Filter的权限模型

--这种方式的优点是实现灵活度高,可整合已有权限系统,对原始系统微服务化特别友好;
--缺点:是需要开发一套新的逻辑,维护增加成本,而且也会使得调用链路变的错乱。
-- 2)OAuth2.0 + JWT
	--'OAuth2.0是业界对与“授权-认证” 比较成熟的面向资源的授权协议。'
> OAuth认证原理:

OAuth认证原理

--‘整个流程中,用户是资源拥有者,关键在于客户端需要资源拥有者的授权,’
---‘这个过程就相当于键入密码或者是其它第三方登录,触发了这个操作之后,客户端就可以想授权服务器申请Token’
----‘拿到后再携带Token到资源所在服务器拉去相应资源.
'JWT(JSON Web Token)是一种使用JSON格式来规约Token或者Session的协议。'
-'由于传统认证方式免不了会生成一个凭证,这个凭证可以是Token或者Session'
--'保存与服务端或者其它持久化工具中,这样一来,凭证的存取就变得十分麻烦,'
---'JWT的出现打破来这个一瓶颈,实现来客户端Session的愿景。'
+ JWT由三个部分组成:
+ .Header 头部:指定JWT使用的签名算法;
+ .Payload载荷:包含一些儿自定义与非自定义的认证信息;
+ .Signature签名:将头部与载荷使用'.'连接之后,使用头部的签名算法生成签名信息并拼装到末尾.
	+ .'OAuth2.0 + JWT 的意义就在于,使用OAuth2.0协议的思想拉去认证生成Token,'
	+ .'使用JWT瞬时保存这个Token,在客户端与资源端进行对称或非对称加密,'
	+ .'使得这个规约具有定时、定量的授权认证功能,'
	+ .'从而免去Token存储所带来的安全或系统扩展问题。'
Zuul + OAuth2.0 + JWT 实战代码:
-----------------------------
--"在服务注册到注册中心之后,是由zuul来引导各个请求到内部敏感服务的"
--"因此,在Zuul网关层实现与下游服务之间的鉴权至关重要"
--'下面👇 基于OAuth2.0与JWT的思想 在Zuul上做的实践,代码如下:'
-----------------------------
---1)编写Zuul-server
/**
  * Zuul-server 中需要做的就是请求接口时,判断是否登录鉴权,如果未登录,则跳转到auth-server的登录页面		  
  * (这里使用的是Spring Security OAuth 的默认登录页面,也可以重写相关代码定制页面)
  * 登录成功后auth-server颁发Jwt token,zuul-server在访问下游服务时将jwttoken放入header中即可。
  */
  // 1)pom.xml 引入OAuth2与Security的支持
  	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-oauth2</artifactId>
		</dependency>
	</dependencies>
	 -----------
	 // 2)编写核心配置文件bootstrap.yml,提供对引用框架的支持
	 spring:
  application:
    name: zuul-server
server:
  port: 5555
eureka:
  client:
    serviceUrl:
      defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8888}/eureka/
  instance:
    prefer-ip-address: true
    
    --- <!--定义了从/client到client-a服务的路由规则-->
zuul:
  routes:
    client-a:
      path: /client/**/
      serviceId: client-a
      ---
      
 ---     
security:
  basic:
    enabled: false
  oauth2:
    client:
      access-token-uri: http://localhost:7777/uaa/oauth/token #令牌端点
      user-authorization-uri: http://localhost:7777/uaa/oauth/authorize #授权端点
      client-id: zuul_server #OAuth2客户端ID
      client-secret: secret #OAuth2客户端密钥
    resource:
      jwt:
        key-value: springcloud123 #使用对称加密方式,默认算法为HS256
        '当前如果安全性要求更高,应该使用非对称加密方式,生成公钥与私钥放置与服务中.'
 -----------
  // 3)编写核心启动类ZuulServerApplication.java
 @SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
@EnableOAuth2Sso
public class ZuulServerApplication extends WebSecurityConfigurerAdapter{
    public static void main(String[] args) {
        SpringApplication.run(ZuulServerApplication.class, args);
    }   
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
		.authorizeRequests()
		.antMatchers("/login", "/client/**")
		.permitAll()
		.anyRequest()
		.authenticated()
		.and()
		.csrf()
		.disable();
	}
}
 -----------
 -'启动类里,重写WebSecurityConfigurerAdapter适配器的Configure(HttpSecurityhttp)方法,声明需要鉴权的url信息.'
  -----------
  // 4)编写auth-server 服务
  '整个示例的另外一个核心就是auth-server,它作为认证授权中心,会颁发jwt token凭证'
  pom.xml 引入对OAuth2的支持:
  		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-oauth2</artifactId>
		</dependency>
----------------------
 // 5)编写核心配置文件bootstrap.yml:
 spring:
  application:
    name: auth-server
server:
  port: 7777
  servlet: 
    contextPath: /uaa #web基路径
eureka:
  client:
    serviceUrl:
      defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8888}/eureka/
  instance:
    prefer-ip-address: true
   ----------------------
 // 6)编写认证授权服务适配器类OAuthConfiguration.java 代码如下:
 @Configuration
@EnableAuthorizationServer
public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter {
	@Autowired
	private AuthenticationManager authenticationManager;
	@Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
        .inMemory()
        .withClient("zuul_server")
        .secret("secret")
        .scopes("WRIGTH", "read").autoApprove(true)
        .authorities("WRIGTH_READ", "WRIGTH_WRITE")
        .authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code");
    }
	@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws 
    Exception {
        endpoints
        .tokenStore(jwtTokenStore())
        .tokenEnhancer(jwtTokenConverter())
        .authenticationManager(authenticationManager);}
    @Bean
    public TokenStore jwtTokenStore() {return new JwtTokenStore(jwtTokenConverter());}
    @Bean
    protected JwtAccessTokenConverter jwtTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("springcloud123");
        return converter;
    }}
    '这个类主要用于指定客户端ID、密钥,以及权限定义与作用域声明,指定ThkenStroe为JWT,不同于以往将'
    'TokenStore指定为Redis或者是其它持久化工具,主要的不同就是在这里.'
       ----------------------
 // 7)编写启动配置类AuthServerApplication.java 代码如下:
 @SpringBootApplication
@EnableDiscoveryClient
public class AuthServerApplication extends WebSecurityConfigurerAdapter {
	public static void main(String[] args) {
		SpringApplication.run(AuthServerApplication.class, args);
	}
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
        .inMemoryAuthentication()
        .withUser("guest").password("guest").authorities("WRIGTH_READ")
        .and()
        .withUser("admin").password("admin").authorities("WRIGTH_READ", "WRIGTH_WRITE");
    }

    @Bean
    public static NoOpPasswordEncoder passwordEncoder() {
      return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }
}
 -- 声明用户admin具有读写权限,用户guest具有读权限;authenticationManagerBean()方法用于手动注入
 --AuthenticationManager;passwordEncoder()用于声明用户名和密码的加密方式,这个功能在
 spring security5.0之前是没有的,需要注意一下。
      ----------------------
      // 8)编写client-a服务
  --client-a 作为zuul-server的下游服务,需要的功能,能够被注册发现,以及能够按照规则解析jwt token即可
  --编写启动类ClientAApplication.java 如下代码:
  @SpringBootApplication
@EnableDiscoveryClient
@EnableResourceServer
@RestController
public class ClientAApplication extends ResourceServerConfigurerAdapter {
    public static void main(String[] args) {
        SpringApplication.run(ClientAApplication.class, args);
    }
	@RequestMapping("/test")
	public String test(HttpServletRequest request) {
		System.out.println("----------------header----------------");
		Enumeration headerNames = request.getHeaderNames();
		while (headerNames.hasMoreElements()) {
			String key = (String) headerNames.nextElement();
			System.out.println(key + ": " + request.getHeader(key));
		}
		System.out.println("----------------header----------------");
		return "hellooooooooooooooo!";}
	@Override
	public void configure(HttpSecurity http) throws Exception {
		http
		.csrf().disable()
		.authorizeRequests()
		.antMatchers("/**").authenticated()
		.antMatchers(HttpMethod.GET, "/test")
		.hasAuthority("WRIGTH_READ");}
	@Override
	public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
		resources
		.resourceId("WRIGTH")
		.tokenStore(jwtTokenStore());}
	@Bean
	protected JwtAccessTokenConverter jwtTokenConverter() {
		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
		converter.setSigningKey("springcloud123");
		return converter;}
	@Bean
	public TokenStore jwtTokenStore() {
		return new JwtTokenStore(jwtTokenConverter());}}
		---
-- 它配置和auth-server的配置其实相差无几;如 反析jwt token配置,声明接口权限可见范围等。
--这里写了一个/test接口用于返回内容与打印header到控制台。
-------
最后测试,依次启动eureka-server、zuul-server、client-a、auth-server
--使用浏览器访问http://localhost:5555/client/test 查看结果集

– - 下图说明下整个例子的流程,其流程图下面👇----
示例认证流程图

在这里插入图片描述

上图👆,forbidden页面,由于还未登录授权,这时候接口是调不通的。
--使用浏览器访问http://localhost:5555结果如下

在这里插入图片描述

--跳转到auth-server的默认登录页,使用用户名admin与密码amdin登录
--使用浏览器再次访问http://localhost:5555/client/test如下

在这里插入图片描述

'调用接口成功,观察client-a服务控制台打印结果,如下:'

在这里插入图片描述

'其中的authorization就是 jwt token,使用BASE64解码后为:'
{"alg":"HS256","typ":"JWT"}
{"exp":1523734683,"user_name":"admin" "authorities":"[WRIGTH_WRITE,WRIGTH_READ]",
"jti":"fjanfahflajfkajfdksfa",
"client_id":"zuul_server","scope":["WRIGTH","read"]}
'从中可以清晰看到jwt token的头部、载荷、签名信息.'
> Spring Cloud Zuul限流:
>  在Hystrix中,可以通过熔断器来实现,通过对某个阀值来对异常流程进行降级处理,
> 系统的“雪崩之灾”,听说过吧,比如:流量排队、限流、分流等。
> 基于Zuul限流策略,可预防 ‘雪崩之灾
> Zuul 中实现限流的方式,使用定义Filter加上相关限流算法,
> 可能考虑💭到Zuul多节点部署,因为算法的因素,需要一个K/V存储工具
> (使用Redis,并利用Ridis单线程的特性,避免多节点带来的问题)
这里使用到一个开箱即用的工具,spring-cloud-zuul-ratelimit
(http://github.com/marcosbarbero/spring-cloud-zuul-ratelimit)
它是专门针对Zuul编写的限流库,提供多种细颗粒度策略:
--'.user:'认证用户或匿名,针对某个用户粒度进行限流。
--'.origin:'客户机IP,针对请求客户机ip颗粒进行限流。
--'.url:'特定url,针对某个请求url粒度进行限流。
--'.serviceId:'特定服务,针对某个服务id粒度进行限流。
-- 以及多种粒度临时变量存储方式:
--'.IN_MEMEORY:'基于本地内存,底层是COncurrentHashMap。
--'.PEOIS:'Redis的K/V存储。
--'.CONSUL:Consul的K/V存储。'
--'.JPS:Spring Data JPA,基于数据库.'
--'.BUKET4J:一个使用Java编写的基于令牌桶算法的限流库,其有四个模式,JCache、
		Hazelcast、Apache lgnite、Inifinispan,其中后面三种支持异步.'

=码代码来~!=

<!-- 编写 zuul-server 服务-->
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<dependency>
			<groupId>com.marcosbarbero.cloud</groupId>
			<artifactId>spring-cloud-zuul-ratelimit</artifactId>
			<version>2.0.6.RELEASE</version>
		</dependency>
	</dependencies>
-------------------------------------------------------------------
spring:
  application:
    name: zuul-server
server:
  port: 5555
eureka:
  client:
    serviceUrl:
      defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8888}/eureka/
  instance:
    prefer-ip-address: true
zuul:
  routes:
    client-a:
      path: /client/**/
      serviceId: client-a
  ratelimit: 
    key-prefix: springcloud-book #按粒度拆分的临时变量key前缀
    enabled: true #启用开关
    repository: IN_MEMORY #key存储类型,默认是IN_MEMORY本地内存,此外还有多种形式
    behind-proxy: true #表示代理之后
    default-policy: #全局限流策略,可单独细化到服务粒度
      limit: 2 #在一个单位时间窗口的请求数量
      quota: 1 #在一个单位时间窗口的请求时间限制
      refresh-interval: 3 #单位时间窗口
      type: 
        - user #可指定用户粒度
        - origin #可指定客户端地址粒度
        - url #可指定url粒度
 -------------------------------------------------------------------
 ="示例,采用本地内存的存储方式,在3秒的时间窗口内不能超过2次的接口调用。"
 ="需要修改配置文件,定制需要的限流策略。"

=然后,启动各服务测试一下,依次启动eureka-server、zuul-server、 client-a、使用postman调用接口,
--调用接口超限之后,返回自定义的目标信息,
--在时间窗阀值内访问接口,接口返回正确信息,一旦超限,后台会抛出429异常。
> Spring Cloud Zuul动态路由:

– 之前的配置文件中的配置都是静态路由,动态路由,修改完配置文件后,不再重启Zuul应用,实现修改映射规则。
—实现动态路由解放方案,有两种给码同志们说,
-1-第一种:结合 Spring Cloud Config+Bus,动态刷新配置文件。
-2-第二种: 重写Zuul的配置读取方式,采用事件刷新机制,从数据库读取路由映射规则。
–此种方式因为基于数据库,可轻松实现管理页面,灵活度较高。
下面👇先说第二种吧~

--'动态路由实现原理刨析:'
--"基于DB与事件刷新机制热修改Zuul映射规则实战之前,给大家说一下,路由加载以及刷新的原理:"
--"通过四个核心类,掌握构建动态路由的原理,类之间依赖关系如下:"

动态路由的原理类之间依赖关系

1.DiscoveryClientRouteLocator类依赖图👆上面
--'顾名思义,此类是Zuul中对于路由配置信息读取与新节点注册变更的操作类.'
这个类的源码,我就不复制粘贴了,大家自己看源码,我解说用途:
'locateRoutes()方法继承自SimpleRouteLocator类并重写类规则,该方法主要的功能就是将配置文件中的映射'
‘-规则信息包装成LinkedHashMap<String,ZuulRoute>,键String是路径path,值ZuulRoute是配置文件的‘
'封装类,以往所见的配置映射读取进来就是使用ZuulRoute来封装。refresh()实现自RefreshableRouteLocator'
’接口,添加刷新功能必须要实现此方法,doRefresh()方法来自SimpleRouteLocator类,需要自定义路由配置加载器.'
2.SimpleRouteLocator类
--'SimpleRouteLocator是DiscoveryClientRouteLocator的父类,此类基本实现类RouteLocator接口,对读'
’取的配置文件信息做一些基本处理,提供方法doRefresh()与locateRoutes()供子类实现刷新策略与映射规则加载策略‘
--'其中有两个方法都是使用protected修饰,是为了让子类不用维护此类一些成员变量就能够实现刷新或者读取路由'
‘的功能,从注释也能看出,调用doRefresh()方法需要实现RefreshableRoyteLocator;locateRoutes()方法’
'默认是一个静态的映射读取方法,方法签名说的很明白,如果需要动态加载映射,则需要子类重写此方法。'
3.ZuulServerAutoConfiguration类
--'该类主要目的是注册各种过滤器、监听器以及其它功能,Zuul在注册中心新增服务后刷新监听器也是在此注册,底层'
‘采用Spring的ApplicationListener来实现的‘。
'其中由方法onApplicationEvent(ApplicationEvent event),可知,Zuul会接收3种事件ContextRefreshedEvent'
‘RefreshScopeRefreshedEvent,RoutesRefreshedEvent通知去刷新路由映射配置信息,此外心跳续约监视器'
'HeartBeatMonitor也会触发这个动作。'
4.ZuuHanderMapping类
'此类是将本地配置的映射关系映射到远程的过程控制器,与事件刷新相关代码,赶快找出源码中:'
‘找到其中的 dirty‘属性很重要,它是用来控制当前是否需要重新加载映射配置信息的标记,在zuul每次进行路由’
'操作的时候都会检查这个值,如果为true,就会触发配置信息的重新加载,同时再将其回设为false。也由setDirty'
(boolean dirty)可知,启动刷新动作必须要实现RefreshableRouteLocator接口。’
---------------------------------------------------------------------------------------
"--说了,路由映射规则的加载原理以及zuul的事件刷新方式,在构建动态路由的时候,只需要重写SimpleRouteLocator"
'类的locateRoutes()方法,并且实现RefreshableRouteLocator接口的refresh()方法,再在内部调用Simple'
'RouteLocator类的doRefresh()方法,就可以构建起一个由Zuul内部事件触发的自定义动态路由加载器,如果不想'
'使用内部事件触发配置更新操作,改为手动触发,可以重写onApplicationEvent(ApplicationEvent event)'
'方法内部实现方式,事实上手动触发的控制性更好。'
> Spring Cloud Zuul动态路由(基于DB的动态路由):

-写一个demo,这里DB选用Mysql.

<!-- 编写 zuul-server 服务-->
--'简单先选用ORM,简单的Spring、 JDBCTemplate'
-- 引入相关配置
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
	</dependencies>
----------------------------
spring:
  application:
    name: zuul-server
  datasource:
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: 1992115
server:
  port: 5555
eureka:
  client:
    serviceUrl:
      defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8888}/eureka/
  instance:
    prefer-ip-address: true

ribbon:
  eureka:
    enabled: true
 '与之前不同的是加入数据库俩节配置,另外如果需要防止服务侵入,可以将ribbon.eureka.enabled 设置为false'  
---------------------------------

=== 存储映射规则的数据库表设计 如图:= ===
数据库表设计

-- '编写DAO类,从数据库读取路由配置信息,代码如下:'
@Component
public class PropertiesDao {
	@Autowired
	private JdbcTemplate jdbcTemplate;
	private final static String SQL = "SELECT * FROM zuul_route WHERE enabled = TRUE";
	public Map<String, ZuulRoute> getProperties() {
		Map<String, ZuulRoute> routes = new LinkedHashMap<>();
		List<ZuulRouteEntity> list = jdbcTemplate.query(SQL, new BeanPropertyRowMapper
		<>(ZuulRouteEntity.class));
		list.forEach(entity -> {
			if (StringUtils.isEmpty(entity.getPath())) return;
			ZuulRoute zuulRoute = new ZuulRoute();
			BeanUtils.copyProperties(entity, zuulRoute);
			routes.put(zuulRoute.getPath(), zuulRoute);
		});return routes;}}
-- '编写自定义路由配置加载器,代码如下:'
/**
 * 这个类是本次改造的核心类,locateRoutes()方法从数据库加载配置信息,
 * 并结合Zuul内部事件刷新机制,实际上每次心跳续约都会触发路由配置重新加载的操作。
 */
public class DynamicZuulRouteLocator extends SimpleRouteLocator
	 implements RefreshableRouteLocator {
	@Autowired
	private ZuulProperties properties;
	@Autowired
	private PropertiesDao propertiesDao;
	public DynamicZuulRouteLocator(String servletPath, ZuulProperties properties) {
		super(servletPath, properties);this.properties = properties;}
	@Override
	public void refresh() {doRefresh();}
	@Override
	protected Map<String, ZuulRoute> locateRoutes() {
		LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>();
		routesMap.putAll(super.locateRoutes());
		routesMap.putAll(propertiesDao.getProperties());
		LinkedHashMap<String, ZuulRoute> 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;}}
-- '最后是对自定义路由配置加载器的生效操作类,代码如下:'
@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;}}
---------------
-最后,测试依次启动工程 eureka-server、zuul-server、client-a.按照数据库的配置,使用Chrome调用接口。

  1. Spring Boot Actuator可以帮助你监控和管理Spring Boot应用,比如健康检查、审计、统计和HTTP追踪等。所有的这些特性可以通过JMX或者HTTP endpoints来获得。 ↩︎

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值