美文网首页
Nacos 1.2.1 /nacos/v1/ns/operato

Nacos 1.2.1 /nacos/v1/ns/operato

作者: hillside6 | 来源:发表于2020-04-19 20:08 被阅读0次

    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...

    相关文章

      网友评论

          本文标题:Nacos 1.2.1 /nacos/v1/ns/operato

          本文链接:https://www.haomeiwen.com/subject/qmivbhtx.html