0%

#背景
使用spring cloud gateway开发的网关服务,业务部门需要接入摄像头使用,就是在登录时,支持摄像头登录。由于之前都是通过配置成http协议访问网关,然后由网关服务进行后台转发,但是现在集成的摄像头接口,只支持https协议进行调用,因此业务部门开发人员使用了两套网关服务,一套配置成https协议,只转发摄像头请求,一套配置成http协议,转发其他请求;两套直接使用同一个sso进行单点登录验证;两个网关部署在同一台服务器上,分别使用443和80端口。

在这种情况下,无论先登录http服务还是https服务,同一个浏览器访问另一个服务时,都能实现sso自动跳转进行免密登录。但是注销时,情况却不一样,当在https服务中进行注销后,刷新http服务页面,会自动跳转到登录认证页面;当首先在http服务中进行注销后,刷新https服务页面,却仍能正常访问,不会跳转到登录认证页面。

解决方案

首先构造测试环境,复现问题

在本地开发机使用idea启动了两个网关服务,分别为https和http协议,可以转发到相同的sso。默认情况下,现象确实如此。

分析跟踪问题

分析注销场景,当注销时,网关服务在后台清理session信息,主要是redis中保存的session相关信息;然后返回的response的header中,使用set-cookie通知浏览器清理会话信息。实际运行场景下,当登录成功时,通过浏览器调试窗口可以看到,http服务中的cookie中没有session信息,只有CASTGC信息,而https服务的cookie中包括了session、CASTGC信息;但是单独登录http服务,会出现session信息,因此可以推断https服务进行单点登录时会清除掉http的session信息。当http服务退出登录时,其相应头包含Set-COokie: session=;Max-Age=0;expires=Thu,01-Jan-1970 00:00:00 GMT;Path=/;HttpOnly的内容进行清除,但是由于session信息在http和https服务是共用的,当使用了https服务时,session信息多了个secure的属性,该属性会导致http服务的set-cookie相应无法正确起作用,F12查看,会看到一个黄色三角号进行警告:This Set-Cookie was blocked because it had the "Secure" attribute but was not received over a secure connection. This Set-Cookie was blocked because it was not sent over a secure-connection and would have overwritten a cookie with Secure attribute.

通过以上分析,总结如下: 当一个服务,采用了相同的域名或ip对外同时提供http服务和https服务时,浏览器中session是共享的(在cookie当中可以看到domain属性只区分到域名或ip,不会包括端口信息)。当使用https服务时,cookie中的session属性中包括了secure属性,该属性会导致session属性无法通过http请求进行修改,从而表现出,http服务注销后,https服务仍在登录状态。

解决办法

根据以上分析,在https服务中,登录设置session时,去掉secure属性即可。具体解决代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class MyGatewapAutoConfiguration{
@Value("${mygateway.setCookieSecure:true}")
String secureState;

@Bean
public WebSessionIdResolver webSessionIdResolver(){
CookieWebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver();
sessionIdResolver.addCookieInitializer(
t->{
if("false".equalsIgnoreCase(secureState)){
t.secure(false);
}
});
return sessionIdResolver;
}
}

通过自定义WebSessionIdResolver,覆盖掉spring cloud gateway中默认的WebSessionIdResolver,可以在每次响应设置session时,进行自定义secure属性设置;secureState开关可以设置是否打开该功能。

关闭secure属性,会降低https的服务的安全性,如无必要,不建议设置。

一、背景

现场某个服务在eureka上注册了三个节点,但运维人员反映大多数请求均落在固定一个节点上,其它两个节点几乎没什么压力,服务请求是通过zuul网关进行转发。
我的第一反应是不可能啊,zuul网关转发不应该是默认负载均衡,轮询转发吗,肯定是配置问题?(甩锅标准套路,哈哈)等拿到现场配置,仔细一看,确实是默认配置,难道是我记错了吗?赶紧启动项目,跟踪代码看看。

补充:读了Spring Cloud官方reference,其中写明了默认配置,还是要多读读官方文档才行啊。

image-20210419153944949

二、构造测试环境

搭建一套最小的集成环境,包括服务如下:

注册中心eureka
网关服务zuul
后端服务,也就是服务提供者 service-provider

三个服务依赖的springboot版本为2.3.9.RELEASEspring cloud版本为Hoxton.SR10。注册中心、网关启动一个实例,后端服务启动两个实例,网关和后端服务均注册到注册中心当中。

