Nacos註冊中心,服務註冊時加入自定義元數據,把各個服務的接口信息放入服務元數據裏

最近在做分佈式細粒度權限控制,業務需要使用各個服務的接口及權限信息進行權限的校驗。
這裏把各個接口的權限級別分爲三種:

  • 公開權限:所有的客戶端都能直接訪問,不參與Token校驗(不需要登錄)、不參與權限校驗
  • 內部公開權限:需要登錄參與Token校驗,不需要進行授權校驗,所有登錄用戶都能訪問
  • 完整控制權限:需要登錄並通過權限校驗才能訪問

下面介紹使用Nacos作爲服務註冊中心和配置中心,在服務註冊時把服務的接口信息放入服務元數據裏,其他需要監聽的服務只需要監聽服務註冊事件並取出服務實例內的接口信息即可。
定義一個枚舉進行公開權限分類PublicScopeEnum

package org.feasy.cloud.auth.config;

/**
 * API公開級別枚舉類
 */
public enum PublicScopeEnum {
    /**
     * 內部公開-只要登錄了系統 都能訪問
     */
    INTERNAL_PUBLIC,
    /**
     * 全部公開-無論有沒有登錄系統,都會公開
     */
    PUBLIC;
}

自定義註解PublicApi用來標識接口是否是開放接口

package org.feasy.cloud.auth.config;

import java.lang.annotation.*;

/**
 * API開放權限註解
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface PublicApi {
    /**
     * 公開域,默認內部公開 即:只要登錄了系統都能訪問
     */
    PublicScopeEnum scope() default PublicScopeEnum.INTERNAL_PUBLIC ;
}

自定義ApplicationApiContainer存儲服務接口信息,並根據開放權限控制類型進行分類

package org.feasy.cloud.auth.config;

import lombok.*;
import lombok.experimental.Accessors;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Pattern;

/**
 * 應用服務 API容器封裝類
 */
@Data
public class ApplicationApiContainer {
    private final String serverKey;
    private final String serverName;
    /**
     * 公開接口集合
     */
    private List<ServerApiBO> publicApis = Collections.synchronizedList(new ArrayList<>());
    /**
     * 內部公開接口集合
     */
    private List<ServerApiBO> internalPublicApis = Collections.synchronizedList(new ArrayList<>());

    /**
     * 納入權限控制的接口集合
     *
     * @param applicationContext
     */
    private List<ServerApiBO> accessApis = Collections.synchronizedList(new ArrayList<>());

    public ApplicationApiContainer(WebApplicationContext applicationContext, String serverKey, String serverName) {
        this.serverKey = serverKey;
        this.serverName = serverName;
        this.initThisApis(applicationContext);
    }

    private void initThisApis(WebApplicationContext applicationContext) {
        // 取出所有的Mapping
        RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
        // 獲取url與類和方法的對應信息
        Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
        // 遍歷服務接口信息,篩選符合條件的數據
        map.forEach((mappingInfo, handlerMethod) -> {
            // 類
            Class<?> controllerClass = handlerMethod.getBeanType();
            // 包路徑
            String classPackage = controllerClass.getName();
            if (verifyClassPackageHasProperties(classPackage, "org.feasy.cloud.**.controller")) {
                // 方法
                Method method = handlerMethod.getMethod();
                // 獲取方法請求類型
                String[] methodTypes=this.getMethodTypes(mappingInfo);
                // 獲取方法路徑
                String[] methodPaths = mappingInfo.getPatternsCondition().getPatterns().toArray(new String[]{});
                // 生成數據
                List<ServerApiBO> serverApiBOS = this.builderServerApiBO(
                        controllerClass.isAnnotationPresent(RequestMapping.class) ? controllerClass.getAnnotation(RequestMapping.class).value()[0] : controllerClass.getSimpleName(),
                        methodTypes,
                        methodPaths
                );
                // 查看類上是否包含@PublicApi註解
                if (controllerClass.isAnnotationPresent(PublicApi.class) || method.isAnnotationPresent(PublicApi.class)) {
                    PublicApi publicApi = controllerClass.isAnnotationPresent(PublicApi.class) ? controllerClass.getAnnotation(PublicApi.class) : method.getAnnotation(PublicApi.class);
                    if (publicApi.scope() == PublicScopeEnum.PUBLIC) {
                        this.publicApis.addAll(serverApiBOS);
                    } else {
                        this.internalPublicApis.addAll(serverApiBOS);
                    }
                } else {
                    this.accessApis.addAll(serverApiBOS);
                }
            }
        });
    }

