Dragão agachado e fênix em microsserviços

O gateway Springcloud e os nacos são familiares a todos. O que é, o que é útil, a princípio, pesquisei muito na Internet, o autor não vai desperdiçar caneta e tinta aqui. Este artigo foca na prática e compartilha com os convidados masculinos e femininos como o gateway + nacos foi implementado no último projeto do autor. O conteúdo é o seguinte:

  • filtragem de lista de permissões
  • Armazenamento e verificação de token do usuário
  • roteamento dinâmico

img-1660828044017883e625eef89cf289af4d2f5ca2deb71.jpg

A pior coisa que fiz quando criança foi querer crescer logo

filtragem de lista de permissões

No último projeto do autor, algumas interfaces não precisam verificar o status de login do usuário, por isso é necessário filtrar a interface, caso o usuário acesse essas interfaces que não requerem verificação, elas serão liberadas diretamente.

Ideia: personalize a implementação do filtro e determine se o URL solicitado está na lista de permissões do filtro. Se estiver, deixe para lá; caso contrário, verifique se o status de login do usuário expirou. A lista de permissões não deve ser codificada no código ou arquivo de configuração, mas deve ser armazenada no banco de dados ou no centro de configuração. Como o projeto geral é uma arquitetura de microsserviço, o nacos é usado como meio de armazenamento e, com atualização dinâmica, também pode perceber alterações na lista branca em tempo real.

O primeiro passo: abstrair o arquivo de configuração que precisa ser monitorado nos nacos, e o arquivo de configuração específico precisa implementar esta classe abstrata:

public abstract class AbstractNacosConfig {

    // 配置文件变更时的回调函数
    public abstract void onReceived(String content);
    
    // 配置文件标识 
    public abstract String getDataId();
    
    // 配置文件所在组
    public String getGroup(){
        return "DEFAULT_GROUP";
    }

}
复制代码

Projete entidades da lista de permissões:

@Data
public class WhiteList {
    // 服务名
    private String serviceName;
    // 该服务下所有不需要校验的接口
    private List<String> urls;

}
复制代码

Após a criação da entidade, você pode criar um novo arquivo de configuração com dataId como white_list em nacos. O autor usa a estrutura kv no design, a chave representa o nome do serviço, o valor são todas as interfaces sob o serviço que não requerem verificação, o grupo é DEFAULT_GROUP, o formato é json e os campos correspondem a WhiteList:

[{"serviceName":"服务名","urls":["接口 url"]}]
复制代码

Após a conclusão do acima, a classe AbstractNacosConfig pode ser implementada:

// 该类在 ioc 容器中的名称就是配置文件在 nacos 中的 dataId
@Component(WhiteListConfig.dataId)
public class WhiteListConfig extends AbstractNacosConfig {

    // key 是服务名,value 是该服务下不需要校验的接口。涉及到并发,因此采用写时复制的 map 结构
    private Map<String,List<String>> whiteListMap = new CopyOnWriteMap<>();
    // 和 nacos 的 dataId 对应上
    static final String dataId = "white_list";


    @Override
    public void onReceived(String content) {
    
        // 将 json 格式转为 WhiteList 集合
        List<WhiteList> whiteLists = JSON.parseArray(content, WhiteList.class);
        
        Map<String, List<String>> listMap = whiteLists.stream().collect(Collectors.toMap(WhiteList::getServiceName, WhiteList::getUrls));
        
        // 原有的白名单集合
        Set<String> keySet = whiteListMap.keySet();
        // 修改后的白名单集合
        Set<String> newKeySet = listMap.keySet();
        
        List<String> keys = new ArrayList<>(newKeySet);
        // 被移除掉的白名单集合
        List<String> removeKeys = new ArrayList<>();

        for (String key : keySet) {

            if (!newKeySet.contains(key)){
                removeKeys.add(key);
            }
        }

        for (String removeKey : removeKeys) {
            whiteListMap.remove(removeKey);
        }

        for (String key : keys) {
            whiteListMap.put(key,listMap.get(key));
        }

    }

    @Override
    public String getDataId() {
        return dataId;
    }
}
复制代码

