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
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";
}
};
}
复制代码
到这,代码层面的配置是完成了,但还不够,因为还少了动态路由的支持。
有时真不想跟人呆一起
动态路由
路由不难理解,就是根据路由规则(断言、过滤器)将请求转发到目标服务中。动态路由指的就是路由规则是动态变化的,网关可以实时感知到变化并能够应用上最新的路由规则。有了第一 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 的存储和动态路由就都能实现了。
如果能牵到你的手,我就比其他男人更接近成功
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! !