快速入门全栈 - 14 SpringCloud 服务网关

一、服务网关与Zuul

客户端和服务端直接连接,存在一定的弊端。客户端如果要请求不同的微服务,会增加客户端的复杂性;存在跨域请求时,还需要额外的处理,并且UI端和服务端存在耦合。

服务网关,就是路由转发加上过滤器。路由转发是接收一切外部的请求,然后将其转发到后端的微服务上;过滤器是在服务网关中实现横切功能,例如权限校验、限流以及监控等,都可以通过过滤器完成。

在技术层面上,用户端口首先要经过负载均衡器,之后再经过API网关,然后连接微服务。服务网关和微服务启动时都要注册到注册中心上,当用户请求时直接对网关进行请求,网管会进行服务发现和复杂均衡,之后会经过权限校验、监控、限流等操作,与服务的响应进行聚合,返回给用户。

Zuul是Netflix开源的微服务网关,可以和Eureka、Ribbon、Hystrix配合使用,Spring Cloud对Zuul进行了整合和增强,主要功能是路由转发和过滤器。Zuul的功能有

  • 身份认证与安全:识别每个资源的验证要求、拒绝那些与要求不符的请求
  • 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生产视图
  • 动态路由:动态地将请求路由转达到不同的集群
  • 压力测试:逐渐增加指向集群的流量
  • 负载分配:为每一种负载类型分配对应容量,并启用超出限定值的请求
  • 静态响应处理:在边缘位置直接建立部分响应、从而避免其转发到内部集群
  • 多区域弹性:跨越AWS Region进行请求路由,,旨在实现Elastic Load Balancing使用的多样化,以及让系统的边缘更贴近系统的使用者

二、Zuul环境搭建

首先我们新建一个SpringBoot应用,并在Maven中添加依赖

<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
	<groupId>org.springframework.cloudgroupId>
	<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
	<groupId>org.springframework.cloudgroupId>
	<artifactId>spring-cloud-starter-zuulartifactId>
dependency>

之后我们来配置application.yml

spring:
  application:
    name: zuul-service
server:
  port: 9000
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

最后在启动类Application中添加注解@EnableZuulProxy即可开启Zuul服务

三、Zuul路由

上面的例子可以将Zuul注册到Eureka注册中心,然而我们怎么通过Zuul来配置路由呢?

Zuul路由有两种模式,一种是不依赖服务发现,比如Nginx,另一种方式是通过微服务的以来发现,结合Eureka来进行路由转发,我们要使用的是第二种。我们需要一组zuul.routes..path与zuul.routes..serviceId参数对的方式进行配置

