经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Spring » 查看文章
springcloud Zuul动态路由的实现
来源:jb51  时间:2018/11/5 10:46:17  对本文有异议

前言

Zuul 是Netflix 提供的一个开源组件,致力于在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。也有很多公司使用它来作为网关的重要组成部分,碰巧今年公司的架构组决定自研一个网关产品,集动态路由,动态权限,限流配额等功能为一体,为其他部门的项目提供统一的外网调用管理,最终形成产品(这方面阿里其实已经有成熟的网关产品了,但是不太适用于个性化的配置,也没有集成权限和限流降级)。

不过这里并不想介绍整个网关的架构,而是想着重于讨论其中的一个关键点,并且也是经常在交流群中听人说起的:动态路由怎么做?

再阐释什么是动态路由之前,需要介绍一下架构的设计。

传统互联网架构图

 

上图是没有网关参与的一个最典型的互联网架构(本文中统一使用book代表应用实例,即真正提供服务的一个业务系统)

加入eureka的架构图

 

book注册到eureka注册中心中,zuul本身也连接着同一个eureka,可以拉取book众多实例的列表。服务中心的注册发现一直是值得推崇的一种方式,但是不适用与网关产品。因为我们的网关是面向众多的其他部门的已有或是异构架构的系统,不应该强求其他系统都使用eureka,这样是有侵入性的设计。

最终架构图

 

要强调的一点是,gateway最终也会部署多个实例,达到分布式的效果,在架构图中没有画出,请大家自行脑补。

本博客的示例使用最后一章架构图为例,带来动态路由的实现方式,会有具体的代码。

动态路由

动态路由需要达到可持久化配置,动态刷新的效果。如架构图所示,不仅要能满足从spring的配置文件properties加载路由信息,还需要从数据库加载我们的配置。另外一点是,路由信息在容器启动时就已经加载进入了内存,我们希望配置完成后,实施发布,动态刷新内存中的路由信息,达到不停机维护路由信息的效果。

zuul–HelloWorldDemo

项目结构

  1. <groupId>com.sinosoft</groupId>
  2. <artifactId>zuul-gateway-demo</artifactId>
  3. <packaging>pom</packaging>
  4. <version>1.0</version>
  5.  
  6. <parent>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-parent</artifactId>
  9. <version>1.5.2.RELEASE</version>
  10. </parent>
  11.  
  12. <modules>
  13. <module>gateway</module>
  14. <module>book</module>
  15. </modules>
  16.  
  17. <dependencyManagement>
  18. <dependencies>
  19. <dependency>
  20. <groupId>org.springframework.cloud</groupId>
  21. <artifactId>spring-cloud-dependencies</artifactId>
  22. <version>Camden.SR6</version>
  23. <type>pom</type>
  24. <scope>import</scope>
  25. </dependency>
  26. </dependencies>
  27. </dependencyManagement>

tip:springboot-1.5.2对应的springcloud的版本需要使用Camden.SR6,一开始想专门写这个demo时,只替换了springboot的版本1.4.0->1.5.2,结果启动就报错了,最后发现是版本不兼容的锅。

gateway项目:

启动类:GatewayApplication.java

  1. @EnableZuulProxy
  2. @SpringBootApplication
  3. public class GatewayApplication {
  4.  
  5. public static void main(String[] args) {
  6. SpringApplication.run(GatewayApplication.class, args);
  7. }
  8.  
  9. }

