基于springboot+redis+bootstrap+mysql开发一套属于自己的分布式springcloud云权限架构(十六)【路由网关】

65 篇文章 5 订阅
21 篇文章 0 订阅

      在前面十六章我们完成了注册中心、链路中心、权限架构生产者、权限架构消费者的集成开发工作,本章将开始重点讲解我们的路由网关的实现,由于我们的微服务内部是无权限的,因此我们的微服务内部是不对外暴露端口的,所有的请求全部通过路由网关来进行请求的,因此在本章我们的路由网关将实现路由分发以及权限过滤的功能。

       直接在我们的工程中创建路由网关的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:hyll-2.0@127.0.0.1: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


  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笨_鸟_不_会_飞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值