# 自定义路由
zuul:
  routes:
    server-provide:
      path: /server-api/**
      servideId: server-provide
# 开启Eureka支持
ribbon:
  eureka:
    enabled: true

例如

zuul:
  routes:
    api-a:
      path: /api-a/**
	  servideId: server-ribbon
    api-b:
      path: /api-b/**
      servideId: server-feign

这样,以/api-a/开头的请求都会转发给service-ribbon服务,以/api-b/开头的请求都转发给service-feign服务

我们还可以使用URL来进行路由的转发

zuul:
  routes:
    test:
      path: /test/**
	  url: http://localhost:8081

四、Zuul过滤器

在进行路由转发的时候,可能会涉及到一些公共操作,例如权限判断或认证,如果没有过滤器,就需要在每个微服务中单独实现,比较麻烦。因此我们使用过滤器在每次路由之前对请求进行过滤。

快速入门全栈 - 14 SpringCloud 服务网关_第1张图片

过滤器有四个类型:

  • PRE: 可以在请求被路由之前调用,我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等,可实现日志监控、身份认证、黑名单等功能
  • ROUTING:在路由请求时候被调用。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或者Netflix Ribbon请求微服务
  • POST:在routing和error过滤器之后被调用,这种过滤器可用来为响应添加标准HTTP Header、手机统计信息和指标、将响应从微服务发送给客户端,可实现审计、统计等功能
  • ERROR:处理请求时发生错误调用,可实现统一异常处理等功能

Http请求会进入一个pre-filter,之后可以路由到自己的个性化路由器,否则会继续到下一个routing-filter,在之后回到post-filter,最后会将应答返回客户端。如果报错了,会经过error-filters,再到post-filters,最终将应答返回。

要编写一个过滤器,我们需要实现一个抽象类ZuulFilter并实现它的抽象函数,有以下四个

  • String filterType():该函数需要返回一个字符串来代表过滤器的类型,而这个类型就是在HTTP请求过程中定义的各个阶段。在Zuul中默认定义了四种不同生命周期的过滤器类型,有:pre、routing、post、error
  • int filterOrder():通过int值来定义过滤器的执行顺序,数值越小优先级越高
  • boolean shouldFilter():返回一个boolean类型来判断该过滤器是否要执行,我们可以通过此方法来指定过滤器的有效范围
  • Object run():过滤器的具体逻辑,在该函数中,我们可以实现自定义的过滤逻辑,来确定是否要拦截当前的请求,不对其进行后续的路由,或是在请求路由返回结果之后,对处理结果做一些加工等

下面我们新建一个过滤器

@Component
public class PreFilter extends ZuulFilter{
	@Override
	public String filterType(){
		return "pre";
	}

	@Override
	public int filterOrder(){
		return 0;
	}

	@Override
	public boolean shouldFilter(){
		return true;
	}

	@Override
	public Object run() throws ZuulException(){
		System.out.println("PreFilter");
		RequestContext ctx = RequestContext.getCurrentContext();
		ctx.setSendZuulResponse(false);
		ctx.setResponseStatusCode(401);
		ctx.setResponseBody("Not Authenticated");
		return null;
	}
}

需要注意的是,记得要将自定义的过滤器添加@Component注解,添加到Spring容器中。在上面的代码中,过滤器会拦截请求,并返回401码,如果是要继续转发请求,则是下面的代码

@Override
public Object run() throws ZuulException(){
	RequestContext ctx = RequestContext.getCurrentContext();
	ctx.setSendZuulResponse(true);
	ctx.setResponseStatusCode(200);
	return null;
}

这样即可应答成功。而要判断是否要正确应答,只需要根据一个简单的判断即可实现。

下面我们来实现这样的代码,如果有Token,则正确应答,否则因权限问题而转发失败

@Override
public Object run() throws ZuulException(){
	RequestContext ctx = RequestContext.getCurrentContext();
	Strint token = ctx.getRequest().getParameter("token");

	if(token != null){
		ctx.setSendZuulResponse(true);
		ctx.setResponseStatusCode(200);
	}else {
		ctx.setSendZuulResponse(false);
		ctx.setResponseStatusCode(401);
		ctx.setResponseBody("Not Authenticated");
	}
	return null;
}

此时如果我的URL为http://localhost:9000/hello?token=123456即可正常访问,如果URL中没有token,则会因为权限不足而转发失败

五、Zuul熔断器

在前面我们讲到Hystrix提供了异常熔断的方法,Zuul也同样提供了服务降级的方法。在Zuul中我们通过实现FallbackProvider接口即可

@Component
public class MyFallbackProvider implements FallbackProvider{
	@Override
	public String getRoute(){ return "*";}

	@Override
	public ClientHttpResponse fallbackResponse(String route, Throwable cause){
		return new ClientHttpResponse(){
			@Override
			public HttpStatus getStatusCode() throws IOException{
				return HttpStatus.OK;
			}

			@Override
			public int getRawStatusCode() throws IOException{
				return this.getStatusCode().value();
			}

			@Override
			public String getStatusText() throws IOException{
				return this.getStatusCode().getReasonPhrase();
			}

			@Override
			public void close(){}

			@Override
			public InputStream getBody() throws IOException {
				return new ByteArrayInputStream("Service不可用".getBytes());
			}

			@Override
			public HttpHeaders getHeaders(){
				HttpHeaders headers = new HttpHeaders();
				MediaType mt = new MediaType("application", "json", Charset.forName("UTF-8"));
				headers.setContentType(mt);
				return headers;
			}
		};
	}
}

六、Zuul实战

下面我们根据一个实例来练习一下前面Zuul的内容。我们通过一个Token来确认用户是否有权限访问服务,要验证用户是否合法的,且操作是否已经失效了。

要实现这些内容,需要使用JWT。整个的操作流程是:

  1. 用户打开客户端以后,客户端要求用户给予授权
  2. 用户同意给予客户端授权
  3. 客户端使用上一步获得的授权,向认证服务器申请令牌
  4. 认证服务器对客户端进行认证以后,确认无误,同意发放令牌
  5. 客户端使用令牌,向资源服务器申请获取资源
  6. 资源服务器确认令牌无误,同意向客户端开放资源

Json Web Token (JWT)是一个非常轻巧的规范,允许我们使用JWT在用户和服务器之间传递安全可靠的信息。它是基于RFC 7519标准定义的一种可以安全传输和自包含的JSON对象。HTTP的Post方法会用户名和密码,然后再服务器端会生成一个Token,并将Token返回给用户。之后客户端会发送受保护的API请求,并在Authorization Header中携带Token,服务器端会检验JWT是否合法,如果合法会取得相关信息,并返回请求的数据。

JWT有及部分进行组成:Header、Payload和Signature:Header中有声明的加密算法(alg)和声明类型(typ);Payload中包含了JWT的签发者(iss)、JWT面向的用户(sub)、接收JWT的一方(aud)、过期时间(exp)、什么时候签发的(iat)、最开始时间,如果当前时间在其之前,则不被接收(nbf)、最后还有签名Signature,signature = 加密算法(header + “.” + payload,密钥)

要使用JWT我们需要在Maven中配置

<dependency>
	<groupId>com.auth0groupId>
	<artifactId>java-jwtartifactId>
	<version>3.4.1version>
dependency>

下面我们来编写一个JWTUtil类来使用JWT

public class JwtUtil{
	// 过期时间
	private static final long EXPIRE_TIME = 15 * 60 * 1000;
	// 私钥
	private static final String TOKEN_SECRET = "privateKey";

	/**
	 * 生成签名,15分钟过期
	 */
	public static String sign(Long userId){
		try{
			// 设置过期时间
			Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
			// 私钥和加密算法
			Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
			// 设置头部消息
			Map<String, Object> header = new HashMap<>(2);
			header.put("Type","Jwt");
			header.put("alg","HS256");
			// 返回token字符串
			return JWT.create()
				.withHeader(header)
				.withClaim("userId",userId)
				.withExpiresAt(date)
				.sign(algorithm);
		    }catch (Exception e){
				e.printStackTrace();
				return null;
			}
		}
	}

	/**
	 * 检验token是否正确
	 */
	 public static Long verify(String token){
		try{
			Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
			JWTVerifier verifier = JWT.require(algorithm).build();
			DecodedJWT jwt = verifier.verify(token);
			Long userId = jwt.getClaim("userId").asLong();
			return userId;
		} catch(Exception e){
			return 0L;
		}	
	 }
}