配置:application.properties

  1. #配置在配置文件中的路由信息
  2. zuul.routes.books.url=http://localhost:8090
  3. zuul.routes.books.path=/books/**
  4. #不使用注册中心,会带来侵入性
  5. ribbon.eureka.enabled=false
  6. #网关端口
  7. server.port=8080

book项目:

启动类:BookApplication.java

  1. @RestController
  2. @SpringBootApplication
  3. public class BookApplication {
  4.  
  5. @RequestMapping(value = "/available")
  6. public String available() {
  7. System.out.println("Spring in Action");
  8. return "Spring in Action";
  9. }
  10.  
  11. @RequestMapping(value = "/checked-out")
  12. public String checkedOut() {
  13. return "Spring Boot in Action";
  14. }
  15.  
  16. public static void main(String[] args) {
  17. SpringApplication.run(BookApplication.class, args);
  18. }
  19. }

配置类:application.properties

  1. server.port=8090

测试访问:http://localhost:8080/books/available

上述demo是一个简单的静态路由,简单看下源码,zuul是怎么做到转发,路由的。

  1. @Configuration
  2. @EnableConfigurationProperties({ ZuulProperties.class })
  3. @ConditionalOnClass(ZuulServlet.class)
  4. @Import(ServerPropertiesAutoConfiguration.class)
  5. public class ZuulConfiguration {
  6.  
  7. @Autowired
  8. //zuul的配置文件,对应了application.properties中的配置信息
  9. protected ZuulProperties zuulProperties;
  10.  
  11. @Autowired
  12. protected ServerProperties server;
  13.  
  14. @Autowired(required = false)
  15. private ErrorController errorController;
  16.  
  17. @Bean
  18. public HasFeatures zuulFeature() {
  19. return HasFeatures.namedFeature("Zuul (Simple)", ZuulConfiguration.class);
  20. }
  21.  
  22. //核心类,路由定位器,最最重要
  23. @Bean
  24. @ConditionalOnMissingBean(RouteLocator.class)
  25. public RouteLocator routeLocator() {
  26. //默认配置的实现是SimpleRouteLocator.class
  27. return new SimpleRouteLocator(this.server.getServletPrefix(),
  28. this.zuulProperties);
  29. }
  30.  
  31. //zuul的控制器,负责处理链路调用
  32. @Bean
  33. public ZuulController zuulController() {
  34. return new ZuulController();
  35. }
  36.  
  37. //MVC HandlerMapping that maps incoming request paths to remote services.
  38. @Bean
  39. public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
  40. ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
  41. mapping.setErrorController(this.errorController);
  42. return mapping;
  43. }
  44.  
  45. //注册了一个路由刷新监听器,默认实现是ZuulRefreshListener.class,这个是我们动态路由的关键
  46. @Bean
  47. public ApplicationListener<ApplicationEvent> zuulRefreshRoutesListener() {
  48. return new ZuulRefreshListener();
  49. }
  50.  
  51. @Bean
  52. @ConditionalOnMissingBean(name = "zuulServlet")
  53. public ServletRegistrationBean zuulServlet() {
  54. ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(),
  55. this.zuulProperties.getServletPattern());
  56. // The whole point of exposing this servlet is to provide a route that doesn't
  57. // buffer requests.
  58. servlet.addInitParameter("buffer-requests", "false");
  59. return servlet;
  60. }
  61.  
  62. // pre filters
  63.  
  64. @Bean
  65. public ServletDetectionFilter servletDetectionFilter() {
  66. return new ServletDetectionFilter();
  67. }
  68.  
  69. @Bean
  70. public FormBodyWrapperFilter formBodyWrapperFilter() {
  71. return new FormBodyWrapperFilter();
  72. }
  73.  
  74. @Bean
  75. public DebugFilter debugFilter() {
  76. return new DebugFilter();
  77. }
  78.  
  79. @Bean
  80. public Servlet30WrapperFilter servlet30WrapperFilter() {
  81. return new Servlet30WrapperFilter();
  82. }
  83.  
  84. // post filters
  85.  
  86. @Bean
  87. public SendResponseFilter sendResponseFilter() {
  88. return new SendResponseFilter();
  89. }
  90.  
  91. @Bean
  92. public SendErrorFilter sendErrorFilter() {
  93. return new SendErrorFilter();
  94. }
  95.  
  96. @Bean
  97. public SendForwardFilter sendForwardFilter() {
  98. return new SendForwardFilter();
  99. }
  100.  
  101. @Configuration
  102. protected static class ZuulFilterConfiguration {
  103.  
  104. @Autowired
  105. private Map<String, ZuulFilter> filters;
  106.  
  107. @Bean
  108. public ZuulFilterInitializer zuulFilterInitializer() {
  109. return new ZuulFilterInitializer(this.filters);
  110. }
  111.  
  112. }
  113.  
  114. //上面提到的路由刷新监听器
  115. private static class ZuulRefreshListener
  116. implements ApplicationListener<ApplicationEvent> {
  117.  
  118. @Autowired
  119. private ZuulHandlerMapping zuulHandlerMapping;
  120.  
  121. private HeartbeatMonitor heartbeatMonitor = new HeartbeatMonitor();
  122.  
  123. @Override
  124. public void onApplicationEvent(ApplicationEvent event) {
  125. if (event instanceof ContextRefreshedEvent
  126. || event instanceof RefreshScopeRefreshedEvent
  127. || event instanceof RoutesRefreshedEvent) {
  128. //设置为脏,下一次匹配到路径时,如果发现为脏,则会去刷新路由信息
  129. this.zuulHandlerMapping.setDirty(true);
  130. }
  131. else if (event instanceof HeartbeatEvent) {
  132. if (this.heartbeatMonitor.update(((HeartbeatEvent) event).getValue())) {
  133. this.zuulHandlerMapping.setDirty(true);
  134. }
  135. }
  136. }
  137.  
  138. }
  139.  
  140. }

我们要解决动态路由的难题,第一步就得理解路由定位器的作用。

 

很失望,因为从接口关系来看,spring考虑到了路由刷新的需求,但是默认实现的SimpleRouteLocator没有实现RefreshableRouteLocator接口,看来我们只能借鉴DiscoveryClientRouteLocator去改造SimpleRouteLocator使其具备刷新能力。

  1. public interface RefreshableRouteLocator extends RouteLocator {
  2. void refresh();
  3. }

DiscoveryClientRouteLocator比SimpleRouteLocator多了两个功能,第一是从DiscoveryClient(如Eureka)发现路由信息,之前的架构图已经给大家解释清楚了,我们不想使用eureka这种侵入式的网关模块,所以忽略它,第二是实现了RefreshableRouteLocator接口,能够实现动态刷新。

对SimpleRouteLocator.class的源码加一些注释,方便大家阅读:

  1. public class SimpleRouteLocator implements RouteLocator {
  2.  
  3. //配置文件中的路由信息配置
  4. private ZuulProperties properties;
  5. //路径正则配置器,即作用于path:/books/**
  6. private PathMatcher pathMatcher = new AntPathMatcher();
  7.  
  8. private String dispatcherServletPath = "/";
  9. private String zuulServletPath;
  10.  
  11. private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();
  12.  
  13. public SimpleRouteLocator(String servletPath, ZuulProperties properties) {
  14. this.properties = properties;
  15. if (servletPath != null && StringUtils.hasText(servletPath)) {
  16. this.dispatcherServletPath = servletPath;
  17. }
  18.  
  19. this.zuulServletPath = properties.getServletPath();
  20. }
  21.  
  22. //路由定位器和其他组件的交互,是最终把定位的Routes以list的方式提供出去,核心实现
  23. @Override
  24. public List<Route> getRoutes() {
  25. if (this.routes.get() == null) {
  26. this.routes.set(locateRoutes());
  27. }
  28. List<Route> values = new ArrayList<>();
  29. for (String url : this.routes.get().keySet()) {
  30. ZuulRoute route = this.routes.get().get(url);
  31. String path = route.getPath();
  32. values.add(getRoute(route, path));
  33. }
  34. return values;
  35. }
  36.  
  37. @Override
  38. public Collection<String> getIgnoredPaths() {
  39. return this.properties.getIgnoredPatterns();
  40. }
  41.  
  42. //这个方法在网关产品中也很重要,可以根据实际路径匹配到Route来进行业务逻辑的操作,进行一些加工
  43. @Override
  44. public Route getMatchingRoute(final String path) {
  45.  
  46. if (log.isDebugEnabled()) {
  47. log.debug("Finding route for path: " + path);
  48. }
  49.  
  50. if (this.routes.get() == null) {
  51. this.routes.set(locateRoutes());
  52. }
  53.  
  54. if (log.isDebugEnabled()) {
  55. log.debug("servletPath=" + this.dispatcherServletPath);
  56. log.debug("zuulServletPath=" + this.zuulServletPath);
  57. log.debug("RequestUtils.isDispatcherServletRequest()="
  58. + RequestUtils.isDispatcherServletRequest());
  59. log.debug("RequestUtils.isZuulServletRequest()="
  60. + RequestUtils.isZuulServletRequest());
  61. }
  62.  
  63. String adjustedPath = adjustPath(path);
  64.  
  65. ZuulRoute route = null;
  66. if (!matchesIgnoredPatterns(adjustedPath)) {
  67. for (Entry<String, ZuulRoute> entry : this.routes.get().entrySet()) {
  68. String pattern = entry.getKey();
  69. log.debug("Matching pattern:" + pattern);
  70. if (this.pathMatcher.match(pattern, adjustedPath)) {
  71. route = entry.getValue();
  72. break;
  73. }
  74. }
  75. }
  76. if (log.isDebugEnabled()) {
  77. log.debug("route matched=" + route);
  78. }
  79.  
  80. return getRoute(route, adjustedPath);
  81.  
  82. }
  83.  
  84. private Route getRoute(ZuulRoute route, String path) {
  85. if (route == null) {
  86. return null;
  87. }
  88. String targetPath = path;
  89. String prefix = this.properties.getPrefix();
  90. if (path.startsWith(prefix) && this.properties.isStripPrefix()) {
  91. targetPath = path.substring(prefix.length());
  92. }
  93. if (route.isStripPrefix()) {
  94. int index = route.getPath().indexOf("*") - 1;
  95. if (index > 0) {
  96. String routePrefix = route.getPath().substring(0, index);
  97. targetPath = targetPath.replaceFirst(routePrefix, "");
  98. prefix = prefix + routePrefix;
  99. }
  100. }
  101. Boolean retryable = this.properties.getRetryable();
  102. if (route.getRetryable() != null) {
  103. retryable = route.getRetryable();
  104. }
  105. return new Route(route.getId(), targetPath, route.getLocation(), prefix,
  106. retryable,
  107. route.isCustomSensitiveHeaders() ? route.getSensitiveHeaders() : null);
  108. }
  109.  
  110. //注意这个类并没有实现refresh接口,但是却提供了一个protected级别的方法,旨在让子类不需要重复维护一个private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();也可以达到刷新的效果
  111. protected void doRefresh() {
  112. this.routes.set(locateRoutes());
  113. }
  114.  
  115.  
  116. //具体就是在这儿定位路由信息的,我们之后从数据库加载路由信息,主要也是从这儿改写
  117. /**
  118. * Compute a map of path pattern to route. The default is just a static map from the
  119. * {@link ZuulProperties}, but subclasses can add dynamic calculations.
  120. */
  121. protected Map<String, ZuulRoute> locateRoutes() {
  122. LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
  123. for (ZuulRoute route : this.properties.getRoutes().values()) {
  124. routesMap.put(route.getPath(), route);
  125. }
  126. return routesMap;
  127. }
  128.  
  129. protected boolean matchesIgnoredPatterns(String path) {
  130. for (String pattern : this.properties.getIgnoredPatterns()) {
  131. log.debug("Matching ignored pattern:" + pattern);
  132. if (this.pathMatcher.match(pattern, path)) {
  133. log.debug("Path " + path + " matches ignored pattern " + pattern);
  134. return true;
  135. }
  136. }
  137. return false;
  138. }
  139.  
  140. private String adjustPath(final String path) {
  141. String adjustedPath = path;
  142.  
  143. if (RequestUtils.isDispatcherServletRequest()
  144. && StringUtils.hasText(this.dispatcherServletPath)) {
  145. if (!this.dispatcherServletPath.equals("/")) {
  146. adjustedPath = path.substring(this.dispatcherServletPath.length());
  147. log.debug("Stripped dispatcherServletPath");
  148. }
  149. }
  150. else if (RequestUtils.isZuulServletRequest()) {
  151. if (StringUtils.hasText(this.zuulServletPath)
  152. && !this.zuulServletPath.equals("/")) {
  153. adjustedPath = path.substring(this.zuulServletPath.length());
  154. log.debug("Stripped zuulServletPath");
  155. }
  156. }
  157. else {
  158. // do nothing
  159. }
  160.  
  161. log.debug("adjustedPath=" + path);
  162. return adjustedPath;
  163. }
  164.  
  165. }

