系列文章目录

如果你看到了这里,那么接下来你将会认识Dubbo3的诞生将如何引领微服务领域更进一步,从而迈入云原生的领域,这当然不仅仅是Dubbo3,之前也介绍了Java生态另外一个云原生领域的技术Quarkus等技术,而本文内容侧重点去介绍Dubbo3迈向云原生
的技术分析和探索,如果有不正确的地方,还需要大家多多指正。


文章目录

  • 系列文章目录
    • 通过令牌进行服务验证
      • 使用场景
      • 主要原理
      • 运作流程和实现原理
        • 服务提供者
        • 注册中心
        • 服务消费者
        • 服务提供者
      • 配置方式(服务提供者)
        • 服务级别
          • 随机token令牌,使用UUID生成
            • xml配置模式进行控制
            • SpringBoot的配置模式进行控制
          • 固定token令牌,相当于密码
            • xml配置模式进行控制
            • SpringBoot的配置模式进行控制
        • 接口类级别
          • 随机token令牌,使用UUID生成
          • 固定token令牌,相当于密码
      • 配置方式(服务消费者)
        • 注意要点
      • 实际案例
        • 建立API接口
        • 建立服务端的案例
        • 建立消费端的案例
          • 注册中心模式进行调用
          • 直连模式进行调用
          • 出现了报错!
            • consumer incorrect token is null
        • 建立服务端的案例2
          • 直连模式进行调用
    • 通过服务鉴权控制调用
      • 服务鉴权-特性说明
        • 鉴权服务中心
        • 总体流程图
        • 使用场景
          • 使用方式
            • 接入方式
        • 实现案例
          • 定义鉴权服务中心服务
            • 建立Dubbo3的服务容器作为鉴权服务实现
            • 数据模型
        • 定义对应的实现鉴权中心鉴权匹配实现类
          • 定义Hutools的数据源DB
          • 实现查询数据库的Hutool操作
          • 实现传递过来的数据库的Hutool操作
        • 定义容器的dubbo.properties
            • 服务提供端
            • 服务提供端-建立服务鉴权过滤器
            • 建立dubbo.properties配置信息读取相关的鉴权服务的地址
            • 服务消费端
          • 模拟远程调用

通过令牌进行服务验证

令牌Token的验证方式主要通过客户端和服务端的令牌验证定义并且通过注册中心层面进行维护和存储和管理的实现机制

使用场景

在一定程度上实现客户端和服务端的可信鉴权,避免任意客户端都可以访问,降低出现安全问题的风险。

主要原理

通过令牌验证在注册中心控制权限,以决定要不要下发令牌给消费者, 可以防止消费者绕过注册中心访问提供者, 另外通过注册中心可灵活改变授权方式,而不需修改或升级提供者。如下图所示。

运作流程和实现原理

服务提供者

  1. 创建对应的令牌Token,通过我们定义的对应token配置,进行读取的参数信息从而进行相关的Generate Token。

注册中心

  1. 接收到了服务提供者传递上报过来的token值,作为该对应服务接口或者整个服务提供者的token值进行管理维护。
  2. 针对于相关的传递过来的token值进行校验和核对工作之后,没有问题则会进行存储到注册中心。

服务消费者

  1. 服务消费者可以从注册中心上面获取token令牌值
  2. 服务消费者会将获取到的token数据值,伴随着调用接口的同时传递给服务提供者。

服务提供者

  1. 服务提供者接收到了对应的服务消费者传递过来的token数据值,进行校验和核对是否属于我们认可的token值,从而实现了控制制定我们服务体系内部的消费者的调用请求。如果不一致则直接会返回失败。

配置方式(服务提供者)

配置对应的token值的范围有几种方式,我们常用的token配置主要有服务级别和接口级别等。

令牌验证,为空表示不开启,如果为true,表示随机生成动态令牌,否则使用静态令牌,令牌的作用是防止消费者绕过注册中心直接访问,保证注册中心的授权功能有效,如果使用点对点调用,需关闭令牌功能

服务级别

可以设置对应的整个服务应用级别的配置,但是优先级会被接口级别的覆盖,可以作为全局的默认值所使用。

