Nacos 1.2.1 /nacos/v1/ns/operator/metrics 403无权限分析

nacos 1.2.0时加入了基于RBAC的权限控制,这便于实际生产中使用。权限控制这一块的内容在官网有相应的博客介绍https://nacos.io/zh-cn/blog/nacos%201.2.0%20guide.html

在使用最新版本1.2.1集成到Spring Cloud的时候,使用创建的其他用户会出现访问指标接口/nacos/v1/ns/operator/metrics没有权限的情况,还有其他的一些接口也会出现这样的情况,本文就该问题一步步的分析,主要介绍的是一种寻找问题的思路,可能其中有对nacos不了解导致自己理解错误,希望大家能指出。

request: /nacos/v1/ns/operator/metrics failed, servers: [127.0.0.1:8848], code: 403, msg: <html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Sun Apr 19 17:38:06 CST 2020</div><div>There was an unexpected error (type=Forbidden, status=403).</div><div>authorization failed!</div></body></html>

从上面这个问题,我们来一步步的探索为什么发生了这样的事情

在nacos中使用权限控制

如果没有权限控制,其他用户可以恶意调用Open-API注销服务、修改配置,这种情况对于生产来说不能容忍的。下面介绍如何在nacos启用权限功能,具体详情可参考官网博客。

在application.properties配置文件中有如下配置,可以配置权限相关的内容,可以看到在1.2.0的版本中,已经作废了spring.security.enabled的相关配置,使用新的权限配置,只需要配置nacos.core.auth.enabled=true,即可开启nacos的权限功能,在管理界面中配置好相应的命名空间,用户、角色、权限等,万事俱备。
注:命名空间最佳实践,在官网博客也有相关介绍,https://nacos.io/zh-cn/blog/namespace-endpoint-best-practices.html