重写过后的自定义路由定位器如下:

  1. public class CustomRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator{
  2.  
  3. public final static Logger logger = LoggerFactory.getLogger(CustomRouteLocator.class);
  4.  
  5. private JdbcTemplate jdbcTemplate;
  6.  
  7. private ZuulProperties properties;
  8.  
  9. public void setJdbcTemplate(JdbcTemplate jdbcTemplate){
  10. this.jdbcTemplate = jdbcTemplate;
  11. }
  12.  
  13. public CustomRouteLocator(String servletPath, ZuulProperties properties) {
  14. super(servletPath, properties);
  15. this.properties = properties;
  16. logger.info("servletPath:{}",servletPath);
  17. }
  18.  
  19. //父类已经提供了这个方法,这里写出来只是为了说明这一个方法很重要!!!
  20. // @Override
  21. // protected void doRefresh() {
  22. // super.doRefresh();
  23. // }
  24.  
  25.  
  26. @Override
  27. public void refresh() {
  28. doRefresh();
  29. }
  30.  
  31. @Override
  32. protected Map<String, ZuulRoute> locateRoutes() {
  33. LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
  34. //从application.properties中加载路由信息
  35. routesMap.putAll(super.locateRoutes());
  36. //从db中加载路由信息
  37. routesMap.putAll(locateRoutesFromDB());
  38. //优化一下配置
  39. LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>();
  40. for (Map.Entry<String, ZuulRoute> entry : routesMap.entrySet()) {
  41. String path = entry.getKey();
  42. // Prepend with slash if not already present.
  43. if (!path.startsWith("/")) {
  44. path = "/" + path;
  45. }
  46. if (StringUtils.hasText(this.properties.getPrefix())) {
  47. path = this.properties.getPrefix() + path;
  48. if (!path.startsWith("/")) {
  49. path = "/" + path;
  50. }
  51. }
  52. values.put(path, entry.getValue());
  53. }
  54. return values;
  55. }
  56.  
  57. private Map<String, ZuulRoute> locateRoutesFromDB(){
  58. Map<String, ZuulRoute> routes = new LinkedHashMap<>();
  59. List<ZuulRouteVO> results = jdbcTemplate.query("select * from gateway_api_define where enabled = true ",new BeanPropertyRowMapper<>(ZuulRouteVO.class));
  60. for (ZuulRouteVO result : results) {
  61. if(org.apache.commons.lang3.StringUtils.isBlank(result.getPath()) || org.apache.commons.lang3.StringUtils.isBlank(result.getUrl()) ){
  62. continue;
  63. }
  64. ZuulRoute zuulRoute = new ZuulRoute();
  65. try {
  66. org.springframework.beans.BeanUtils.copyProperties(result,zuulRoute);
  67. } catch (Exception e) {
  68. logger.error("=============load zuul route info from db with error==============",e);
  69. }
  70. routes.put(zuulRoute.getPath(),zuulRoute);
  71. }
  72. return routes;
  73. }
  74.  
  75. public static class ZuulRouteVO {
  76.  
  77. /**
  78. * The ID of the route (the same as its map key by default).
  79. */
  80. private String id;
  81.  
  82. /**
  83. * The path (pattern) for the route, e.g. /foo/**.
  84. */
  85. private String path;
  86.  
  87. /**
  88. * The service ID (if any) to map to this route. You can specify a physical URL or
  89. * a service, but not both.
  90. */
  91. private String serviceId;
  92.  
  93. /**
  94. * A full physical URL to map to the route. An alternative is to use a service ID
  95. * and service discovery to find the physical address.
  96. */
  97. private String url;
  98.  
  99. /**
  100. * Flag to determine whether the prefix for this route (the path, minus pattern
  101. * patcher) should be stripped before forwarding.
  102. */
  103. private boolean stripPrefix = true;
  104.  
  105. /**
  106. * Flag to indicate that this route should be retryable (if supported). Generally
  107. * retry requires a service ID and ribbon.
  108. */
  109. private Boolean retryable;
  110.  
  111. private Boolean enabled;
  112.  
  113. public String getId() {
  114. return id;
  115. }
  116.  
  117. public void setId(String id) {
  118. this.id = id;
  119. }
  120.  
  121. public String getPath() {
  122. return path;
  123. }
  124.  
  125. public void setPath(String path) {
  126. this.path = path;
  127. }
  128.  
  129. public String getServiceId() {
  130. return serviceId;
  131. }
  132.  
  133. public void setServiceId(String serviceId) {
  134. this.serviceId = serviceId;
  135. }
  136.  
  137. public String getUrl() {
  138. return url;
  139. }
  140.  
  141. public void setUrl(String url) {
  142. this.url = url;
  143. }
  144.  
  145. public boolean isStripPrefix() {
  146. return stripPrefix;
  147. }
  148.  
  149. public void setStripPrefix(boolean stripPrefix) {
  150. this.stripPrefix = stripPrefix;
  151. }
  152.  
  153. public Boolean getRetryable() {
  154. return retryable;
  155. }
  156.  
  157. public void setRetryable(Boolean retryable) {
  158. this.retryable = retryable;
  159. }
  160.  
  161. public Boolean getEnabled() {
  162. return enabled;
  163. }
  164.  
  165. public void setEnabled(Boolean enabled) {
  166. this.enabled = enabled;
  167. }
  168. }
  169. }