随机token令牌,使用UUID生成

该参数接收两种类型值:boolean类型-True则生成UUID随机令牌,若为String则自定义令牌。

xml配置模式进行控制
<dubbo:provider token="true" />
SpringBoot的配置模式进行控制

使用 Spring Boot 减少非必要配置,结合 Annotation 与 application.properties/application.yml 开发 Dubbo 应用

dubbo.provider.token=true

或者

dubbo:provider:token: true

使用这种方式的安全级别好一些,因为每次生产的都是uuid,无规律话,不容易被第三方客户端进行破解从而进行调用。

固定token令牌,相当于密码

定义了全局provider的token数据uuid模式,对所有的接口和服务实现均起作用!定义了全局provider的token数据-123456,对所有的接口和服务实现均起作用!

xml配置模式进行控制
<dubbo:provider token="123456" />
SpringBoot的配置模式进行控制
dubbo.provider.token=123456

或者

dubbo:provider:token: 123456

接口类级别

随机token令牌,使用UUID生成

实现方式和效果与服务级别相同。

<dubbo:service interface="com.xxx.TestService" token="true" />

或者可以采用@DubboService注解中的token属性进行标识。

定义了该service接口的token数据,对该接口的所有方法实现均起作用!

固定token令牌,相当于密码
<dubbo:service interface="com.xxx.TestService" token="123456" />

或者可以采用@DubboService注解中的token属性进行标识。

定义了该service接口的token数据,对该接口的所有方法实现均起作用!

配置方式(服务消费者)

Dubbo官方并未直接暴漏对应的对于消费者端的DubboReference或者ReferenceConfig上配置token,但是通过源码可以知道,Dubbo采用隐式参数传递token,通过attachment进行携带进行传输。

RpcContext.getContext().setAttachment("token","123456"):
注意要点
  • token的配置也可以在,协议级别,使用的spring boot的starter配置中未找到协议级别如何配置。

  • 配置Token的生产者的服务,只会允许消费者通过注册中心注册后,才可以获取到对应的token数据,再消费的数据才能够访问,否则会出现出现无效token的错误。

  • 由上面的介绍,数据token是由注册中心下发拉取到的。

实际案例

建立API接口

public interface CommonRpcApi {RpcResponse<Boolean> tokenAuth(RpcRequest<String> requestParam);
}

建立服务端的案例

@DubboService(token = "token")
public class DefaultCommonRpcApi implements CommonRpcApi {@Overridepublic RpcResponse<Boolean> tokenAuth(RpcRequest<String> requestParam) {return RpcResponse.success();}
}

建立消费端的案例

注册中心模式进行调用
    @DubboReferenceCommonRpcApi commonRpcApi;@GetMapping("/token")public ResponseEntity<RpcResponse<String>> doTokenAuth(){return ResponseEntity.ok(commonRpcApi.tokenAuth(new RpcRequest(RpcContext.getContext().getAttachment("token"))));}

验证结果没有任何问题和错误异常。

直连模式进行调用

模拟非注册中心过来的外部rpc调用,用于校验token不同或者不进行token鉴权的场景!

    @DubboReference(url = "dubbo://${dubbo.address:localhost}:28081")CommonRpcApi commonRpcApi;@GetMapping("/token")public ResponseEntity<RpcResponse<String>> doTokenAuth(){return ResponseEntity.ok(commonRpcApi.tokenAuth(new RpcRequest(RpcContext.getContext().getAttachment("token"))));}
出现了报错!
org.apache.dubbo.rpc.RpcException: Invalid token! Forbid invoke remote service interface com.dubbo.shopping.api.oss.CommonRpcApi method tokenAuth() from consumer 192.168.1.104 to provider 192.168.1.104, consumer incorrect token is null
consumer incorrect token is null

出现了不一致的问题在,当存在这种场景我们很难捕捉到对应的uuid模式的token值,那么我们可以指定token值进行测试效果。

建立服务端的案例2