1. 网关服务配置路由信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server:
port: 8080
eureka:
instance:
prefer-ip-address: true
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: zuul-gateway
zuul:
debug:
request: true
routes:
provider:
path: /service-provider/**
stripPrefix: false
serviceId: service-provider

2. 注册中心配置

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8761
eureka:
instance:
hostname: localhost
prefer-ip-address: false
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/

3. 后端服务配置

后端服务启动了两个实例,端口分别为8081、8082。

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8081
servlet:
context-path: /service-provider
eureka:
client:
enabled: true
service-url:
default: http://localhost:8761/eureka/
spring:
application:
name: service-provider
1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8082
servlet:
context-path: /service-provider
eureka:
client:
enabled: true
service-url:
default: http://localhost:8761/eureka/
spring:
application:
name: service-provider

后端服务提供了一个get请求的接口,方便直接在浏览器中调用(也省了再运行一个服务去调用该接口)。服务实现就是打印helloworld

等待服务运行成功,我们可以通过访问以下三个url完成调用:

http://127.0.0.1:8081/service-provider/

http://127.0.0.1:8082/service-provider/

http://127.0.0.1:8080/service-provider/

返回的结果都是如下图一样:

image-20210417214819811

三、调试跟踪调用链

1.确定默认负载均衡策略

根据现场描述,问题出在负载均衡策略的选择上,因此找到负载均衡策略,查看接口IRule的定义:

1
2
3
4
5
6
7
8
9
10
package com.netflix.loadbalancer;

public interface IRule {
//选择服务节点
Server choose(Object var1);
//设置负载均衡器
void setLoadBalancer(ILoadBalancer var1);
//获取负载均衡器
ILoadBalancer getLoadBalancer();
}

可以看到,这3个接口,第一个就是选择服务节点,在idea中通过快捷键option+command+B找到所有所有继承该接口的类,可以定位到都集中在com.netflix.ribbon:ribbon-loadbalancer:2.3.0这个jar包中,排除抽象类,最终可用的类都放在com.netflix.loadbalancer主要有如下几个:

1
2
3
4
5
6
7
com.netflix.loadbalancer.AvailabilityFilteringRule
com.netflix.loadbalancer.BestAvailableRule
com.netflix.loadbalancer.RandomRule
com.netflix.loadbalancer.RetryRule
com.netflix.loadbalancer.RoundRobinRule
com.netflix.loadbalancer.WeightedResponseTimeRule
com.netflix.loadbalancer.ZoneAvoidanceRule

先将所有的策略的choose方法都设置断点,然后在浏览器中发起请求http://127.0.0.1:8080/service-provider/,观察调用堆栈如下,可以看到,调用的是PredicateBasedRule里的choose方法,定位具体使用的策略类为ZoneAvoidanceRule

image-20210417222814660

2.定位ZoneAvoidanceRule初始化过程

我们在ZoneAvoidanceRule类的构造函数上设置断点,重启zuul网关,观察断点触发时机。可以观察到,程序启动完成后,断点都没有被触发,因此可以推断是懒加载的情况,既在请求发生时,按需创建。在浏览器中发起请求,断点被触发,查看堆栈如下:

image-20210417225700302

观察调用堆栈,可以看到其构造函数是在org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration中被调用的,点进去查看代码如下:

image-20210417225917962

在这段代码中可以看到,首先是根据第112行判断当前服务标识下是否设置了IRule.class,如果设置了,就使用设置的策略,否则就使用ZoneAvoidanceRule作为默认策略。下面定位第112行的内部处理逻辑。

3.定位当前服务标识配置处理

在上图中,org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration第112设置断点,按照步骤2中的方法重新执行,触发断点,进入到isSet内部,查看到类org.springframework.cloud.netflix.ribbon.PropertiesFactory中处理逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public boolean isSet(Class clazz, String name) {
return StringUtils.hasText(getClassName(clazz, name));
}

public String getClassName(Class clazz, String name) {
if (this.classToProperty.containsKey(clazz)) {
String classNameProperty = this.classToProperty.get(clazz);
String className = environment
.getProperty(name + "." + NAMESPACE + "." + classNameProperty);
return className;
}
return null;
}

@SuppressWarnings("unchecked")
public <C> C get(Class<C> clazz, IClientConfig config, String name) {
String className = getClassName(clazz, name);
if (StringUtils.hasText(className)) {
try {
Class<?> toInstantiate = Class.forName(className);
return (C) SpringClientFactory.instantiateWithConfig(toInstantiate,
config);
}
catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Unknown class to load " + className
+ " for class " + clazz + " named " + name);
}
}
return null;
}

查看此时this.classToProperty变量值如下:

image-20210417231628221

其处理逻辑就是根据class找到对应的名称,此处为NFLoadBalancerRuleClassName,再拼接name + "." + NAMESPACE + "." + classNameProperty为属性名(service-provider.ribbon.NFLoadBalancerRuleClassName),从系统配置environment中进行查找。如果找到,就调用org.springframework.cloud.netflix.ribbon.PropertiesFactory#get方法初始化对象,否则就使用默认的负载均衡策略。

四、总结

通过RibbonClientConfiguration可以看到,ribbon可以分别对单个服务进行个性化配置;同时懒加载的处理方式,保证了程序能够快速启动。下一步需要跟踪判断,ribbon的懒加载的实现方式,为什么在第一次调用时才会调用到RibbonClientConfiguration里的函数,而不是程序启动初始化时执行。

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

草稿

刚刚提到了 Hexo 的一种特殊布局:draft,这种布局在建立时会被保存到 source/_drafts 文件夹,您可通过 publish 命令将草稿移动到 source/_posts 文件夹,该命令的使用方式与 new 十分类似,您也可在命令中指定 layout 来指定布局。

1
$ hexo publish [layout] <title>

草稿默认不会显示在页面中,您可在执行时加上 --draft 参数,或是把 render_drafts 参数设为 true 来预览草稿。
参考文档

More info: Deployment
Seedeffl

时间来到了2018年的末尾,2018年12月29日。许久没熬夜到这个时间点了,只有夜里这个时间才完完整整的属于我自己。看过一个段子,下一年的计划,就是把上一个年头的计划修修改改,基本都不会实现,这也许就是我这种普通人的真实写照吧。

心里有许多话要说,但夜已深,且markdown用的不是很熟,只待后面有空再整理。

总的来说,今年的事情有以下几个:

1、工作换了,工资低,工作杂,没成就感;

2、给自己和老婆买了保险,抵抗疾病的风险;

3、买了个二手mbp,新的买不起,穷;

4、几年没锻炼,身体不如从前;

5、我妈眼睛有一只看不见了,我总感觉是带我家孩子没休息好,占主要因素,很愧疚;

6、孩子在我看着的那天,把胳膊摔断了,很内疚心疼

没做成的事:

1、Clojure一年没学习了,荒废了

2、工作没做好,没能做成成就

3、xxx

4、–没能锻炼身体–