配置这个自定义的路由定位器:

  1. @Configuration
  2. public class CustomZuulConfig {
  3.  
  4. @Autowired
  5. ZuulProperties zuulProperties;
  6. @Autowired
  7. ServerProperties server;
  8. @Autowired
  9. JdbcTemplate jdbcTemplate;
  10.  
  11. @Bean
  12. public CustomRouteLocator routeLocator() {
  13. CustomRouteLocator routeLocator = new CustomRouteLocator(this.server.getServletPrefix(), this.zuulProperties);
  14. routeLocator.setJdbcTemplate(jdbcTemplate);
  15. return routeLocator;
  16. }
  17.  
  18. }

现在容器启动时,就可以从数据库和配置文件中一起加载路由信息了,离动态路由还差最后一步,就是实时刷新,前面已经说过了,默认的ZuulConfigure已经配置了事件监听器,我们只需要发送一个事件就可以实现刷新了。

  1. public class RefreshRouteService {
  2.  
  3. @Autowired
  4. ApplicationEventPublisher publisher;
  5.  
  6. @Autowired
  7. RouteLocator routeLocator;
  8.  
  9. public void refreshRoute() {
  10. RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
  11. publisher.publishEvent(routesRefreshedEvent);
  12. }
  13.  
  14. }

具体的刷新流程其实就是从数据库重新加载了一遍,有人可能会问,为什么不自己是手动重新加载Locator.dorefresh?非要用事件去刷新。这牵扯到内部的zuul内部组件的工作流程,不仅仅是Locator本身的一个变量,具体想要了解的还得去看源码。

到这儿我们就实现了动态路由了,所以的实例代码和建表语句我会放到github上,下载的时候记得给我star QAQ !!!

链接:https://github.com/lexburner/zuul-gateway-demo

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号