@DubboService(token = "123456")
public class DefaultCommonRpcApi implements CommonRpcApi {@Overridepublic RpcResponse<Boolean> tokenAuth(RpcRequest<String> requestParam) {return RpcResponse.success();}
}
直连模式进行调用

我们手动注入token进行控制校验模式

    @DubboReference(url = "dubbo://${dubbo.address:localhost}:28081")CommonRpcApi commonRpcApi;@GetMapping("/token")public ResponseEntity<RpcResponse<String>> doTokenAuth(){RpcContext.getContext().setAttachment("token","123456");return ResponseEntity.ok(commonRpcApi.tokenAuth(new RpcRequest(RpcContext.getContext().getAttachment("token"))));}

发现调用结果又变的正常了!


通过服务鉴权控制调用

基于上面的【通过令牌进行服务验证】的控制实现,对于安全性而言还是缺乏了机动性、可配置、灵活性等。所以接下来引入通过了【服务鉴权控制】从而增加安全性和机动性以及可配置化等功能实现。

服务鉴权-特性说明

Dubbo3服务鉴权类似支付之类的对安全性敏感的业务可能会有限制匿名调用的需求。在加固安全性方面,2.7.5引入了基于AK/SK机制的认证鉴权机制,并且引入了鉴权服务中。

鉴权服务中心

主要原理是消费端在请求需要鉴权的服务时,会通过SK、请求元数据、时间戳、参数等信息来生成对应的请求签名,通过Dubbo3的Attachment机制携带到对端进行验签,验签通过才进行业务逻辑处理。如下图所示:

总体流程图

使用场景

针对于调用方进行相关的服务调用鉴权

使用方式
接入方式
  1. 使用者需要在微服务站点上填写自己的应用信息,并为该应用生成唯一的证书凭证。

  2. 在管理站点上提交工单,申请某个敏感业务服务的使用权限,并由对应业务管理者进行审批,审批通过之后,会生成对应的 AK/SK到鉴权服务中心。

  3. 导入该证书到对应的应用下,并且进行配置。配置方式也十分简单,以注解方式为例:

实现案例

定义鉴权服务中心服务
建立Dubbo3的服务容器作为鉴权服务实现

Undertow容器处理功能,用于接收对应的Http鉴权请求接口服务,用于处理来资源服务提供者端的Http请求。

数据模型

定义对应的实现鉴权中心鉴权匹配实现类

定义Hutools的数据源DB

在对应的resources下的config文件下建立db.setting文件,之后配置对应的数据源