之后我们可以新建一个Controller,

@RestController
public class JwtController{
	@RequestMapping("/getToken")
	public Map getToken(){
		// 前面传用户信息
		Long userId = 111L;
		// 返回令牌
		Map token = new HashMap<>();
		String sign = JwtUtil.sign(userId);
		token.put("sign",sign);
		token.put("userId",userId);
		return token;
	}
}

下面我们要实现,在网关的时候验证是否sign是有效的,也就是要在过滤器中编写run方法

@Override
public Object run() throws ZuulException(){
	RequestContext ctx = RequestContext.getCurrentContext();
	Strint token = ctx.getRequest().getParameter("token");
	token = token == null?"":token;
	String userId = ctx.getRequest().getParameter("userId");
	long token_userid = JwtUtil.verify(token);

	if(token_userid == Long.valueof(userId)){
		ctx.setSendZuulResponse(true);
		ctx.setResponseStatusCode(200);
	}else {
		ctx.setSendZuulResponse(false);
		ctx.setResponseStatusCode(401);
		ctx.setResponseBody("Not Authenticated");
	}
	return null;
}

我和几位大佬建立了一个微信公众号,欢迎关注后查看更多技术干货文章
欢迎加入交流群QQ1107710098

你可能感兴趣的:(快速入门全栈)