客户端和服务端直接连接,存在一定的弊端。客户端如果要请求不同的微服务,会增加客户端的复杂性;存在跨域请求时,还需要额外的处理,并且UI端和服务端存在耦合。
服务网关,就是路由转发加上过滤器。路由转发是接收一切外部的请求,然后将其转发到后端的微服务上;过滤器是在服务网关中实现横切功能,例如权限校验、限流以及监控等,都可以通过过滤器完成。
在技术层面上,用户端口首先要经过负载均衡器,之后再经过API网关,然后连接微服务。服务网关和微服务启动时都要注册到注册中心上,当用户请求时直接对网关进行请求,网管会进行服务发现和复杂均衡,之后会经过权限校验、监控、限流等操作,与服务的响应进行聚合,返回给用户。
Zuul是Netflix开源的微服务网关,可以和Eureka、Ribbon、Hystrix配合使用,Spring Cloud对Zuul进行了整合和增强,主要功能是路由转发和过滤器。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注册到Eureka注册中心,然而我们怎么通过Zuul来配置路由呢?
Zuul路由有两种模式,一种是不依赖服务发现,比如Nginx,另一种方式是通过微服务的以来发现,结合Eureka来进行路由转发,我们要使用的是第二种。我们需要一组zuul.routes.
# 自定义路由
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
在进行路由转发的时候,可能会涉及到一些公共操作,例如权限判断或认证,如果没有过滤器,就需要在每个微服务中单独实现,比较麻烦。因此我们使用过滤器在每次路由之前对请求进行过滤。
过滤器有四个类型:
Http请求会进入一个pre-filter,之后可以路由到自己的个性化路由器,否则会继续到下一个routing-filter,在之后回到post-filter,最后会将应答返回客户端。如果报错了,会经过error-filters,再到post-filters,最终将应答返回。
要编写一个过滤器,我们需要实现一个抽象类ZuulFilter并实现它的抽象函数,有以下四个
下面我们新建一个过滤器
@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,则会因为权限不足而转发失败
在前面我们讲到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的内容。我们通过一个Token来确认用户是否有权限访问服务,要验证用户是否合法的,且操作是否已经失效了。
要实现这些内容,需要使用JWT。整个的操作流程是:
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;
}