Etapa 2: crie um listener para monitorar as alterações no arquivo de configuração no nacos, conforme a seguir:


@Component
public class NacosConfigListener {

    @Autowired
    private NacosConfigManager nacosConfigManager;

    // 通过依赖注入,获取 AbstractNacosConfig 的全部实现及 bean 名称
    @Autowired
    private Map<String, AbstractNacosConfig> nacosConfigMap;

    // 在该类的初始化方法中,完成监听器的注册逻辑
    @PostConstruct
    public void init() throws Exception {

      Set<String> dataIds = nacosConfigMap.keySet();
        // 遍历集合,为每个需要监听的文件设置监听器
        for (String dataId : dataIds) {

            AbstractNacosConfig abstractNacosConfig = nacosConfigMap.get(dataId);

            String content = nacosConfigManager.getConfigService().getConfigAndSignListener(dataId, abstractNacosConfig.getGroup(), 3000,
                    new AbstractListener() {
                        @Override
                        public void receiveConfigInfo(String configInfo) {
                            // 配置变更后,调用 onReceived 方法
                            abstractNacosConfig.onReceived(configInfo);
                        }
                    });
            if (content != null) {
                abstractNacosConfig.onReceived(content);
            }

        }
        


    }

}
复制代码

Etapa 3: Implemente a interface GlobalFilter para concluir a filtragem da lista de permissões:

@Component
public class TokenCheckFilter implements GlobalFilter, Ordered {

    // UserTokenRepository 负责用户 token 的存储和校验,读者可以根据项目实际情况进行实现
    @Autowired
    private UserTokenRepository userTokenRepository;

    @Autowired
    private WhiteListConfig whiteListConfig;

    private static final PathMatcher PATH_MATCHER = new AntPathMatcher();
    

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 由 RouteToRequestUrlFilter 放入
        URI url = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);

        if (url == null){
            return chain.filter(exchange);
        }
        
        // 请求的服务名,也就是
        String host = url.getHost(); 
        // 获取白名单列表
        Map<String, List<String>> whiteListMap = whiteListConfig.getWhiteListMap();
        // 请求的路径
        String path = url.getPath();

        if (whiteListMap != null){

            List<String> whiteList = whiteListMap.get(host);

            if (!CollUtil.isEmpty(whiteList)){
                for (String white : whiteList) {
                    // 如果下面的 if 为 true,说明请求的是白名单中的接口,直接放行即可
                    if (StrUtil.isNotEmpty(white) && PATH_MATCHER.match(white,path)){
                        return chain.filter(exchange);
                    }
                }
            }

        }
        
        // 后面的逻辑就是校验用户 token 是否有效,根据项目情况实现即可
        
    }

    // 定义该 filter 的执行顺序,该 filter 需要排在 RouteToRequestUrlFilter 的后面
    @Override
    public int getOrder() {
        return 10050;
    }
}
复制代码

Armazenamento e verificação de token do usuário

用户 token 的校验应该是每个网关都要完成的使命,而存储则不一定。有的项目的 token 存储可能是在用户服务中,网关只是对请求中携带的 token 进行校验而已。笔者也想过这种方案,但被否决掉了。因为这种方案会在无形中将用户服务和网关服务耦合在一起。比如,用户服务在后续的迭代中将 token 的存储从 数据库 移到了 redis 中,那么网关也势必要跟着修改。基于这个考虑,笔者选择将 token 的存储和校验都放在网关进行实现。

token 的存储和校验并不在同一个 filter 中,校验用的是全局的 filter,而存储则不是。存储对应的 filter 应该在调用完用户登录接口之后起作用。

// 用户登录后的 filter 逻辑
@Component
public class UserLoginFilter implements GatewayFilter, Ordered {