#*************** Access Control Related Configurations ***************#
### If enable spring security, this option is deprecated in 1.2.0:
#spring.security.enabled=false
### The ignore urls of auth, is deprecated in 1.2.0:
nacos.security.ignore.urls=/,/error,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/v1/auth/**,/v1/console/health/**,/actuator/**,/v1/console/server/**
### The auth system to use, currently only 'nacos' is supported:
nacos.core.auth.system.type=nacos
### If turn on auth system:
nacos.core.auth.enabled=true
### The token expiration in seconds:
nacos.core.auth.default.token.expire.seconds=18000
### The default token:
nacos.core.auth.default.token.secret.key=SecretKey012345678901234567890123456789012345678901234567890123456789
### Turn on/off caching of auth information. By turning on this switch, the update of auth information would have a 15 seconds delay.
nacos.core.auth.caching.enabled=false
权限配置界面

在Spring Cloud中使用nacos的服务发现

引入相关的依赖

dependencies {
    implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery'
}

启用服务发现

@EnableDiscoveryClient
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication .class, args);
    }
}

在配置文件中配置nacos的相关内容,配置文件为bootstrap.properties,并非application.properties,两者的区别可以百度搜索一下

spring.cloud.nacos.server-addr=127.0.0.1:8848
spring.cloud.nacos.username=test
spring.cloud.nacos.password=123456780
spring.cloud.nacos.discovery.namespace=local
spring.cloud.nacos.discovery.metadata.info.name=${spring.application.name}
spring.cloud.nacos.discovery.metadata.user.name=${spring.security.user.name}
spring.cloud.nacos.discovery.metadata.user.password=${spring.security.user.password}
spring.cloud.nacos.config.namespace=local

注:这里需要配置config.namespace才能正常启动,但是实际只引用了discovery相关的内容,有空再去研究下原因

当你的项目还引用了actuator的依赖,启动就会发现文章开始的时候出现的错误了,访问/nacos/v1/ns/operator/metrics没有权限。

分析问题

当出现这个问题的时候,我以为是真的没有权限导致,或者我的帐号密码不对,当我给新加的角色加上public的读写权限发现还是出现403没有权限,于是开始debug从源码中找到问题

启动服务,发现actuator健康状态异常


健康状态异常

找到对应的健康检查代码实现,根据源码发现是调用namingService.getServerStatus();来判断服务是否正常

public class NacosDiscoveryHealthIndicator extends AbstractHealthIndicator {
    private final NamingService namingService;

    public NacosDiscoveryHealthIndicator(NamingService namingService) {
        this.namingService = namingService;
    }

    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        // Just return "UP" or "DOWN"
        String status = namingService.getServerStatus();
        // Set the status to Builder
        builder.status(status);
        switch (status) {
        case "UP":
            builder.up();
            break;
        case "DOWN":
            builder.down();
            break;
        default:
            builder.unknown();
            break;
        }
    }
}

注:这个类NacosDiscoveryHealthIndicator 是在NacosDiscoveryEndpointAutoConfiguration 中自动配置,使用的名字是nacos-discovery,含有“-”的名字在actuator中是有警告的,Endpoint ID 'nacos-discovery' contains invalid characters, please migrate to a valid format.这个希望官方在后续修改为符合规范的命名吧。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Endpoint.class)
@ConditionalOnNacosDiscoveryEnabled
public class NacosDiscoveryEndpointAutoConfiguration {
    @Bean
    @ConditionalOnEnabledHealthIndicator("nacos-discovery")
    public HealthIndicator nacosDiscoveryHealthIndicator(NacosDiscoveryProperties nacosDiscoveryProperties) {
        return new NacosDiscoveryHealthIndicator(nacosDiscoveryProperties.namingServiceInstance());
    }
}

上面我们发现状态的判断是在namingService.getServerStatus()中进行,跟踪调试,发现实际在NamingProxy类的方法中出现了我们的目标,在这里发现调用了/operator/metrics这个地址,里面也可以跟进去debug,其实是构造发送http请求,最终返回的403错误。

 public boolean serverHealthy() {
        try {
            String result = reqAPI(UtilAndComs.NACOS_URL_BASE + "/operator/metrics",
                new HashMap<String, String>(2), HttpMethod.GET);
            JSONObject json = JSON.parseObject(result);
            String serverStatus = json.getString("status");
            return "UP".equals(serverStatus);
        } catch (Exception e) {
            return false;
        }
    }
http请求

还是不懂,既然带了token,为啥还会出现403错误呢。我们用nacos的Open-API来验证
第一步先登录获取token

POST http://127.0.0.1:8848/nacos/v1/auth/users/login
Content-Type: application/x-www-form-urlencoded

username=test&password=123456780

返回内容

{
  "globalAdmin": false,
  "tokenTtl": 18000,
  "accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNTg3MzA5OTQwfQ.hqH9NOfKJIr8TcPAHFx0yqnPqYWSIFIjSkP3fklQP_w"
}

我们拿到token再去调用其他的接口,Bearer这个权限验证方式可以百度搜索,发现返回的内容是config data not exist,这表示没有配置文件,但是token验证是通过的

GET http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=demo-admin-server.properties&group=DEFAULT_GROUP&tenant=local
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNTg3MzA5OTQwfQ.hqH9NOfKJIr8TcPAHFx0yqnPqYWSIFIjSkP3fklQP_w

然后调试我们本文的主角,发现返回403 Forbidden authorization failed!,为啥不行呢???!!!

GET http://127.0.0.1:8848/nacos/v1/ns/operator/metrics
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNTg3MzA5OTQwfQ.hqH9NOfKJIr8TcPAHFx0yqnPqYWSIFIjSkP3fklQP_w

启动大招,上GitHub拿到nacos的源码,找问题

全局搜索,在OperatorController类中找到了metrics接口的定义,@GetMapping比较了解,@Secured是权限相关的配置

    @Secured(resource = "naming/metrics", action = ActionTypes.READ)
    @GetMapping("/metrics")
    public JSONObject metrics(HttpServletRequest request) {
        JSONObject result = new JSONObject();
        int serviceCount = serviceManager.getServiceCount();
        int ipCount = serviceManager.getInstanceCount();
        int responsibleDomCount = serviceManager.getResponsibleServiceCount();
        int responsibleIPCount = serviceManager.getResponsibleInstanceCount();
        result.put("status", serverStatusManager.getServerStatus().name());
        result.put("serviceCount", serviceCount);
        result.put("instanceCount", ipCount);
        result.put("raftNotifyTaskCount", raftCore.getNotifyTaskCount());
        result.put("responsibleServiceCount", responsibleDomCount);
        result.put("responsibleInstanceCount", responsibleIPCount);
        result.put("cpu", SystemUtils.getCPU());
        result.put("load", SystemUtils.getLoad());
        result.put("mem", SystemUtils.getMem());
        return result;
    }

全局搜索"authorization failed!",发现出现在NacosAuthManager类中,根据角色服务判断是否有权限,permission是请求的资源的权限,需要根据用户的权限来匹配。

    @Override
    public void auth(Permission permission, User user) throws AccessException {
        if (Loggers.AUTH.isDebugEnabled()) {
            Loggers.AUTH.debug("auth permission: {}, user: {}", permission, user);
        }
        if (!roleService.hasPermission(user.getUserName(), permission)) {
            throw new AccessException("authorization failed!");
        }
    }

更进一步,在NacosRoleServiceImpl类中,根据用户获取所有的角色,如果有ROLE_ADMIN角色直接开启管理员模式,其他的就需要一步步的判断权限是否正确。

    public boolean hasPermission(String username, Permission permission) {
        List<RoleInfo> roleInfoList = getRoles(username);
        if (Collections.isEmpty(roleInfoList)) {
            return false;
        }
        // Global admin pass:
        for (RoleInfo roleInfo : roleInfoList) {
            if (GLOBAL_ADMIN_ROLE.equals(roleInfo.getRole())) {
                return true;
            }
        }
        // Old global admin can pass resource 'console/':
        if (permission.getResource().startsWith(NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX)) {
            return false;
        }
        // For other roles, use a pattern match to decide if pass or not.
        for (RoleInfo roleInfo : roleInfoList) {
            List<PermissionInfo> permissionInfoList = getPermissions(roleInfo.getRole());
            if (Collections.isEmpty(permissionInfoList)) {
                continue;
            }
            for (PermissionInfo permissionInfo : permissionInfoList) {
                String permissionResource = permissionInfo.getResource().replaceAll("\\*", ".*");
                String permissionAction = permissionInfo.getAction();
                if (permissionAction.contains(permission.getAction()) &&
                    Pattern.matches(permissionResource, permission.getResource())) {
                    return true;
                }
            }
        }
        return false;
    }

上面的代码中有权限Permission的相关判断,主要的两个属性action和resource是什么呢?我们从数据库中来看,action表示读写权限,直接字符串包含即可判断;Resource启动了正则匹配,那问题就出现在这里了。


数据库中的角色权限

我们看InstanceController类中与OperatorController类中接口的定义,区别在于resource的定义,一个是字符串,另一个是根据方法生成的字符串

@GetMapping("/list")
@Secured(parser = NamingResourceParser.class, action = ActionTypes.READ)
public JSONObject list(HttpServletRequest request) throws Exception {}

@Secured(resource = "naming/metrics", action = ActionTypes.READ)
@GetMapping("/metrics")
public JSONObject metrics(HttpServletRequest request) {}
public class NamingResourceParser implements ResourceParser {
    private static final String AUTH_NAMING_PREFIX = "naming/";
    @Override
    public String parseName(Object request) {
        HttpServletRequest req = (HttpServletRequest) request;
        String namespaceId = req.getParameter(CommonParams.NAMESPACE_ID);
        String serviceName = req.getParameter(CommonParams.SERVICE_NAME);
        String groupName = req.getParameter(CommonParams.GROUP_NAME);
        if (StringUtils.isBlank(groupName)) {
            groupName = NamingUtils.getGroupName(serviceName);
        }
        serviceName = NamingUtils.getServiceName(serviceName);
        StringBuilder sb = new StringBuilder();
        if (StringUtils.isNotBlank(namespaceId)) {
            sb.append(namespaceId);
        }
        sb.append(Resource.SPLITTER);
        if (StringUtils.isBlank(serviceName)) {
            sb.append("*")
                .append(Resource.SPLITTER)
                .append(AUTH_NAMING_PREFIX)
                .append("*");
        } else {
            sb.append(groupName)
                .append(Resource.SPLITTER)
                .append(AUTH_NAMING_PREFIX)
                .append(serviceName);
        }
        return sb.toString();
    }
}

所以Permission中resource的定义为namespaceId:groupName:serviceName,实际在管理界面配置的时候还没有具体的groupName与serviceName配置。可能官方也还在开发吧。问题找到了,实际就是权限的资源格式不统一导致的,可能是我才疏学浅没领悟到精髓,也可能是官方还在Coding...

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,104评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,816评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,697评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,836评论 1 298
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,851评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,441评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,992评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,899评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,457评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,529评论 3 341
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,664评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,346评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,025评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,511评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,611评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,081评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,675评论 2 359

推荐阅读更多精彩内容