    /**
     * 生成一個ServerApiBO對象
     */
    private List<ServerApiBO> builderServerApiBO(String moduleKey, String[] methodTypes, String[] methodPaths) {
        List<ServerApiBO> serverApiBOS = new ArrayList<>();
        if (methodTypes.length <= 0) {
            serverApiBOS.add(new ServerApiBO()
                    .setModuleKey(moduleKey)
                    .setModuleName(moduleKey)
                    .setApiType("POST")
                    .setApiPath(this.serverKey + methodPaths[0])
            );
            serverApiBOS.add(new ServerApiBO()
                    .setModuleKey(moduleKey)
                    .setModuleName(moduleKey)
                    .setApiType("PUT")
                    .setApiPath(this.serverKey + methodPaths[0])
            );
            serverApiBOS.add(new ServerApiBO()
                    .setModuleKey(moduleKey)
                    .setModuleName(moduleKey)
                    .setApiType("GET")
                    .setApiPath(this.serverKey + methodPaths[0])
            );
            serverApiBOS.add(new ServerApiBO()
                    .setModuleKey(moduleKey)
                    .setModuleName(moduleKey)
                    .setApiType("DELETE")
                    .setApiPath(this.serverKey + methodPaths[0])
            );
        } else {
            serverApiBOS.add(new ServerApiBO()
                    .setModuleKey(moduleKey)
                    .setModuleName(moduleKey)
                    .setApiType(methodTypes[0])
                    .setApiPath(this.serverKey + methodPaths[0])
            );
        }
        return serverApiBOS;
    }

    private String[] getMethodTypes(RequestMappingInfo mappingInfo){
        List<RequestMethod> requestMethodList = new ArrayList<>(mappingInfo.getMethodsCondition().getMethods());
        String[] methodTypes=new String[requestMethodList.size()];
        for (int i=0;i<requestMethodList.size();i++){
            methodTypes[i]=requestMethodList.get(i).toString();
        }
        return methodTypes;
    }
    /**
     * 驗證包路徑
     *
     * @param classPackage 需要驗證的包路徑
     * @param scanPackages 驗證條件的包路徑,可以傳入多個
     * @return 驗證結果,只要有一個條件符合,條件就會成立並返回True
     */
    private static boolean verifyClassPackageHasProperties(String classPackage, String... scanPackages) {
        for (String scanPackage : scanPackages) {
            if (Pattern.matches(buildRegexPackage(scanPackage), classPackage)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 轉換驗證條件,使其支持正則驗證
     *
     * @param scanPackage 驗證條件包路徑
     * @return 驗證條件正則
     */
    private static String buildRegexPackage(String scanPackage) {
        return scanPackage.replace("**", "[\\w]*") + ".[\\w]*";
    }

    @Data
    @EqualsAndHashCode(callSuper = false)
    @Accessors(chain = true)
    class ServerApiBO {
        private String id;
        private String moduleKey;
        private String moduleName;
        private String apiName;
        private String apiPath;
        private String apiType;
    }
}

自定義NacosDiscoveryClientConfiguration配置類修改元數據

package org.feasy.cloud.auth.config;


import com.alibaba.cloud.nacos.ConditionalOnNacosDiscoveryEnabled;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.discovery.NacosWatch;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.feasy.cloud.redis.conf.FastJsonRedisSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.CommonsClientAutoConfiguration;
import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.WebApplicationContext;

import java.text.SimpleDateFormat;
import java.util.*;

/**
 * nacos客戶端註冊至服務端時,更改服務詳情中的元數據
 */
@Slf4j
@Configuration
@ConditionalOnNacosDiscoveryEnabled
@AutoConfigureBefore({SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class})
public class NacosDiscoveryClientConfiguration {
    @Value("${spring.application.name}")
    private String applicationName;

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(value = {"spring.cloud.nacos.discovery.watch.enabled"}, matchIfMissing = true)
    public NacosWatch nacosWatch(NacosDiscoveryProperties nacosDiscoveryProperties, WebApplicationContext webApplicationContext) {
        //更改服務詳情中的元數據,增加服務註冊時間
        nacosDiscoveryProperties.getMetadata().put("startup.time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        nacosDiscoveryProperties.getMetadata().put("apis", JSONObject.toJSONString(new ApplicationApiContainer(webApplicationContext,applicationName,applicationName)));
        return new NacosWatch(nacosDiscoveryProperties);
    }

}

啓動服務註冊到Nacos註冊中心查看服務元數據:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章