#中括表示一个分组,其下面的所有属性归属于这个分组,在此分组名为ds1,也可以没有分组
[ds1]
#自定义数据源设置文件,这个文件会针对当前分组生效,用于给当前分组配置单独的数据库连接池参数,没有则使用全局的配置
driver = com.mysql.jdbc.Driver
#JDBC url,必须
url = jdbc:mysql://127.0.0.1:3306/dubbo-shopping
#用户名,必须
user = root
#密码,必须,如果密码为空,请填写 pass =
pass = root$
实现查询数据库的Hutool操作
List<Entity> authData = Db.use(dataSource).query("select * from auth_data");
if(CollectionUtil.isNotEmpty(authData)){Entity entity = authData.get(0);String ak = entity.getStr("ak");String sk = entity.getStr("sk");
}
实现传递过来的数据库的Hutool操作
@Data
@Slf4j
@NoArgsConstructor
public class AuthService {DataSource dataSource = DSFactory.get("ds1");/*** 匹配ak和sk的值* @param appCode* @param appKey* @param secretKey* @return*/public boolean matchSecretKey(String appCode,String appKey,String secretKey){log.info("appCode:{} - local-appkey:{} - local-secretKey:{}",appCode,appKey,secretKey);if(StringUtils.isEmpty(appKey)){return Boolean.FALSE;}try {List<Entity> authData = Db.use(dataSource).query("select * from auth_data where code = ?",appCode);if(CollectionUtil.isNotEmpty(authData)){Entity entity = authData.get(0);String ak = entity.getStr("ak");String sk = entity.getStr("sk");log.info("remote-appkey:{} - remote-secretKey:{}",ak,sk);if(ak.equals(ak)){if(StringUtils.isEmpty(sk)){return Boolean.FALSE;}else if(!SecureUtil.md5(sk).equals(secretKey)){return Boolean.FALSE;}}else{return Boolean.FALSE;}return Boolean.TRUE;}return Boolean.FALSE;} catch (SQLException e) {log.error("auth is error!",e);return Boolean.FALSE;}}}```
#### 鉴权服务 UndertowContainer采用Undertow容器服务机制,在之前的章节已经介绍和说明了如何实现对应的dubbo的自定义容器实现,在这里我们使用的是UndertowContainer。如果想要学习可以关注之前的文章章节。```java
package com.hyts.assemble.dubbo3.comp.container;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.NumberUtil;
import com.hyts.assemble.dubbo3.comp.auth.AuthService;
import io.undertow.Undertow;
import io.undertow.util.Headers;
import org.apache.dubbo.common.config.ConfigurationUtils;
import org.apache.dubbo.container.Container;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Collectors;public class UnderTowContainer implements Container {//定义容器端口public static final String UNDERTOW_PORT = "dubbo.undertow.port";//定义容器contextPathpublic static final String UNDERTOW_DEFAULT_PATH = "dubbo.undertow.path";private AuthService authService = new AuthService();//设置HttpHandler回调方法final Undertow server = Undertow.builder().addHttpListener(NumberUtil.parseInt(ConfigurationUtils.getProperty(UNDERTOW_PORT)), "localhost").setHandler(exchange -> {if(exchange.getRequestPath().equals(ConfigurationUtils.getProperty(UNDERTOW_DEFAULT_PATH))){String appKey = String.valueOf(exchange.getQueryParameters().get("appKey").poll());String secretKey = String.valueOf(exchange.getQueryParameters().get("secretKey").poll());String appCode = String.valueOf(exchange.getQueryParameters().get("appCode").poll());boolean result = authService.matchSecretKey(appCode,appKey,secretKey);exchange.getResponseSender().send(String.valueOf(result));}}).build();// 定义启动方法        @Overridepublic void start() {server.start();}// 定义停止方法 @Overridepublic void stop() {server.stop();}
}

定义容器的dubbo.properties

dubbo.container=spring,jetty,log4j,undertow
dubbo.undertow.path=/auth
dubbo.undertow.port=8081
服务提供端

只需要设置 service.auth 为 true,表示该服务的调用需要鉴权认证通过。param.sign为true表示需要对参数也进行校验,之前的章节的内容我们已经介绍了对应的如何建立校验功能的实现机制控制。

// (注解方式)
@DubboService(parameters = {"service.auth","true"})
public class AuthServiceImpl implements AuthService {}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:property-placeholder/><dubbo:application name="direct-consumer"/><dubbo:registry address="zookeeper://${zookeeper.address:127.0.0.1}:2181" check="false"/><dubbo:protocol name="dubbo" port="20880"/><bean id="rpcAuthSampleApi" class="com.dubbo.shopping.commodity.rpc.auth.AuthServiceImpl"/><dubbo:service id="rpcAuthSampleApiHandler" interface="com.dubbo.shopping.commodity.api.AuthService"ref="rpcAuthSampleApi" version="0.0.0" filter="auth"><dubbo:parameter key="service.auth" value="true"/>
</dubbo:service>
</beans>
服务提供端-建立服务鉴权过滤器

在之前的章节文章中介绍了对应的META-INF/dubbo下建立org.apache.dubbo.rpc.Filter文件,之后进行auth=com.dubbo.shopping.common.auth.filter.AuthFilter,之后会进行定义我们的authFilter过滤器实现类。

建立dubbo.properties配置信息读取相关的鉴权服务的地址

resources文件下建立dubbo.properties之后添加对应的内容

dubbo.auth.url=http://localhost:8081/auth

可以使用对应的配置工具进行获取配置信息。

ConfigurationUtils.getProperty("dubbo.auth.url")

