# 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。官方给的定义是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。我们可以更通俗的理解为**服务注册中心加配置中心。** ![20200707131306194](https://gitee.com/zhaoyang0425/img/raw/master/img/20200707131306194.png) 它在分布式架构中作用相当于之前的**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/ ![在这里插入图片描述](https://gitee.com/zhaoyang0425/img/raw/master/img/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ZhdGlhb3llemk=,size_16,color_FFFFFF,t_70-20210218211933963.png) 在使用Nacos之前,需要先安装Nacos控制台,控制台下载地址:https://github.com/alibaba/nacos/ Nacos控制台目前提供两种启动方式: - standalone       此模式一般用于 demo 和测试,不用改任何配置 - cluster             此模式一般用于生产环境 在此,由于是进行Demo演示,采取较为简单的standalone模式。 ### 3、启动步骤 - 下载解压文件,目录如下: [![r0ihOs.png](https://img-blog.csdnimg.cn/img_convert/9d6893147685da3db4a96d9500c638ba.png)](https://imgchr.com/i/r0ihOs) - 进入bin目录,在当前目录打开黑窗口,Linux/Unix执行sh startup.sh -m standalone(windows系统执行 startup.cmd -m standalone) (-m standalone 表示以单机模式运行) ![在这里插入图片描述](https://gitee.com/zhaoyang0425/img/raw/master/img/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ZhdGlhb3llemk=,size_16,color_FFFFFF,t_70-20210218215738409.png) - 启动成功画面 ![在这里插入图片描述](https://gitee.com/zhaoyang0425/img/raw/master/img/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ZhdGlhb3llemk=,size_16,color_FFFFFF,t_70-20210218215743726.png) - 在浏览器中输入http://localhost:8848/nacos 用户名:nacos 密码:nacos - 登录成功界面如下 ![image-20210218215845848](https://gitee.com/zhaoyang0425/img/raw/master/img/image-20210218215845848.png) ## 三、Nacos作为注册中心的基本运行 Nacos注册中心功能,通过server-consumer和server-provider两个工程完成演示。 + server-provide:服务提供者 + server-consumer:服务消费者 ![img](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2xhcnNjaGVuZy9teUltZy9tYXN0ZXIvYmxvZ0ltZy9OYWNvcy8yMDE5MDcwOTE1NTYwMC5wbmc) + 将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个修改成不同端口,以便测试负载均衡。至此服务提供者工程创建完毕。 image-20210218220213997 ### 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控制台,在服务列表中会显示这两个服务,控制台可以对其进行监控和管理; ![image-20210218220445462](https://gitee.com/zhaoyang0425/img/raw/master/img/image-20210218220445462.png) + 浏览器中访问服务消费者的接口http://127.0.0.1:9001/getHello, 可以看到成功返回结果; image-20210218220858900 image-20210218220915333 image-20210218220939047 image-20210218221010570 + 多次刷新页面可看见已实现负载均衡 ![图片](https://gitee.com/zhaoyang0425/img/raw/master/img/640.png) ## 四、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配置管理->配置列表中新建配置 ![image-20210218221356718](https://gitee.com/zhaoyang0425/img/raw/master/img/image-20210218221356718.png) 点击右上方  "+"  按钮,进入如下页面: image-20210218221423923 + **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 + **文件内容** ![image-20210218221508207](https://gitee.com/zhaoyang0425/img/raw/master/img/image-20210218221508207.png) 以上的每一项,在工程的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,可以看到如下返回结果 image-20210218221610342 + 说明成功读取到配置文件内容,接着修改控制台配置文件内容,测试是否能够动态刷新 image-20210218221707598 + 调用 http://localhost:9100/getInfo接口,返回如下结果,表明动态刷新成功。 image-20210218221726810 ### 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 ![image-20210218221800561](https://gitee.com/zhaoyang0425/img/raw/master/img/image-20210218221800561.png) + 重启nacos-config,访问 http://localhost:9100/getInfo,返回如下结果: image-20210218221910125 ### 3、自定义命名空间(Namespace) 命名空间用于进行租户粒度的配置隔离。不同的命名空间下,可以存在相同的 Group 或 Data ID 的配置。Namespace 的常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。 在bootstarp.×××文件没有指定 `${spring.cloud.nacos.config.namespace}` 配置的情况下, 默认使用的是 Nacos 上 Public 这个namespae。如果需要使用自定义的命名空间,可以通过如下方式实现: + 查看命名空间列表 ![image-20210218221954929](https://gitee.com/zhaoyang0425/img/raw/master/img/image-20210218221954929.png) + 将命名空间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命名空间下新建配置 ![image-20210218222049437](https://gitee.com/zhaoyang0425/img/raw/master/img/image-20210218222049437.png) + 重启nacos-config,访问 http://localhost:9100/getInfo,返回如下结果: image-20210218222134510 ### 4、自定义Group 在同一个group下,配置文件名不能重复,所以当需要创建文件名称相同的两个配置文件时,将两个配置文件创建在不同的group下即可。当我们再同一个group下创建一个已有的配置文件时,nacos会将其视为配置文件的修改,而不是新建。 + 在Nacos-server上DEV命名空间下新建配置,填写自定义的Group名称,直接输入即可。本案例中自定义组名称为MYSELF_GROUP。 ![image-20210218222239799](/Users/zhaoyang/Library/Application Support/typora-user-images/image-20210218222239799.png) + 在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,返回如下结果: image-20210218222310418 ## 五、Nacos安全漏洞! nacos最新版本1.4.1对于User-Agent绕过安全漏洞的serverIdentity key-value修复机制,依然存在绕过问题,在nacos开启了serverIdentity的自定义key-value鉴权后,通过特殊的url构造,依然能绕过限制访问任何http接口。 ### 1、漏洞复现 - 绕过了鉴权,添加了新用户 ![image-20210218223141864](https://gitee.com/zhaoyang0425/img/raw/master/img/image-20210218223141864.png) - 绕过了鉴权,返回了用户列表数据 ![image-20210218223312320](https://gitee.com/zhaoyang0425/img/raw/master/img/image-20210218223312320.png) - 返回的用户列表数据中,多了一个我们通过绕过鉴权创建的新用户 ![image-20210218223632842](https://gitee.com/zhaoyang0425/img/raw/master/img/image-20210218223632842.png) ![image-20210218223452864](https://gitee.com/zhaoyang0425/img/raw/master/img/image-20210218223452864.png) - 访问首页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)