    @Autowired
    private UserTokenRepository userTokenRepository;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange.mutate().response(storageToken(exchange)).build());
    }

    // 对响应的内容进行处理
    private ServerHttpResponseDecorator storageToken(ServerWebExchange exchange){

        ServerHttpResponse response = exchange.getResponse();

        DataBufferFactory bufferFactory = response.bufferFactory();

        ServerHttpResponseDecorator decoratorResponse = new ServerHttpResponseDecorator(response){
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {

                if(body instanceof Flux){

                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;

                    return super.writeWith(fluxBody.map(dataBuffer -> {
                        
                        byte[] content =  new byte[dataBuffer.readableByteCount()];
                        // 将登录接口的返回值读到字节数组中
                        dataBuffer.read(content);
                        // res 就是登录接口的返回值
                        String res = new String(content, Charset.forName("utf-8"));
                        // 存储用户 token
                        String token = userTokenRepository.storageToken(res);
            
                        return bufferFactory.wrap(token.getBytes());
                    }));

                }

                return super.writeWith(body);
            }
        };

        return decoratorResponse;

    }

    // 设置该 filter 的优先级为最高
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
复制代码

光自定义 filter 还不够,还需要配合 GatewayFilterFactory 才能用上:

    @Bean
    public GatewayFilterFactory<Object> userLoginGatewayFilterFactory(UserLoginFilter userLoginFilter){
        return new GatewayFilterFactory<Object>(){

            @Override
            public Class<Object> getConfigClass() {
                return Object.class;
            }

            @Override
            public Object newConfig() {
                return new Object();
            }

            @Override
            public GatewayFilter apply(Object config) {
                // 将登录接口的 filter 返回
                return userLoginFilter;
            }

            @Override
            public String name() {
                return "userLoginFilter";
            }
        };
    }
复制代码

到这,代码层面的配置是完成了,但还不够,因为还少了动态路由的支持。

img-1660998070023df430c3dd214dd55055cdf2f5fab632a.jpg

有时真不想跟人呆一起

动态路由

路由不难理解,就是根据路由规则(断言、过滤器)将请求转发到目标服务中。动态路由指的就是路由规则是动态变化的,网关可以实时感知到变化并能够应用上最新的路由规则。有了第一 part 的铺垫,各位读者肯定也知道路由规则应该存哪了。

首先在 nacos 上新建一个 dataId 为 dynamic_route 的配置文件,格式依然为 json,字段和 RouteDefinition 对应:

[{     "predicates":[{"name":"Path","args":{"path":"/user/login"}}],
        "uri":"lb://user",
        "filters":[{"name":"userLoginFilter"}]
 }]
复制代码

配置文件创建后,就可以实现 AbstractNacosConfig 类:


@Component(DynamicRouteConfig.dataId)
public class DynamicRouteConfig extends AbstractNacosConfig{

    @Getter
    private List<RouteDefinition> routeDefinitions;

    static final String dataId = "dynamic_route";

    @Override
    public void onReceived(String content) {
        // 将 json 串转成 RouteDefinition 对象集合
        List<RouteDefinition> definitionList = JSON.parseArray(content, RouteDefinition.class);
        this.routeDefinitions = definitionList;
    }

    @Override
    public String getDataId() {
        return dataId;
    }


}
复制代码

接着可以实现 RouteDefinitionLocator 接口,并放到 ioc 容器中:


    @Autowired
    private DynamicRouteConfig dynamicRouteConfig;

    @Bean
    public RouteDefinitionLocator dynamicRouteRouteDefinitionRepository(){

        return () -> {
            List<RouteDefinition> list= dynamicRouteConfig.getRouteDefinitions();
            return Flux.fromIterable(list);
        };

    }
复制代码

这几步完成后,用户 token 的存储和动态路由就都能实现了。

img-1661003701847759745ffcb4de494cf6dcb9f0169d12c.jpg

如果能牵到你的手,我就比其他男人更接近成功

O acima é tudo o que fiz para compartilhar. O autor não analisou o princípio da implementação, pois envolverá o código-fonte, e o código-fonte pode persuadir alguns leitores. O autor não sabe escrever artigos sobre análise de código fonte, se os leitores tiverem alguma boa sugestão, podem colocar na área de comentários, e eu as adotarei de acordo com a situação. Se há leitores que gostam de compartilhar e escrever, eles também são bem-vindos para se comunicar comigo, e podemos progredir juntos.

Finalizado, polvilhe flores manualmente! !

おすすめ

転載: juejin.im/post/7134159864267276319