整合nacos报403错误
因为平台写的一个限流代码逻辑有问题,所以准备使用sentinel
来限流。平台依赖里面已经引入了,之前也测试过,把sentinel
关于nacos
的配置加上后,启动一直输出403
错误 [fixed-10.0.20.188_8848-test] [check-update] get changed dataId error, code: 403。
403的原因
先说结论,sentinel
(我这里用到的alibaba-sentinel-datasource
是2.1.2.RELEASE
版本)的代码有问题,他这个版本的nacos
相关配置没有username
和password
的属性,导致只能在nacos
服务没有配置账号密码的时候使用,一旦改了账号密码,就无法正常访问nacos
。今年不能用去年可以是因为今年测试环境nacos
改了密码而去年是默认的。(文心一言竟然跟我说,他是共享了nacos
的配置,所以不用配置账号密码,离谱)
仓库找了个比这高的版本(2021.0.5.0),发现这两个属性加上了,有需要的可以升级试试。不得不吐槽一句,这个依赖的版本是真的多,而且还没啥规律。如果想通过改源码的方式解决问题看最后。
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2021.0.5.0</version>
</dependency>
配置截图,
com.alibaba.cloud.sentinel.datasource.config.NacosDataSourceProperties
类截图:
403问题排查过程
打断点查看http请求代码
因为控制台输出了ERROR
日志,找到输出日志的代码行,打个断点。这里打断点是最方便的,我是通过logging.level
把log
级别改成了trace
,然后看的http请求日志,在请求那里打的断点。
com.alibaba.nacos.client.config.impl.ClientWorker
:
通过调试发现在386行会发起http请求nacos
服务器,agent
属性是sentinel
自己写的一个HttpAgent
对象,主要是在发送http请求前做一些处理(比如拼上配置文件里面配置的nacos.server-addr
)。
调用agent.httpPost
之后,会在com.alibaba.nacos.client.config.http.ServerHttpAgent#httpPost
处理,ServerHttpAgent
类是HttpAgent
接口的实现类。这里面主要就是在发送http请求之前,通过injectSecurityInfo
方法给请求参数加入一些安全相关的参数。
injectSecurityInfo
方法里面会判断securityProxy
属性里面是否有token
,有就加入,这里为空,所以大概率是这个问题了。
和nacos框架发送的http请求做对比
为了做一个对比,查看一下nacos框架发送的http请求和这个有啥区别,因为这是nacos
里面的代码,所以nacos
框架也会调用,可以看一下nacos
框架请求nacos
服务器里面有哪些信息。
可以发现nacos
框架发送的请求,他这个securityProxy
里面是有token
信息的。这个是能正常请求nacos
服务器的。现在能确定问题就是出现在这里了。所以现在的重点就是securityProxy
这个属性。
securityProxy属性排查
因为SecurityProxy
是ServerHttpAgent
类的属性,搜了一下发现这个属性只在构造函数那里初始化赋值的,所以在ServerHttpAgent
类的构造函数那里打个断点。在nacos
框架初始化和sentinel
初始化的时候,都会来调用这个,所以我们可以通过调试nacos
框架的初始化来排查sentinel
这边的哪里出了问题。
com.alibaba.nacos.client.config.http.ServerHttpAgent#ServerHttpAgent(java.util.Properties)
截图
由于这个地方是在项目启动的时候调用,所以打完断点需要重启一下。
nacos框架初始化securityProxy
进入断点之后,发现nacos
框架的这个properties
里面是有账号密码配置的(配置文件里面配置了)
com.alibaba.nacos.client.security.SecurityProxy
构造器里面给账号密码属性赋值(sentinel
的那个压根都没有这两个属性,所以也能解释的通了,按道理这么低级的错误不应该的)
赋值完成之后,调用SecurityProxy
里面的login
方法,这个方法会发送登录请求,然后拿到token
赋值给accessToken
属性。
sentinel整合nacos的初始化securityProxy
com.alibaba.cloud.sentinel.custom.SentinelDataSourceHandler#afterSingletonsInstantiated
类继承了SmartInitializingSingleton
类,并重写了afterSingletonsInstantiated
方法
文心一言:
SmartInitializingSingleton接口用于在Spring容器中所有单例Bean初始化完成后执行一些自定义的初始化逻辑。当所有单例Bean的依赖关系都解析完毕,并且所有单例Bean的实例化与初始化过程完成后,Spring容器会自动回调SmartInitializingSingleton接口的实现类的afterSingletonsInstantiated()方法。这个方法只会被调用一次。
在这个重写的方法内部,会对每个数据源进行初始化。sentinel
里面每一个数据源都对应一个bean
,这个bean
由sentinel
自己的beanFactory
创建。
注册bean
的方法。里面有一行BeanDefinitionBuilder.genericBeanDefinition(dataSourceProperties.getFactoryBeanName());
代码,用来创建一个bean
定义构造器对象,后面要使用这个对象要创建NacosDataSourceFactoryBean
类的bean
文心一言:
BeanDefinitionBuilder主要用于在Spring的IoC(Inversion of Control,控制反转)容器中构建和配置Bean定义。Bean定义描述了如何创建一个Bean实例,包括其类名、作用域、初始化方法、属性设置等信息。通过BeanDefinitionBuilder,开发者可以以编程的方式轻松地创建和配置Bean定义,而不需要手动编写冗长的XML配置文件或注解。
遍历属性集合,然后里面校验一下,就通过BeanDefinitionBuilder builder
对象把这些属性添加进去
注册bean
之后又通过beanFactory
来获取bean
,注册的bean
类型是com.alibaba.cloud.sentinel.datasource.factorybean.NacosDataSourceFactoryBean
类型
在sentinel
自己实现的BeanFactory#getObject
方法内,每次都是返回NacosDataSource
对象(也就是说前面创建的bean是这个自定义的工厂bean,每次getBean都是获取到NacosDataSource对象,如果想获取到这个bean对象本身,getBean的时候名字前面加一个&)。在初始化Properties
的时候,只判断地址是否配置,都没有关于账号密码的代码,所以sentinel
用nacos
数据源在当初设计的时候,就是没有考虑到nacos
会改账号或密码的情况,而不是配置文件里面漏了这两个属性。
因为前面都没有账号密码,所以ServerHttpAgent
构造器这里的属性里面只有两个值,肯定也没有用户名和密码的。
大致的调用栈
排查结论
通过排查发现,这个对象所需的属性是从配置文件中拿的,因为nacos
框架配置了username
和password
,所以拿的到。而sentinel
并没有提供相应的属性配置,所以就为空,直接导致了后面的403
问题。
解决方案
- 如开头所说,升级版本就可以了,这是最简单的,当然有的系统可能升级之后会报错,这个就需要自己排查一下了
- 把账号密码改成默认的(当我没说,这个应该没人弄)
- 重写代码,自己项目
main/java
下面创建一个全类名一样的类,覆盖原有的类
com.alibaba.cloud.sentinel.datasource.factorybean.NacosDataSourceFactoryBean
类和com.alibaba.cloud.sentinel.datasource.config.NacosDataSourceProperties
类,在两个类里面都加上username
和password
属性,然后在NacosDataSourceFactoryBean
里面赋值给属性类。
或者说不改NacosDataSourceProperties
代码直接改NacosDataSourceFactoryBean
工厂,在里面获取nacos
框架的配置,用那里面的账号密码也可以。
重写的NacosDataSourceProperties
类,记得加set
、get
方法,也可以使用Lombok
package com.alibaba.cloud.sentinel.datasource.config;
import com.alibaba.cloud.sentinel.datasource.factorybean.NacosDataSourceFactoryBean;
import org.springframework.util.StringUtils;
import javax.validation.constraints.NotEmpty;
/**
* Nacos Properties class Using by {@link DataSourcePropertiesConfiguration} and
* {@link NacosDataSourceFactoryBean}.
*
* @author <a href="mailto:fangjian0423@gmail.com">Jim</a>
*/
public class NacosDataSourceProperties extends AbstractDataSourceProperties {
private String serverAddr;
@NotEmpty
private String groupId = "DEFAULT_GROUP";
@NotEmpty
private String dataId;
private String endpoint;
private String namespace;
private String accessKey;
private String secretKey;
/**
* 自己添加的属性 用户名
*/
private String username;
/**
* 自己添加的属性 名称
*/
private String password;
public NacosDataSourceProperties() {
super(NacosDataSourceFactoryBean.class.getName());
}
@Override
public void preCheck(String dataSourceName) {
if (StringUtils.isEmpty(serverAddr)) {
serverAddr = this.getEnv().getProperty(
"spring.cloud.sentinel.datasource.nacos.server-addr",
"localhost:8848");
}
}
public String getServerAddr() {
return serverAddr;
}
public void setServerAddr(String serverAddr) {
this.serverAddr = serverAddr;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public String getDataId() {
return dataId;
}
public void setDataId(String dataId) {
this.dataId = dataId;
}
public String getEndpoint() {
return endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public String getAccessKey() {
return accessKey;
}
public void setAccessKey(String accessKey) {
this.accessKey = accessKey;
}
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public void setUsername(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
}
重写的NacosDataSourceFactoryBean
类,太长了,这里就列出部分,直接加进去就行了。别忘了set
、get
方法
/**
* 自己添加的属性 用户名
*/
private String username;
/**
* 自己添加的属性 密码
*/
private String password;
@Override
public NacosDataSource getObject() throws Exception {
Properties properties = new Properties();
if (!StringUtils.isEmpty(this.serverAddr)) {
properties.setProperty(PropertyKeyConst.SERVER_ADDR, this.serverAddr);
} else {
properties.setProperty(PropertyKeyConst.ACCESS_KEY, this.accessKey);
properties.setProperty(PropertyKeyConst.SECRET_KEY, this.secretKey);
properties.setProperty(PropertyKeyConst.ENDPOINT, this.endpoint);
}
if (!StringUtils.isEmpty(this.namespace)) {
properties.setProperty(PropertyKeyConst.NAMESPACE, this.namespace);
}
// 自己加的逻辑 把账号密码加进去
if (!StringUtils.isEmpty(this.username)) {
properties.setProperty(PropertyKeyConst.USERNAME, this.username);
}
if (!StringUtils.isEmpty(this.password)) {
properties.setProperty(PropertyKeyConst.PASSWORD, this.password);
}
return new NacosDataSource(properties, groupId, dataId, converter);
}
改造之后效果截图:
账号密码等属性都有了