在前面十六章我们完成了注册中心、链路中心、权限架构生产者、权限架构消费者的集成开发工作,本章将开始重点讲解我们的路由网关的实现,由于我们的微服务内部是无权限的,因此我们的微服务内部是不对外暴露端口的,所有的请求全部通过路由网关来进行请求的,因此在本章我们的路由网关将实现路由分发以及权限过滤的功能。
直接在我们的工程中创建路由网关的modules如下所示:
接着在我们的api-gateway项目中创建如下的包结构:
接着打开我们的pom.xml引入以下的MAVEN依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gateway</groupId>
<artifactId>api-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>api-gateway</name>
<description>路由网关</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.base</groupId>
<artifactId>model</artifactId>
<version>[0.0.1-SNAPSHOT,)</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
<!-- 引入json的依赖 classifier必须要加这个是json的jdk的依赖-->
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
<version>RELEASE</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Edgware.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
接着配置我们的路由网关成为鉴权中心的消费者,以及使得我们的路由网关成为真正的路由网关,我们需要做以下两个步骤的配置,首先打开我们的主入口类ApiGatewayApplication.java,增加以下注解:
package com.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy
@EnableFeignClients
@EnableDiscoveryClient
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
接着在resource底下新建application-prod-5100.properties配置文件,文件内容如下:
spring.application.name=api-gateway
server.port=5100
# 链路数据收集并发送地址
spring.zipkin.base-url=http://127.0.0.1:9100
# 当前应用收集信息百分比
spring.sleuth.sampler.percentage=0.1
zuul.routes.v1/rbac.path=/v1/rbac/**
zuul.routes.v1/rbac.serviceId=rbac-consumer
# 实现指定的路由的cookie信息的传递
zuul.routes.v1/rbac.sensitiveHeaders=
# 通过浏览器开发工具查看登录以及登录之后的请求详情, 可以发现, 引起问题的大致原因是由于SpringSecurity或Shiro在登录完成之后,通过重定向的方式跳转到登录后的页
#面,此时登录后的请求结果状态码为302, 请求响应头信息中的 Location指向了具体的服务实例地址, 而请求头信息中的Host也指向 了具体的服务实例 IP地址和端口。 所以, 该
#问题的根本原因在于Spring Cloud Zuul在路由请求时,并没有将最初的Host信息设置正确。那么如何解决 这个问题呢?
#能够使得网关在进行路由转发前为请求设置Host头信息,以标识最初的服务端请求地址。 具体配置方式如下:
zuul.add-host-header=true
# 注册中心地址
eureka.client.serviceUrl.defaultZone=http://fjhyll:[email protected]:2100/eureka/
zuul.SendErrorFilter.post.disable=true
# 设置通信的超时时间
ribbon.SocketTimeout=250
# 因此在消费者的重试时间加起来的总和超过的话就直接连接超时
# 设置连接的超时时间
ribbon.ReadTimeout=50000
#断路器的超时时间,断路器的超时时间需要大于ribbon的超时时间,不然不会触发重试。
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000
# 开启GZIP的压缩功能以减少HTTP通信的消耗。
feign.compression.request.enabled=true;
feign.compression.response.enabled=true;
# 以下的请求的类型且请求数据的大小超过2048的将为会压缩传输。
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
# 该参数用来开启重试机制,它默认是关闭的。
spring.cloud.loadbalancer.retry.enabled=true
# 请求连接的超时时间。
AUTHENTICATION-SERVICE.ribbon.ConnectTimeout=250
# 请求处理的超时时间,该超时时间的影响层级大于全局的超时时间,设置了该时间那么,如果调用生产端的时候超过1秒那么就直接调用重试规则,因此若重试次数和切换次数都是为1那么,响应的时间不超过4秒
AUTHENTICATION-SERVICE.ribbon.ReadTimeout=3000
# 对所有操作请求都进行重试。
AUTHENTICATION-SERVICE.ribbon.OkToRetryOnAllOperations=true
# 以下重试实现响应EUREKA-PRODUCER的最大次数是 :(1 + MaxAutoRetries)* (1 + MaxAutoRetriesNextServer)
# 假设 MaxAutoRetries = 2 ,MaxAutoRetriesNextServer = 4 ,那么最大的重试次数为15次
# 切换实例的重试次数。
AUTHENTICATION-SERVICE.ribbon.MaxAutoRetriesNextServer=1
# 对当前实例的重试次数。
AUTHENTICATION-SERVICE.ribbon.MaxAutoRetries=1
feign.hystrix.enabled=true
接着引入我们的鉴权中心的生产者服务,我们直接在我们的service包底下创建AuthenticationService.java文件内容如下:
package com.gateway.service;
import com.base.entity.Identify;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.Map;
/*
* 类描述:
* @auther linzf
* @create 2018/1/24 0024
*/
@FeignClient(value="AUTHENTICATION-SERVICE")
public interface AuthenticationService {
/**
* 功能描述:调用生产者端的轨迹处理方法
* @param identify
*/
@RequestMapping(value = "/identify" ,method = RequestMethod.POST)
Map<String,Object> identify(@RequestBody Identify identify);
}
接着我们编写路由网关的过滤器,改过滤器主要实现的功能是拦截所有的客户端请求,并对相应的请求做鉴权处理以后来进行业务的放行,因此我们在filter包底下创建AccessFilter.java文件,内容如下:
package com.gateway.filter;
import com.base.entity.Identify;
import com.base.util.ip.IPUtil;
import com.gateway.service.AuthenticationService;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/*
* 类描述:
* @auther linzf
* @create 2017/12/22 0022
*/
public class AccessFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(AccessFilter.class);
@Autowired
private AuthenticationService authenticationService;
/**
* filterType: 过滤器的类型, 它决定过滤器在请求的哪个生命周期中执行。 这里定义为pre, 代表会在请求被路由之前执行。
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* filterOrder: 过滤器的执行顺序。 当请求在一个阶段中存在多个过滤器时, 需要根据该方法返回的值来依次执行。
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* shouldFilter: 判断该过滤器是否需要被执行。 这里我们直接返回了true, 因此该过滤器对所有请求都会生效。 实际运用中我们可以利用该函数来指定过滤器的有效范围。
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
*
* 这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求, 不对其进行路由, 然后通过 ctx.setResponseStatusCode
*(401)设置了其返回的错误码, 当然也可以进 一步优化我们的返回, 比如,通过ctx.se七ResponseBody(body)对返回的body内容进行编辑等。
* @return
*/
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response= ctx.getResponse();
// 设置允许跨域访问Access-Control-Allow-Origin设置的为当前dinner工程的IP+端口
response.setHeader("Access-Control-Allow-Headers", "Authentication");
response.setHeader("Access-Control-Allow-Methods","POST,GET,OPTIONS,DELETE");
response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
log.info("send {} request to{}", request.getMethod () ,request.getRequestURL().toString()+"--"+ request.getContentType());
Object accessToken = request.getParameter("token");
if(accessToken == null) {
log.warn("access token is empty");
ctx.setSendZuulResponse(false);
// 401错误表示需要登陆才可以
ctx.setResponseStatusCode(401);
//为了被error过滤器捕获
ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
ctx.set("error.exception",new RuntimeException("AccessToken不允许为空!"));
}
Map<String,Object> result = authenticationService.identify(new Identify((String)accessToken, IPUtil.getIpAddress(request)));
log.info("鉴权中心鉴定结果是:", result.get("msg"));
if((boolean)result.get("result")==false){
ctx.setSendZuulResponse(false);
// 401错误表示需要登陆才可以
ctx.setResponseStatusCode(401);
//为了被error过滤器捕获
ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
ctx.set("error.exception",new RuntimeException((String)result.get("msg")));
}
return null;
}
}
在我们的客户端向我们发送请求的时候,总会有部分请求在通过路由网关调用相应的微服务的时候失败,因此我们需要在路由网关做相应的错误处理,因此我们在controller、entity、util包底下分别构建了以下的实体类:
entity包底下创建了ErrorException.java内容如下:
package com.gateway.entity;
/*
* 类描述:错误信息实体
* @auther linzf
* @create 2018/1/2 0002
*/
public class ErrorException {
// 报错的类
private String exceptionClass;
// 错误的原因
private String exceptionMessage;
public String getExceptionClass() {
return exceptionClass;
}
public void setExceptionClass(String exceptionClass) {
this.exceptionClass = exceptionClass;
}
public String getExceptionMessage() {
return exceptionMessage;
}
public void setExceptionMessage(String exceptionMessage) {
this.exceptionMessage = exceptionMessage;
}
}
util包底下创建了CombineException.java内容如下:
package com.gateway.util;
import com.gateway.entity.ErrorException;
import net.sf.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
/*
* 类描述:封装返回的错误信息工具类
* @auther linzf
* @create 2018/1/2 0002
*/
public class CombineException {
/**
* 功能描述:获取错误的消息
* @param throwable
* @return
*/
public static JSONObject getErrorException(Throwable throwable){
ByteArrayOutputStream baos = new ByteArrayOutputStream();
throwable.printStackTrace(new PrintStream(baos));
String exception = baos.toString();
List<ErrorException> exceptionList = recursionException(exception);
JSONObject jobj = new JSONObject();
for(ErrorException errorException:exceptionList){
if(errorException.getExceptionClass().indexOf("com.netflix.client.ClientException")!=-1){
jobj.put("errorCode","500");
jobj.put("info",errorException.getExceptionMessage());
jobj.put("msg","服务器维护中!");
return jobj;
}else if(errorException.getExceptionClass().indexOf("java.util.concurrent.TimeoutException")!=-1){
jobj.put("errorCode","409");
jobj.put("info",errorException.getExceptionMessage());
jobj.put("msg","服务器连接超时!");
return jobj;
}
}
if(exceptionList.size()>0){
jobj.put("errorCode","400");
jobj.put("info",exceptionList.get(0).getExceptionMessage());
jobj.put("msg","服务器响应发生错误!");
}
jobj.put("errorCode","400");
jobj.put("msg","服务器响应发生错误!");
return jobj;
}
/**
* 功能描述:递归调用获取错误信息的集合
* @param exception
* @return
*/
private static List<ErrorException> recursionException(String exception){
List<ErrorException> exceptionList = new ArrayList<ErrorException>();
int start = exception.indexOf("Caused by:");
if(start!=-1){
int end = exception.substring(start).indexOf("\r\n\t");
String exceptionInfo = exception.substring(start,start+end);
String [] arr = exceptionInfo.split(":");
if(arr!=null&&arr.length>=3){
ErrorException errorException = new ErrorException();
errorException.setExceptionClass(arr[1]);
errorException.setExceptionMessage(arr[2]);
exceptionList.add(errorException);
}else if(arr!=null&&arr.length==2){
ErrorException errorException = new ErrorException();
errorException.setExceptionClass(arr[1]);
errorException.setExceptionMessage("");
exceptionList.add(errorException);
}
if(!exception.substring(start+end).equals("")){
exceptionList.addAll(recursionException(exception.substring(start+end)));
}
}
return exceptionList;
}
/**
* 功能描述:实现获取错误信息
* @param throwable
*/
public static List<ErrorException> initException(Throwable throwable){
ByteArrayOutputStream baos = new ByteArrayOutputStream();
throwable.printStackTrace(new PrintStream(baos));
String exception = baos.toString();
return recursionException(exception);
}
}
controller包底下创建ErrorHandlerController.java内容如下:
package com.gateway.controller;
import com.gateway.util.CombineException;
import com.netflix.zuul.context.RequestContext;
import org.springframework.boot.autoconfigure.web.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/*
* 类描述:
* @auther linzf
* @create 2017/12/26 0026
*/
@RestController
public class ErrorHandlerController implements ErrorController {
@Override
public String getErrorPath() {
return "/error";
}
@RequestMapping("/error")
public String error(HttpServletRequest request) {
RequestContext ctx = RequestContext.getCurrentContext();
return CombineException.getErrorException(ctx.getThrowable()).toString();
}
}
最后我们需要在主入口类做出以下的修改,将我们前面编写的filter过滤器引入:
package com.gateway;
import com.base.util.redis.RedisCache;
import com.gateway.filter.AccessFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableZuulProxy
@EnableFeignClients
@EnableDiscoveryClient
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
@Bean
public AccessFilter accessFilter() {
return new AccessFilter();
}
}
到此我们就完成了路由网关的全部开发工作,我们这时就可以开启我们的注册中心、链路中心、权限架构生产者、权限架构消费者、路由网关,以及开启我们的Advanced REST client工具做如下的测试:
这时候大家会看到我们的返回服务器给予到我们的返回结果,这是因为我们的路由网关在此处开启了权限验证,为了验证我们的正常服务,我们在此处关闭我们的权限验证的环节,修改后的AccessFilter.java代码如下:
package com.gateway.filter;
import com.base.entity.Identify;
import com.base.util.ip.IPUtil;
import com.gateway.service.AuthenticationService;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/*
* 类描述:
* @auther linzf
* @create 2017/12/22 0022
*/
public class AccessFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(AccessFilter.class);
@Autowired
private AuthenticationService authenticationService;
/**
* filterType: 过滤器的类型, 它决定过滤器在请求的哪个生命周期中执行。 这里定义为pre, 代表会在请求被路由之前执行。
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* filterOrder: 过滤器的执行顺序。 当请求在一个阶段中存在多个过滤器时, 需要根据该方法返回的值来依次执行。
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* shouldFilter: 判断该过滤器是否需要被执行。 这里我们直接返回了true, 因此该过滤器对所有请求都会生效。 实际运用中我们可以利用该函数来指定过滤器的有效范围。
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
*
* 这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求, 不对其进行路由, 然后通过 ctx.setResponseStatusCode
*(401)设置了其返回的错误码, 当然也可以进 一步优化我们的返回, 比如,通过ctx.se七ResponseBody(body)对返回的body内容进行编辑等。
* @return
*/
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response= ctx.getResponse();
// 设置允许跨域访问Access-Control-Allow-Origin设置的为当前dinner工程的IP+端口
response.setHeader("Access-Control-Allow-Headers", "Authentication");
response.setHeader("Access-Control-Allow-Methods","POST,GET,OPTIONS,DELETE");
response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
log.info("send {} request to{}", request.getMethod () ,request.getRequestURL().toString()+"--"+ request.getContentType());
/*
Object accessToken = request.getParameter("token");
if(accessToken == null) {
log.warn("access token is empty");
ctx.setSendZuulResponse(false);
// 401错误表示需要登陆才可以
ctx.setResponseStatusCode(401);
//为了被error过滤器捕获
ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
ctx.set("error.exception",new RuntimeException("AccessToken不允许为空!"));
throw new RuntimeException("AccessToken不允许为空!");
}
Map<String,Object> result = authenticationService.identify(new Identify((String)accessToken, IPUtil.getIpAddress(request)));
log.info("鉴权中心鉴定结果是:", result.get("msg"));
if((boolean)result.get("result")==false){
ctx.setSendZuulResponse(false);
// 401错误表示需要登陆才可以
ctx.setResponseStatusCode(401);
//为了被error过滤器捕获
ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
ctx.set("error.exception",new RuntimeException((String)result.get("msg")));
throw new RuntimeException((String)result.get("msg"));
}
*/
return null;
}
}
接着再做如上的测试结果如下:
到此为止我们已经完成了路由网关的集成开发工作,大家以后可以再此基础上做更大的扩展,此处就不再累述了,在下一章我们将讲解如何将我们过往的工程改造成我们的spring cloud的微服务。
到此为止的GitHub项目地址:https://github.com/185594-5-27/spring-cloud-rbac/tree/master-gateway
上一篇文章地址:基于springboot+redis+bootstrap+mysql开发一套属于自己的分布式springcloud云权限架构(十五)【权限架构消费者(完整实现)】
下一篇文章地址:基于springboot+redis+bootstrap+mysql开发一套属于自己的分布式springcloud云权限架构(十七)【权限架构系统(基础框架搭建)】
QQ交流群:578746866