定义对应的服务提供端-建立服务鉴权过滤器

    @Overridepublic Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {boolean authNeed = Boolean.valueOf(invoker.getUrl().getParameter("service.auth"));String ak = invocation.getAttachment("AK");String sk = invocation.getAttachment("SK");String authUrl = ConfigurationUtils.getProperty("dubbo.auth.url");if(authNeed){log.info("PROCESS AUTH THE INVOKE!APPKEY {} , SECRETKEY {} : authUrl:{}",ak,sk,authUrl);String result = HttpUtil.get(authUrl+"?appCode="+"dubbo-shopping"+"&appKey="+ak+"&secretKey="+sk);if(!Boolean.valueOf(result)){log.error("NOT AUTH THE INVOKE! APPKEY {} , SECRETKEY {}",ak,sk);throw new RpcException("NOT AUTH THE INVOKE!");            }}log.info("PASS AUTH THE INVOKE!APPKEY {} , SECRETKEY {}",ak,sk);
//        if(paramSign){//        }return invoker.invoke(invocation);}
服务消费端

只需要配置好对应的证书等信息即可,之后会自动地在对这些需要认证的接口发起调用前进行签名操作,通过与鉴权服务的交互,用户无需在代码中配置 AK/SK 这些敏感信息,并且在不重启应用的情况下刷新 AK/SK,达到权限动态下发的目的。

该方案目前已经提交给 Dubbo 开源社区,并且完成了基本框架的合并,除了 AK/SK 的鉴权方式之外,通过 SPI 机制支持用户可定制化的鉴权认证以及适配公司内部基础设施的密钥存储。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:property-placeholder/><dubbo:application name="direct-consumer"/><dubbo:registry address="zookeeper://${zookeeper.address:127.0.0.1}:2181" check="false"/><dubbo:provider token="true"/><dubbo:protocol name="dubbo" port="20880"/><dubbo:reference id="authSampleApi" check="false" interface="com.dubbo.shopping.commodity.api.AuthSampleApi" version="*"/>
</beans>
模拟远程调用
package com.dubbo.shopping.commodity.controller;import cn.hutool.crypto.SecureUtil;
import com.dubbo.shopping.commodity.api.AnnotationConstants;
import com.dubbo.shopping.commodity.api.AuthSampleApi;
import com.dubbo.shopping.commodity.api.CommodityQueryApi;
import com.dubbo.shopping.commodity.entity.BaseInfo;
import com.dubbo.shopping.commodity.model.CommodityQueryDTO;
import com.dubbo.shopping.model.rpc.RpcRequest;
import io.swagger.annotations.ApiOperation;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.rpc.RpcContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;/*** <p>* 商品主题信息 前端控制器* </p>** @author libo* @since 2022-05-08*/
@RestController
@RequestMapping("/commodity/base-info")
public class BaseInfoController {@AutowiredAuthSampleApi authSampleApi;@ApiOperation("权限控制调用")@RequestMapping("/auth")public ResponseEntity auth(){RpcContext.getClientAttachment().setAttachment("AK","dubbo3");RpcContext.getClientAttachment().setAttachment("SK", SecureUtil.md5("123456"));return ResponseEntity.ok(authSampleApi.executeAuth("test parameter"));}
}

既可以实现鉴权服务机制,大家还可以自己进行扩展实现。

【Dubbo3高级特性】「提升系统安全性」通过令牌进行服务验证及服务鉴权控制实战指南相关推荐

  1. [免费专栏] ATTACK安全之Android车机证书攻击场景检测「检测系统代理」

    也许每个人出生的时候都以为这世界都是为他一个人而存在的,当他发现自己错的时候,他便开始长大 少走了弯路,也就错过了风景,无论如何,感谢经历 ATTACK付费专栏长期更新,本篇最新内容请前往: [车联网 ...

  2. B站上线斯坦福最新「机器学习系统(MLSys)」全集,小伙伴有福了!

    点击上方"AI遇见机器学习",选择"星标"公众号 重磅干货,第一时间送达 来自:新智元 来源:外媒 编辑:keyu [导读]2020年秋季开始,斯坦福大学开始陆 ...

  3. win10桌面管理文件收纳_微软win10发布7月更新,三大版本同步更新,着重提升系统安全性...

    2020年7月14日,微软windows10系统发布了07累积更新,对2004版.1909版和1903版三大版本win10系统同步推送更新补丁,其中win10的2004版的07累积更新补丁为KB456 ...

  4. win10推送_win10发布5月首个更新,着重提升系统安全性,为新版本推送作铺垫

    2020年,微软windows10系统基本上每月至少发布两个更新补丁,其中3月份更新补丁最多,达到了四个.直到2020年5月12日,微软windows10系统才发布了5月份首个05更新补丁KB4556 ...

  5. 「分布式技术专题」基于Gossip协议的去中心服务

    概述 Gossip 协议可以翻译为流言协议.它是在 1987 年发表在 ACM 上的论文 <Epidemic Algorithms for Replicated Database Mainten ...

  6. 「Java分享客栈」随时用随时翻:微服务链路追踪之zipkin搭建

    前言 微服务治理方案中,链路追踪是必修课,SpringCloud的组件其实使用很简单,生产环境中真正令人头疼的往往是软件维护,接口在微服务间的调用究竟哪个环节出现了问题,哪个环节耗时较长,这都是项目上 ...

  7. Linux提升系统安全性:自动注销 TMOUT

    为了增强Linux系统的安全性,我们需要在用户输入空闲一段时间后自动注销并断开终端连接,这个操作可以设置/etc/profile文件中TMOUT值来实现,这样可以有效避免当管理员不在时其他人员对服务器 ...

  8. Linux提升系统安全性:历史命令 HISTSIZE

    在Linux中Shell环境的命令历史机制为用户提供了极大的便利,同时也给用户带来了潜在的风险.只要获得用户的命令历史文件,该用户的命令操作过程将会一览无余,如果曾经在命令行输入明文的密码,则无意中服 ...

  9. 观看自由!B站上线斯坦福最新「机器学习系统(MLSys)」全集

    https://mbd.baidu.com/newspage/data/landingsuper?context=%7B%22nid%22%3A%22news_9422904139844416602% ...

最新文章

  1. c++ std::priority_queue优先队列
  2. MEET2020 | 嘉宾已确认!李开复、倪光南等AI大咖齐聚,共话人工智能新价值新边界新格局...
  3. php $conf,$conf
  4. [luogu P2590 ZJOI2008] 树的统计 (树链剖分)
  5. qt最大化和还原实现_研究进展 | 水生所关于细菌异化型硝酸盐还原成铵与反硝化脱氮两种途径抉择的分子调控机制研究取得进展...
  6. 大二下周总结(14)
  7. python 国产_再体验国产最好的Python IDE之NovalIDE
  8. Linux系统提高编辑效率的vim工具重要知识
  9. SSH远程登陆配置sshd_config文件详解
  10. 三极管实现的锁存电路
  11. 服务器被ddos攻击,防止DDOS攻击?
  12. Python计算思维训练——数组和曲线绘制练习(三)
  13. 修改php fpm监听端口,怎样修正php fpm监听端口_后端开发
  14. 深入浅出MMC子系统
  15. 浅谈Mediator仲裁者模式
  16. 日常bug记录——mybatis传值为null
  17. [渝粤教育] 广东-国家-开放大学 21秋期末考试马克思主义基本原理概论(A)10882k1 (3)
  18. linux--shell--crontab定义运行任务
  19. hcaptcha 我是人类验证码怎么跳过怎么验证自动识别
  20. 自媒体平台为什么总在强调垂直度,这里有你想要的答案!

热门文章

  1. 计算机四级-网络工程师
  2. 超聚变服务器操作系统FusionOS与阿里云PolarDB数据库完成兼容性认证
  3. Kali Linux更新源和软件教程
  4. L298N电机驱动使用方法
  5. Web开发项目——学生选课系统
  6. 东莞这位农民工大叔,火了!
  7. Windows永久关闭defender
  8. 【USACO题库】3.2.3 Spinning Wheels纺车的轮子
  9. 炼厂322℃工艺气余热换热器设计
  10. 关于波峰焊、回流焊的适用范围