服务网关
一、服务网关
服务网关又称为API网关,API 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:
客户端会多次请求不同的微服务,增加了客户端的复杂性。
存在跨域请求,在一定场景下处理相对复杂。
认证复杂,每个服务都需要独立认证。
难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。
某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。
以上这些问题可以借助 API 网关解决。API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 API 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控、限流,熔断等等可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性,典型的架构图如图所示:
使用 API 网关后的优点如下:
易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
解耦前后端,减少了客户端与各个微服务之间的交互次数,屏蔽后端微服务的改动,保证系统的稳定性。
提供高级功能,负载均衡,统一鉴权,协议转换,监控监测等一系列功能。
缓存,持久化管理
协议转换
常见的微服务网关根据使用特性大致被分成流量网关和业务网关。两种网关分别有不同关注点,下面是总结的两种网关类型特性:
二、网关的选择
Spring Cloud Zuul:本身基于
Netflix
开源的微服务网关,可以和Eureka
,Ribbon
,Hystrix
等组件配合使用。Kong : 基于OpenResty的 API 网关服务和网关服务管理层。
Spring Cloud Gateway:是由
spring
官方基于Spring5.0
,Spring Boot2.0
,Project Reactor
等技术开发的网关,提供了一个构建在Spring Ecosystem
之上的API网关,旨在提供一种简单而有效的途径来发送API,并向他们提供交叉关注点,例如:安全性,监控/指标和弹性。目的是为了替换Spring Cloud Netfilx Zuul
的。Zuul2:Zuul的升级版本,提供更加强大的功能,同时将zuul中多线程同步修改为异步高并发(集成Netty)。
GIA-GATEWAY基于Spring Cloud Gateway封装的实现。
三、Zuul
zuul的特性:
认证和安全 识别每个需要认证的资源,拒绝不符合要求的请求。
性能监测 在服务边界追踪并统计数据,提供精确的生产视图。
动态路由 根据需要将请求动态路由到后端集群。
压力测试 逐渐增加对集群的流量以了解其性能。
负载卸载 预先为每种类型的请求分配容量,当请求超过容量时自动丢弃。
静态资源处理 直接在边界返回某些响应
搭建Zuul
zuul的本质也是一个微服务
1、构建springboot工程,引入依赖
<dependencies>
<!-- zuul server support-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- com.sccba.util support 同时引入了其他依赖-->
<dependency>
<groupId>com.sccba</groupId>
<artifactId>util</artifactId>
<version>${project.parent.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>复制
util解决其他的引入,主要是微服务依赖
<dependencies>
<!-- spring boot starter support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- spring boot starter test support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- web support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- hot deploy support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- eureka client support
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency> -->
<!-- nacos 替换eureka -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- feign support openfeign默认支持ribbon 和 hystrix,然而不引入会报错 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- ribbon support -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<!-- hystrix support
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency> -->
<!-- spring cloud alibaba sentinal 替换hystrix
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel</artifactId>
</dependency> -->
<!-- annotation support User中使用-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
</dependency>
<!-- lombok support -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- yml parser support-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
<!-- apollo support -->
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>1.4.0</version>
</dependency>
<!-- redis support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 解决不引入报错 RedisConnectionFactory 找不到-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- 共享session support -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- swagger2 support -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- swagger2 ui support -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!-- security support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
</project>复制
2、注解@EnableZuulProxy
/**
* @author BaZinGa
* @date
*/
@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class, SecurityAutoConfiguration.class})
@EnableFeignClients // 开启Feign
@ServletComponentScan
public class ZuulApplication extends SpringBootServletInitializer{
public static void main(String[] args) {
ApplicationContext app = SpringApplication.run(ZuulApplication.class, args);
ApplicationContextUtil acu = new ApplicationContextUtil();
acu.setApplicationContext(app);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(ZuulApplication.class);
}
@Bean
public static ObjectMapper objectMapper() {
return new ObjectMapper().disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
}
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer () {
return new PropertySourcesPlaceholderConfigurer();
}
}复制
3、配置
spring.application.name = ${app.id}-zuul
spring.cache.redis.key-prefix = zuul
logging.path = /logs/zuul/mslogs
# 解决请求在传递过程中session变化的问题
zuul.sensitive-headers = *
zuul.ignored-headers = Access-Control-Allow-Origin,Access-Control-Allow-Credentials,Access-Control-Allow-Methods
# 忽略指定服务,逗号隔开,*代表所有服务,只路由指定的微服务
zuul.ignored-services = *
# 超时配置,注意此配置的时间必须大于Ribbon的超时时间
zuul.host.connect-timeout-millis = 600000
zuul.host.socket-timeout-millis = 600000
# zuul路由规则配置 budget、login表示路由别名
zuul.routes.budget.path = /budgetApi/**
zuul.routes.budget.serviceId = budgetservice
zuul.routes.login.path = /loginApi/**
zuul.routes.login.serviceId = loginservice
zuul.routes.orghr.path = /orghrApi/**
zuul.routes.orghr.serviceId = orghrservice
zuul.routes.origin.path = /originApi/**
zuul.routes.origin.serviceId = originservice
# 指定path和url,无法负载均衡
zuul.routes.zuul.path = /sso/**
zuul.routes.zuul.url = http://localhost
# 配置Prometheus的tag
management.metrics.tags.application = zuul
# 服务发现名称
spring.cloud.nacos.discovery.service = zuul
# 日志采集显示应用名
spring.zipkin.service.name = zuul
# 自定义采样规则
sentinel.flowRules =复制
配置和原理分析
1、其他路由写法
zuul:
routes:
rest-demo: /rest/**
# 或者
zuul:
routes:
route-name: #路由别名,无其他意义,与例1效果一致
service-id: rest-demo
path: /rest/**复制
上述写法也是将rest开头的请求路由到rest-demo服务
2、即指定path和URL,又保留Zuul的Hystrix、Ribbon特性
zuul:
routes:
route-name: #路由别名,无其他意义,与例1效果一致
service-id: rest-demo
path: /rest/**
ribbon:
eureka:
enable: false #为Ribbon禁用Eureka
rest-demo:
ribbon:
listOfServers: localhost:9000,localhost:9001复制
3、路由的正则匹配
@Bean
public PatternServiceRouteMapper serviceRouteMapper(){
/**
* A RegExp Pattern that extract needed information from a service ID. Ex :
* "(?<name>.*)-(?<version>v.*$)"
*/
//private Pattern servicePattern;
/**
* A RegExp that refer to named groups define in servicePattern. Ex :
* "${version}/${name}"
*/
//private String routePattern;
return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)", "${version}/${name}");
}复制
4、路由前缀
zuul:
prefix: /api
strip-prefix: true
routes:
rest-demo: /rest/**复制
5、忽略某些微服务的某些路径
zuul:
ignoredPatterns: /**/user/* #忽略所有包含/user/的地址请求
routes:
route-demo:
service-Id: rest-demo
path: /rest/**复制
6、Filter工作原理
6.1ZuulFilter
Zuul是围绕一系列Filter展开的,这些Filter在整个HTTP请求过程中执行一连串的操作。
Zuul Filter有以下几个特征:
Type:用以表示路由过程中的阶段(内置包含PRE、ROUTING、POST和ERROR)
Execution Order:表示相同Type的Filter的执行顺序
Criteria:执行条件
Action:执行体
6.2FilterType
PRE Filter:在请求路由到目标之前执行。一般用于请求认证、负载均衡和日志记录。
ROUTING Filter:处理目标请求。这里使用Apache HttpClient或Netflix Ribbon构造对目标的HTTP请求。
POST Filter:在目标请求返回后执行。一般会在此步骤添加响应头、收集统计和性能数据等。
ERROR Filter:整个流程某块出错时执行。
除了上述默认的四种Filter类型外,Zuul还允许自定义Filter类型并显示执行。例如,我们定义一个STATIC类型的Filter,它直接在Zuul中生成一个响应,而非将请求在转发到目标。
6.3工作原理图
源码分析
1、登录Filter
这里我们需要解决分布式持久化的问题,最终选择的方案是spring session。实现思路是:登录成功后,将loginId作为key,jwt作为value存入redis,后续每次请求进行校验。
1.1spring session
引入依赖,spring session使用redis进行存储
<!-- redis support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 解决不引入报错 RedisConnectionFactory 找不到-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- 共享session support -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>复制
1.2启用spring session
@Configuration
// maxInactiveIntervalInSeconds session过期时间限制 redisFlushMode redis刷新模式,确保zuul中加入的session能立即被微服务获取到
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = GlobalConst.SESSION_TIMEOUT, redisFlushMode = RedisFlushMode.IMMEDIATE)
public class RedisSessionConfig {
@Bean
public CookieSerializer httpSessionIdResoler() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
// 取消samesite和修改session为默认的jsessionid
// cookieSerializer.setCookieName("JSESSIONID");
cookieSerializer.setCookiePath("/");
cookieSerializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
cookieSerializer.setSameSite(null);
return cookieSerializer;
}
}复制
1.3redis配置
参考Redis目录下的使用说明
1.4LoginFilter
/**
* @author BaZinGa
* @date 2019-02-23
* @desc filterType:返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下:
* pre:路由之前
* routing:路由之时
* post:路由之后
* error:发送错误调用
* filterOrder:过滤的顺序
* shouldFilter:这里可以写逻辑判断,是否要过滤,本文true,永远过滤。
* run:过滤器的具体逻辑。
**/
@Component
public class LoginFilter extends ZuulFilter {
private static Logger logger = LoggerFactory.getLogger(LoginFilter.class);
// 指定过滤器类型
@Override
public String filterType() {
return POST_TYPE;
}
// 指定其在过滤器链上所处的顺序,数字越小,优先级越高
@Override
public int filterOrder() {
return SEND_RESPONSE_FILTER_ORDER - 10;
}
// 是否启用这个filter
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 如果是登录请求则不进行
if (request.getRequestURI().indexOf("/login/verify") > -1 && request.getRequestURI().indexOf("/sso/") < 0) {
return true;
}
return false;
}
// 校验token
@Override
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if ((GlobalConst.OPTIONS).equalsIgnoreCase(request.getMethod())) {
return null;
}
HttpSession session = request.getSession();
InputStream inputSteam = ctx.getResponseDataStream();
// 登录校验成功后返回jwt,以loginid为key,jwt为value存入redis
String response = StreamUtils.copyToString(inputSteam, Charset.forName("UTF-8"));
JSONObject jo = JSONObject.fromObject(response);
int code = (int) jo.get("code");
if (GlobalConst.SUCCESS_CODE == code) {
String token = (String) ((JSONObject) jo.get("data")).get("token");
String loginId = JwtUtil.getLoginIdByJJWT(token);
session.setAttribute(loginId, token);
} else {
logger.info(response);
}
ctx.setResponseBody(response);
ctx.setResponseStatusCode(HttpStatus.SC_OK);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}复制
2、单点登录Filter
外部系统首先调用获取Token接口获取Token(30s过期),Token放入Redis后返回给外部系统
@Override
public Response getSsoToken(String loginId) {
try {
if ("".equals(StringUtil.null2String(loginId))) {
return new Response(ResultCodeEnum.PARAMS_ERROR,null);
}
redisUtil.setRedisTemplate(RedisTemplateEnum.SSOREDISTEMPLATE);
// 如果redis中已经存在,则直接返回
if (redisUtil.getValue("ssoToken:"+loginId) != null) {
return new Response(ResultCodeEnum.SUCCESS,redisUtil.getValue("ssoToken:"+loginId));
}
// 30S过期
String jwt = JwtUtil.createTokenByJJWT(loginId,GlobalConst.JWT_TIMEOUT);
if (!"".equals(jwt)) {
redisUtil.addKey(GlobalConst.SSOTOKEN + ":" + loginId, jwt);
return new Response(ResultCodeEnum.SUCCESS,jwt);
} else {
return new Response(ResultCodeEnum.TOKEN_CREATE_EMPTY,null);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
return new Response(ResultCodeEnum.TOKEN_CREATE_FAIL,null);
}
}复制
判断传入的Token为空,一致性,正确性后,生成新Token放入session完成持久化
@Component
public class SsoLoginFilter extends ZuulFilter {
@Autowired
RedisUtil redisUtil;
private static Logger logger = LoggerFactory.getLogger(SsoLoginFilter.class);
// 指定过滤器类型
@Override
public String filterType() {
return PRE_TYPE;
}
// 指定其在过滤器链上所处的顺序,数字越小,优先级越高
@Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 10;
}
// 是否启用这个filter
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 如果是登录请求则不进行
if (request.getRequestURI().indexOf("/sso/login/verify") > -1) {
return true;
}
return false;
}
// 校验token
@Override
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if ((GlobalConst.OPTIONS).equalsIgnoreCase(request.getMethod())) {
return null;
}
// 如果是单点登录,完成校验后返回
String ssoLoginId = request.getParameter("loginId");
redisUtil.setRedisTemplate(RedisTemplateEnum.SSOREDISTEMPLATE);
String ssoToken = redisUtil.getValue(GlobalConst.SSOTOKEN + ":" + ssoLoginId) == null ? null : (String) redisUtil.getValue(GlobalConst.SSOTOKEN + ":" + ssoLoginId);
// 判断token是否为空
if (ssoToken == null) {
logger.info("ssoToken not found");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
ctx.getResponse().setContentType("application/json;charset=UTF-8");
ctx.setResponseBody(JSONObject.fromObject(new Response(ResultCodeEnum.SSO_LOGIN_FAIL,"ssoToken not found")).toString());
return null;
}
// 判断token的一致性
String token = request.getParameter("ssoToken");
if (!ssoToken.equals(token)) {
logger.info("token not consistant");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
ctx.getResponse().setContentType("application/json;charset=UTF-8");
ctx.setResponseBody(JSONObject.fromObject(new Response(ResultCodeEnum.SSO_LOGIN_FAIL,"token not consistant")).toString());
return null;
}
// 判断token的正确性和一致性
if(!JwtUtil.verifyTokenByJJWT(token)){
logger.info("token verify fail");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
ctx.getResponse().setContentType("application/json;charset=UTF-8");
ctx.setResponseBody(JSONObject.fromObject(new Response(ResultCodeEnum.SSO_LOGIN_FAIL,"token verify fail")).toString());
return null;
}
String loginId = JwtUtil.getLoginIdByJJWT(token);
if(!loginId.equalsIgnoreCase(ssoLoginId)){
logger.info("loginid not consistant");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
ctx.getResponse().setContentType("application/json;charset=UTF-8");
ctx.setResponseBody(JSONObject.fromObject(new Response(ResultCodeEnum.SSO_LOGIN_FAIL,"loginid not consistant")).toString());
return null;
}
// 单点登录成功,放入session管理
String newToken = JwtUtil.createTokenByJJWT(ssoLoginId);
HttpSession session = request.getSession();
session.setAttribute(ssoLoginId, newToken);
// 移除redis中的token
redisUtil.delete(GlobalConst.SSOTOKEN + ":" + ssoLoginId);
// 返回
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.SC_OK);
ctx.getResponse().setContentType("application/json;charset=UTF-8");
ctx.setResponseBody(JSONObject.fromObject(new Response(ResultCodeEnum.SUCCESS,newToken)).toString());
return null;
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}复制
3、Token校验Filter
@Component
public class TokenFilter extends ZuulFilter {
private static Logger logger = LoggerFactory.getLogger(TokenFilter.class);
// 指定过滤器类型
@Override
public String filterType() {
return PRE_TYPE;
}
// 指定其在过滤器链上所处的顺序,数字越小,优先级越高
@Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 1;
}
// 是否启用这个filter
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 如果是登录请求则不进行
if (request.getRequestURI().indexOf("/login/verify") > -1) {
return false;
}
return true;
}
// 校验token
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if ((GlobalConst.OPTIONS).equalsIgnoreCase(request.getMethod())) {
return null;
}
HttpSession session = request.getSession(false);
if (session == null) {
logger.info("no session");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
ctx.getResponse().setContentType("application/json;charset=UTF-8");
ctx.setResponseBody(JSONObject.fromObject(new BasicResponse(ResultCodeEnum.SESSION_EMPTY,null)).toString());
return null;
}
String token = request.getHeader("token");
// 执行认证
if (token == null || "".equals(token)) {
logger.info("token is empty,relogin please");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
ctx.getResponse().setContentType("application/json;charset=UTF-8");
ctx.setResponseBody(JSONObject.fromObject(new BasicResponse(ResultCodeEnum.TOKEN_EMPTY,null)).toString());
return null;
}
// 验证 token
if(!JwtUtil.verifyTokenByJJWT(token)) {
logger.info("token verify failed,relogin please!");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
ctx.getResponse().setContentType("application/json;charset=UTF-8");
ctx.setResponseBody(JSONObject.fromObject(new BasicResponse(ResultCodeEnum.JWT_VERIFY_FAIL,null)).toString());
return null;
}
String loginId = JwtUtil.getLoginIdByJJWT(token);
String sessionToken = (String) session.getAttribute(loginId);
if (sessionToken == null || "".equals(sessionToken)) {
logger.info("key not found in session,relogin please");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
ctx.getResponse().setContentType("application/json;charset=UTF-8");
ctx.setResponseBody(JSONObject.fromObject(new BasicResponse(ResultCodeEnum.LOGINID_EMPTY_IN_SESSION,null)).toString());
return null;
}
// 校验token一致性
if (!token.equals(sessionToken)) {
logger.info("token is not consistent,relogin please");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
ctx.getResponse().setContentType("application/json;charset=UTF-8");
ctx.setResponseBody(JSONObject.fromObject(new BasicResponse(ResultCodeEnum.TOKEN_NOT_CONSISTANT,null)).toString());
return null;
}
// 添加Cookie
ctx.addZuulRequestHeader("Cookie", request.getHeader("cookie"));
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(HttpStatus.SC_OK);
return null;
}
}复制
4、跨域问题解决
/**
* zuul跨域配置
* @author zhangwy
* */
@Configuration
public class GatewayCorsConfig {
@SuppressWarnings({ "rawtypes", "unchecked" })
@Bean
public FilterRegistrationBean corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setMaxAge(3600L);
// 解决https的问题
// config.addExposedHeader("X-forward-port, X-forward-host");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}复制
5、传递请求时参数传递问题
/**
* 自定义的请求头处理类,处理服务发送时的请求头;将服务接收到的请求头中的token字段取出来,并设置到新的请求头里面去转发给下游服务;
* 比如A服务收到一个请求,请求头里面包含token字段,A处理时会使用Feign客户端调用B服务,那么token这个字段就会添加到请求头中一并发给B服务;
*
* @author zhangwy
*/
@Configuration
public class FeignHeaderConfig {
Logger logger = LoggerFactory.getLogger(getClass());
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
HttpServletRequest request = attrs.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String value = request.getHeader(name);
/**
* 遍历请求头里面的属性字段,将token添加到新的请求头中转发到下游服务
*/
if ("token".equalsIgnoreCase(name)) {
requestTemplate.header(name, value);
}
}
} else {
logger.warn("FeignHeaderConfig", "获取请求头失败!");
}
/*
// 转发body参数
Enumeration<String> bodyNames = request.getParameterNames();
StringBuffer body = new StringBuffer();
if (bodyNames != null) {
while (bodyNames.hasMoreElements()) {
String name = bodyNames.nextElement();
String value = request.getParameter(name);
body.append(name).append("=").append(value).append("&");
}
}
if (body.length() != 0) {
body.deleteCharAt(body.length() - 1);
requestTemplate.body(body.toString());
}
*/
}
};
}
}复制
6、解决请求传递时session变化的问题
# 解决请求在传递过程中session变化的问题
zuul.sensitive-headers = *
zuul.ignored-headers = Access-Control-Allow-Origin,Access-Control-Allow-Credentials,Access-Control-Allow-Methods复制
7、统一Fallback问题
/**
* @author zhangwy
* @description 统一fallback处理
* @date 2019-08-14
*/
@Component
public class SccbaFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
// 表明是哪个微服务需要回退,*代表所有服务
return "*";
}
/**
* 如果请求服务失败,返回信息给客户端
*/
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse(){
@Override
public InputStream getBody() throws IOException {
Response response = new Response(ResultCodeEnum.SERVICE_DOWN,route+":"+cause.getMessage());
return new ByteArrayInputStream(JSONObject.toJSON(response).toString().getBytes("UTF-8"));
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
//和body中的内容编码一致,否则容易乱码
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
return headers;
}
/**
* 网关向api服务请求是失败了,但是消费者客户端向网关发起的请求是OK的,
* 不应该把api的404,500等问题抛给客户端
* 网关和api服务集群对于客户端来说是黑盒子
*/
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.OK.value();
}
@Override
public String getStatusText() throws IOException {
return HttpStatus.OK.getReasonPhrase();
}
@Override
public void close() {
}
};
}
}复制
8、sentinel和zuul统一Fallback(此方案正确性待验证)
引入依赖
<!-- sentinel support -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-zuul-adapter</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.6.3</version>
</dependency>复制
8、sentinel和zuul统一Fallback(此方案正确性待验证)
引入依赖
<!-- sentinel support -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-zuul-adapter</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.6.3</version>
</dependency>复制
/**
* @author zhangwy
* @description zuul integrate sentinel
* @date 2019-08-14
*/
@Configuration
public class SentinelConfig {
@Bean
public ZuulFilter sentinelZuulPreFilter() {
return new SentinelZuulPreFilter();
}
@Bean
public ZuulFilter sentinelZuulPostFilter() {
return new SentinelZuulPostFilter();
}
@Bean
public ZuulFilter sentinelZuulErrorFilter() {
return new SentinelZuulErrorFilter();
}
@PostConstruct
public void doInit() {
// 注册 FallbackProvider
ZuulBlockFallbackManager.registerProvider(new SccbaBlockFallbackProvider());
initGatewayRules();
}
/**
* 配置限流规则
*/
private void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("yinjihuan").setCount(1) // 限流阈值
.setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
);
GatewayRuleManager.loadRules(rules);
}
}复制
/**
* @author zhangwy
* @description 自定义报错处理类
* @date 2019-08-14
*/
public class SccbaBlockFallbackProvider implements ZuulBlockFallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public BlockResponse fallbackResponse(String route, Throwable cause) {
if (cause instanceof BlockException) {
return new BlockResponse(429, "Sentinel block exception", route);
} else {
return new BlockResponse(500, "System Error", route);
}
}
}复制
9、Zuul动态配置路由规则(未执行)
https://blog.csdn.net/niemingming/article/details/80905656
10、OPTIONS请求导致session变化问题
原因:前端请求发请求之前会先发一次options请求,然后才发get/post的请求获取数据。
解决:
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return null;
}复制
11、Zuul集成OAuth2+Jwt(未执行)
https://blog.csdn.net/weixin_38003389/article/details/83654721
12、终止请求转发
Filter中通过context.setSendZuulResponse(false)可以终止请求的转发,但是只有pre类型的过滤器支持终止转发,其他过滤器都是按照顺序执行的,而且pre类型的过滤器也只有在所有pre过滤器执行完后才可以终止转发,做不到终止过滤器继续执行。
四、服务网关演进(未执行)
1、Kong
Kong(https://github.com/Kong/kong)是一个云原生,高效,可扩展的分布式 API 网关。
从技术的角度讲,Kong 可以认为是一个 OpenResty 应用程序。OpenResty 运行在 Nginx 之上,使用 Lua 扩展了 Nginx。Lua 是一种非常容易使用的脚本语言,可以让你在 Nginx 中编写一些逻辑操作。
Kong = OpenResty + Nginx + Lua
OpenResty(又称:ngx_openresty) 是一个基于 NGINX 的可伸缩的 Web 平台,由中国人章亦春发起,提供了很多高质量的第三方模块。
OpenResty 是一个强大的 Web 应用服务器,Web 开发人员可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,更主要的是在性能方面,OpenResty可以快速构造出足以胜任 10K 以上并发连接响应的超高性能 Web 应用系统。
Zuul和Spring Cloud Gateway可以认为是组件或者框架,而Kong可以称得上是产品,举例而言,如果选择使用 Zuul,当需要为应用添加限流功能,由于 Zuul 只提供了基本的路由功能,开发者需要自己研发 Zuul Filter,可能你觉得一个功能还并不麻烦,但如果在此基础上对 Zuul 提出更多的要求,很遗憾,Zuul 使用者需要自行承担这些复杂性。而对于 Kong 来说,限流功能就是一个插件,只需要简单的配置,即可开箱即用。Kong 的插件机制是其高可扩展性的根源,Kong 可以很方便地为路由和服务提供各种插件,网关所需要的基本特性,Kong 都如数支持。
云原生: 与平台无关,Kong可以从裸机运行到Kubernetes动态路由:Kong 的背后是 OpenResty+Lua,所以从 OpenResty 继承了动态路由的特性熔断健康检查日志: 可以记录通过 Kong 的 HTTP,TCP,UDP 请求和响应。鉴权: 权限控制,IP 黑白名单,同样是 OpenResty 的特性SSL: Setup a Specific SSL Certificate for an underlying service or API.监控: Kong 提供了实时监控插件认证: 如数支持 HMAC, JWT, Basic, OAuth2.0 等常用协议限流REST API: 通过 Rest API 进行配置管理,从繁琐的配置文件中解放可用性: 天然支持分布式高性能: 背靠非阻塞通信的 nginx,性能自不用说插件机制: 提供众多开箱即用的插件,且有易于扩展的自定义插件接口,用户可以使用 Lua 自行开发插件
2、Zuul2
2.1背景
因为 Zuul 开源时间较早,在架构方面也存在一些问题,所以Netflix 对外宣布他们将会调整 Zuul 的架构。Zuul 原本采用同步阻塞架构,转型后叫作 Zuul 2,采用异步非阻塞架构。Zuul 2 和 Zuul 1 在架构方面的主要区别在于,Zuul 2 运行在异步非阻塞的框架上,比如 Netty。Zuul 1 依赖多线程来支持吞吐量的增长,而 Zuul 2 使用的 Netty 框架依赖事件循环和回调函数。
2.2架构
过滤器前端和后端的 Netty 事件处理器(handler)主要负责处理网络协议、Web 服务器、连接管理和代理工作。这些内部工作被抽象之后,所有主要的工作都会交给过滤器完成。入站过滤器在代理请求之前运行,可用于验证、路由或装饰请求。端点过滤器可用于返回静态响应,或将请求代理到后端服务。出站过滤器在返回响应后运行,可用于诸如压缩(gzipping)、指标或增删自定义请求头之类的内容。
Zuul 的功能几乎完全取决于每个过滤器的逻辑。这意味着它可以部署在多种上下文中,使用配置和运行的过滤器解决不同的问题。
IO编程
在传统的IO模型中,每个连接创建成功之后都需要一个线程来维护,每个线程包含一个while死循环,那么1w个连接对应1w个线程,继而1w个while死循环,这就带来如下几个问题:
线程资源受限:线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起。
线程切换效率低下:单机cpu核数固定,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降。
除了以上两个问题,IO编程中,我们看到数据读写是以字节流为单位,效率不高。
NIO编程
通过选择器解决多线程的问题,共用一个线程。
线程数量的降低带来,切换效率的提升。
通过字节块来代替字节流从而提升效率。
Netty
Netty是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。因为实际利用JDK进行NIO编程很复杂,所以Netty封装了JDK的NIO,大大简化了开发难度。
在一些场景下,我们也可以用netty将springboot内置的容器tomcat替换掉从而达到更好的效果,Netty和Tomcat最大的区别就在于通信协议,Tomcat是基于Http协议的,他的实质是一个基于http协议的web容器,但是Netty不一样,他能通过编程自定义各种协议,因为netty能够通过codec自己来编码/解码字节流,完成类似redis访问的功能,这就是netty和tomcat最大的不同。Netty是基于Java NIO开发的,而Tomcat是Apache下的针对HTTP的服务器项目,前者更像一个中间件框架,后者更像一个工具。
2.3Zuul2和zuul1如何选择
[参考链接] http://blog.didispace.com/api-gateway-Zuul-1-zuul-2-how-to-choose/#lg=1&slide=1
总体上,同步阻塞模式比较适用于计算密集型(CPU bound)应用场景。对于IO密集型场景(IO bound),同步阻塞模式会白白消耗很多线程资源,它们都在等待IO的阻塞状态,没有做实质性工作。异步非阻塞模式比较适用于IO密集型(IO bound)场景,这种场景下系统大部分时间在处理IO,CPU计算比较轻,少量事件环线程就能处理。
结论:生产环境中继续使用Zuul1,原因如下:
Zuul1同步编程模型简单,门槛低,开发运维方便,容易调试定位问题。Zuul2门槛高,调试不方便。
Zuul1监控埋点容易,比如和调用链监控工具CAT集成,如果你用Zuul2的话,CAT不好埋点是个问题。
Zuul1已经开源超过6年,稳定成熟,坑已经被踩平。Zuul2刚开源很新,实际落地案例不多,难说有bug需要踩坑。
大部分公司达不到Netflix那个量级,Netflix是要应对每日千亿级流量,它们才挖空心思搞异步,一般公司亿级可能都不到,Zuul1绰绰有余。
Zuul1可以集成Hystrix熔断组件,可以部分解决后台服务慢阻塞网关线程的问题。
Zuul1可以使用Servlet 3.0规范支持的AsyncServlet进行优化,可以实现前端异步,支持更多的连接数,达到和Zuul2一样的效果,但是不用引入太多异步复杂性。
对于Zuul2,我的建议是持谨慎观望的态度,可以在测试环境小规模实验验证,但是暂不上到生产环境。
3、spring cloud gateway(计划中)
演进方式:
https://yq.aliyun.com/articles/653380
https://www.jianshu.com/p/cebdbcee29c5
https://blog.csdn.net/zhuguanghalo/article/details/82870676
服务网关对比:
https://www.imooc.com/article/36230
4、SIA-GATEWAY(计划中)
SIA-GATEWAY 是基于SpringCloud微服务生态体系下开发的一个分布式微服务网关系统。具备简单易用、可视化、高可扩展、高可用性等特征,提供云原生、完整及成熟的接入服务解决方案。
主要特性:
简单易用, 支持基于Docker容器的快速部署及交付。
兼容性良好, 兼容SpringBoot微服务及传统HTTP-URL的负载均衡及路由服务。
高可扩展性, 支持基于Java语言的第三方插件扩展特性及动态加载机制。
支持多租户,多用户角色下的网关拆分管理。
可视化管理,提供实时路由拓扑、网关集群拓扑展示功能。
服务治理,支持网关集群Dashboard、实时日志、历史日志查询、熔断管理、预警管理等功能。
多注册中心支持,提供分布式网关集群下对多注册中心集群的切换管理功能。
动态路由组件绑定机制,提供包括URL统计、日志、灰度发布、限流、安全等公共服务组件。
[地址] https://github.com/siaorg/sia-gateway