一、Spring Cloud Zuul 过滤链

1.1 工作原理

Zuul的核心逻辑是由一系列的Filter来实现的,他们能够在进行HTTP请求或者相应的时候执行相关操作。Zuul Filter的主要特性有一下几点:

  • Filter的类型:Filter的类型决定了它在Filter链中的执行顺序。路由动作发生前、路由动作发生时,路由动作发生后,也可能是路由过程发生异常时。
  • Filter的执行顺序:同一种类型的Filter可以通过filterOrder()方法来设定执行顺序
  • Filter的执行条件:Filter运行所需要的条件
  • Filter的执行效果:符合某个Filter执行条件,产生的执行效果

Zuul内部提供了一个动态读取、编译和运行这些Filter的机制。Filter之间不能直接通信,在请求线程中通过RequestContext来共享状态,它的内部是用ThreadLocal实现的。

上图描述了Zuul关于Filter的请求生命周期。

  • pre:在Zuul按照规则路由到下级服务之前执行。如果需要对请求进行预处理,比如鉴权、限流等,可在考虑在这类Filter中实现。
  • route:这类Filter是Zuul路由动作的执行者,是Http客户端构建和发送HTTP请求的地方。
  • post:这类Filter是在原服务返回结果或者异常信息发生后执行,如果需要对返回信息做一些处理,可以在此类Filter进行处理。
  • error:在整个生命周期内如果发生异常,则会进入error Filter,可以做全局异常处理

其中post Filter抛出错误分成两种情况:

1)在post Filter抛错之前,pre、route Filter没有抛错,此时会进入ZuulException的逻辑,打印堆栈信息,然后再返回status=500的Error信息

2)再post Filter跑错之前,pre、route Filter已有跑错,此时不会打印堆栈信息,直接返回status=500的error信息。

也就是说整个责任链中重点不只是post Filter,还可能是error Filter。

在实际项目中,需要子实现以上类型的Filter来对链路进行处理,根据业务的需求,选取对应生命周期的Filter来达到目的。每个Filter之间通过RequestContext(Zuul包中)类来进行通信,内部采用ThreadLocal保存每个请求的一些信息,包括请求路由,错误信息,HttpServletRequest,HttpServletResponse,这使得一些操作十分可靠,它害扩展了ConcurrentHashMap,目的是为了在处理过程中保存任何形式的信息。

1.2 Zuul中的原生Filter

Zuul Server通过@EnableZuulProxy开启之后,搭配Spring Boot Actuator,会多两个管控断点。

在配置文件中配置一下:

management:endpoints:web:exposure:include: 'routes,filters'
复制代码

1、/route:返回当前Zuul Server中已生成的映射规则,加上/details可查看明细。例如

每个路由的详细信息

2、/filters:返回当前Zuul Filter中已注册生效的Filter

从Filter的信息可以看到,所有已经注册生效的Filter的信息:Filter实现类的路径、Filter执行次序、是否被禁用、是否静态。而且很明显地可以看出Zuul内Filter的整个请求的生命流程,如下图:

Zuul中各内置的Filter:

名称 类型 次序 描述
ServletDetectionFilter pre -3 通过Spring Dispatcher检查请求是否通过
Servlet30WrapperFilter pre -2 适配HttpServletRequest为Servlet30RequestWrapper对象
FormBodyWrapperFilter pre -1 解析表单数据并为下游请求重新编码
DebugFiter pre 1 Debug路由表示
PreDecorationFilter pre 5 处理请求上下文共后续使用,设置下游相关信息头
RibbonRoutingFilter route 10 使用Ribbon、Hystrix或者嵌入式HTTP客户端发送请求
SimpleHostRoutingFilter route 100 使用Apache Httpclient转发请求
SendForwardFilter route 500 使用Servlet转发请求
SendResponseFilter post 1000 将代理请求的响应写入当前相应
SendErrorFilter error 0 如果RequestContext.getThrowable()不为空,则转发到error.path配置的路径

上表为使用@EnableZuulProxy之后安装的Filter,当使用@EnableZuulServer将会缺少PreDecorationFilter、RibbonRoutingFilter、SimpleHostRoutingFilter。这些原生的Filter可以关掉,例如:在配置文件里面配置zuul.SendErrorFilter.error.disable=true

1.3 多过滤器组成过滤链

在实际中我们不仅是只定义一个过滤器,而是多个过滤器组成过滤链来完成工作,除了Zuul的其他网关也是有这个功能

要在Zuul中自定义Filter子需要继承ZuulFilter即可。它是个抽象类,主要实现的几个方法:

  • String filterType():使用返回值定义Filter的类型,有pre、route、post、error
  • int filterOrder():使用返回值设置Filter的执行顺序
  • boolean shouldFilter():使用返回值设置Filter是否执行,即所定义Filter的开关
  • Object run():Filter里面的核心执行逻辑便需要写在该方法里面

自定义一个前置过滤器,如下:

public class CustomPreFilter extends ZuulFilter {@Overridepublic String filterType() {return "pre";}@Overridepublic int filterOrder() {return 0;}@Overridepublic boolean shouldFilter() {return true;}@Overridepublic Object run() throws ZuulException {LOG.info("This is custom pre filter...");return null;}
}
复制代码

FirstPreFilter注入到Spring Bean容器

@Configuration
public class ZuulFilterConfig {@Beanpublic CustomPreFilter customPreFilter() {return new CustomPreFilter();}
}
复制代码

然后启动分别启动eurekazuulservice-a,访问http://localhost:88/servicea/add?a=1&b=2。观察网关的日志输出

INFO 20260 --- [ XNIO-1 task-1] c.m.better.zuul.filter.CustomPreFilter : This is custom pre filter...

到这可以看到定义一个Zuul过滤器其实很简单,对于微服务网关来说不仅是Zuul,其他的微服务网关也是,很大部分的开发工作都是开发各种过滤器来达到我们目的。现在来实现一个简单的参数校验功能:

FirstPreFilter:

public class FirstPreFilter extends ZuulFilter {private Logger log = LoggerFactory.getLogger(FirstPreFilter.class);@Overridepublic String filterType() {// 自定义的过滤器类型为前置过滤器return PRE_TYPE;}@Overridepublic int filterOrder() {// 自定义过滤器的执行次序return 2;}@Overridepublic boolean shouldFilter() {return true;}@Overridepublic Object run() throws ZuulException {log.info("first pre filter...");// 拿到请求上下文RequestContext requestContext = RequestContext.getCurrentContext();// 拿到HttpServletRequestHttpServletRequest request = requestContext.getRequest();// 获取传入的参数值String a = request.getParameter("a");if (StringUtils.isBlank(a)) {// 禁止路由,也就是不允许访问下游服务requestContext.setSendZuulResponse(false);// 设置响应结果,供PostFilter使用,参数是字符串,序列化一下返回对象也行。ObjectMapper mapper = new ObjectMapper();Map<String, Object> map = new HashMap<>();map.put("code", -1);map.put("msg", "参数a不能为空");String result = null;try {result = mapper.writeValueAsString(map);} catch (JsonProcessingException e) {e.printStackTrace();}requestContext.setResponseBody(result);// parameter-check-success保存于上下文,作为同类型下游Filter的执行开关requestContext.set("parameter-check-success", false);return null;}// 设置避免报空requestContext.set("parameter-check-success", true);return null;}
}
复制代码

SecondPreFilter:

public class SecondPreFilter extends ZuulFilter {private Logger log = LoggerFactory.getLogger(SecondPreFilter.class);@Overridepublic String filterType() {return PRE_TYPE;}@Overridepublic int filterOrder() {return 3;}@Overridepublic boolean shouldFilter() {RequestContext requestContext = RequestContext.getCurrentContext();// 参数a是否检验成功,不成功那就没必要继续执行下去return (boolean) requestContext.get("parameter-check-success");}@Overridepublic Object run() throws ZuulException {log.info("second pre filter...");// 拿到请求上下文RequestContext requestContext = RequestContext.getCurrentContext();// 拿到HttpServletRequestHttpServletRequest request = requestContext.getRequest();// 获取传入的参数值String b = request.getParameter("b");if (StringUtils.isBlank(b)) {// 禁止路由,也就是不允许访问下游服务requestContext.setSendZuulResponse(false);// 设置响应结果,供PostFilter使用,参数是字符串,序列化一下返回对象也行。ObjectMapper mapper = new ObjectMapper();Map<String, Object> map = new HashMap<>();map.put("code", -1);map.put("msg", "参数b不能为空");String result = null;try {result = mapper.writeValueAsString(map);} catch (JsonProcessingException e) {e.printStackTrace();}requestContext.setResponseBody(result);// parameter-check-success保存于上下文,作为同类型下游Filter的执行开关requestContext.set("parameter-check-success", false);return null;}return null;}
}
复制代码

CustomPostFilter:

public class CustomPostFilter extends ZuulFilter {private static final Logger LOG = LoggerFactory.getLogger(CustomPostFilter.class);@Overridepublic String filterType() {return POST_TYPE;}@Overridepublic int filterOrder() {return 0;}@Overridepublic boolean shouldFilter() {return true;}@Overridepublic Object run() throws ZuulException {System.out.println("这是PostFilter!");// 从RequestContext获取上下文RequestContext requestContext = RequestContext.getCurrentContext();// 处理返回中文乱码requestContext.getResponse().setCharacterEncoding("UTF-8");// 获取上下文中保存的responseBodyString responseBody = requestContext.getResponseBody();// 如果responseBody不为空,则说明流程有异常发生if (null != responseBody) {//设定返回状态码requestContext.setResponseStatusCode(500);//替换响应报文requestContext.setResponseBody(responseBody);}return null;}
}
复制代码

这整个小功能实现下来,体验到了Zuul中过滤器的执行顺序,以及通过RequestContext来获取HttpServletRequest得到请求信息。

二、Spring Cloud Zuul整合OAuth2+JWT入门实战

作为一个微服务网关,一般我们会在网关上进行鉴权,对于网关后面众多的无状态服务常用的授权和认证便是基于OAuth2。

2.1 什么是OAuth2和JWT

OAuth2是OAuth协议的第二个版本,是对授权认证比较成熟地面向资源的授权协议,在业界中广泛应用。出了定义了常用的用户名密码登录之后,还可以使用第三方一个用登录。例如在某些网站上可以使用QQ、微信、Github等进行登录。其主要流程如下:

至于JWT则是一种使用JSON格式来规约Token和Session的协议。因为传统的认证方式中会产生一个凭证,比如Session会话是保存在服务端,然后依赖于Cookie返回给客户端,Session是有状态的。但是对于众多的微服务来说又是无状态,便诞生像JWT这样的解决方案。

JWT通常有三部分组成:

  • Header:头部,指定JWT使用的签名算法
  • Payload:载荷,包含一些自定义或非自定义的认证信息
  • Signature:签名,将头部和载荷用.连接之后,使用头部的签名算法生成的签名信息并拼接到末尾

OAuth2 + JWT 就是服务端使用OAuth2的方式进行认证,然后颁发一个Token,而这个Token使用JWT。客户端拿着这个Token,便可以访问系统,一般我们会给这个Token设置一个有效期,因为服务端并不会保存这个Token。OAuth2的实现有很多,这里使用Spring社区的基于Spring Security实现的OAuth2

2.2 Zuul + OAuth2 + JWT 入门实操

2.2.1 修改cloud-zuul-gateway

在Zuul网关中我们需要对接口的请求进行保护,判断是否登录鉴权。如果未登录需要重定向到登录页面,登录成功由认证服务器颁发JWT Token;把JWT Token放到请求头传递到下游服务器。

引入Maven依赖:

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
复制代码

配置文件:

  • 首先定义了service-a服务的路由规则
  • 注册中心Eureka的地址
  • 验证授权端点:http://localhost:7788/uaa/oauth/authorize
  • Token的颁发端点:http://localhost:7788/uaa/oauth/token
  • 默认是使用HS256加密算法,密钥是hahaha。加密算法的话建议使用安全性更高的非堆成加密
server:port: 88
spring:application:name: zuul-gateway
eureka:client:serviceUrl:defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8671}/eureka/instance:prefer-ip-address: true
zuul:routes:service-a:path: /servicea/**serviceId: service-a
security:oauth2:client:access-token-uri: http://localhost:7788/uaa/oauth/token #令牌端点user-authorization-uri: http://localhost:7788/uaa/oauth/authorize #授权端点client-id: zuul-gateway #OAuth2客户端IDclient-secret: my-secret #OAuth2客户端密钥resource:jwt:key-value: hahaha #使用对称加密方式,默认算法为HS256
复制代码

WebSecurity的配置:主要是声明

@Configuration
@Order(101)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/login", "/servicea/**").permitAll().anyRequest().authenticated().and().csrf().disable();}
}复制代码

在启动类上添加@EnableOAuth2Sso注解

@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
@EnableOAuth2Sso
public class ZuulServerApplication {public static void main(String[] args) {SpringApplication.run(ZuulServerApplication.class, args);}
}
复制代码

2.2.2 编写认证服务器cloud-auth-server

创建cloud-auth-server来基于OAuth2 实现我们的认证服务器。依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>cloud-zuul-practice-intermediate</artifactId><groupId>com.msr.better</groupId><version>1.0</version></parent><modelVersion>4.0.0</modelVersion><artifactId>cloud-auth-server</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
复制代码

配置文件application.yml

spring:application:name: cloud-auth-server
server:port: 7788servlet:contextPath: /uaa
eureka:client:serviceUrl:defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8671}/eureka/instance:prefer-ip-address: true
复制代码

认证服务器配置:继承AuthorizationServerConfigurerAdapter编写认证授权服务器配置。主要是指定clientId、密钥、以及权限定义和作用域声明,指定JwtTokenStore,类似的实现Spring Security还有RedisTokenStore等。

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate AuthenticationManager authenticationManager;@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("zuul-gateway").secret("my-secret").scopes("write", "read").autoApprove(true).authorities("WRIGTH_READ", "WRIGTH_WRITE").authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code").redirectUris("http://localhost:88/login");}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.tokenStore(jwtTokenStore()).tokenEnhancer(jwtTokenConverter()).authenticationManager(authenticationManager);}@Beanpublic TokenStore jwtTokenStore() {return new JwtTokenStore(jwtTokenConverter());}@Beanprotected JwtAccessTokenConverter jwtTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setSigningKey("hahaha");return converter;}
}
复制代码

Web Security 相关配置:声明guest用户,密码为guest,拥有READ权限。admin用户,密码为admin,拥有READ、WRITE权限。

AuthenticationManager是认证管理器,需要注入到Spring容器中。passwordEncoder()声明密码的加密方式,在Spring Security中要求需要对密码进行加密,因此需要向Spring容器中注入。但是这里使用了内存的方式存放用户信息,而且密码是原值保存,所以使用NoOpPasswordEncoder,即不做加密处理。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic static NoOpPasswordEncoder passwordEncoder() {return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();}@Bean(name = BeanIds.AUTHENTICATION_MANAGER)@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.inMemoryAuthentication().withUser("guest").password("guest").authorities("READ").and().withUser("admin").password("admin").authorities("READ", "WRITE");}
}
复制代码

认证服务器启动类:

@SpringBootApplication
@EnableDiscoveryClient
public class AuthApplication {public static void main(String[] args) {SpringApplication.run(AuthApplication.class, args);}
}
复制代码

2.2.3 cloud-service-a服务整合资源服务器

service-a的编写相对简单,在Spring Security OAuth2中,每个服务都是一个资源服务器,拥有者该服务的资源。

引入依赖:

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
复制代码

配置文件:

server:port: 8080
spring:application:name: service-a
eureka:client:serviceUrl:defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8671}/eureka/instance:prefer-ip-address: true
复制代码

编写资源服务器:

@Configuration
public class ServiceAResourceServerConfig extends ResourceServerConfigurerAdapter {@Overridepublic void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().antMatchers("/**").authenticated().antMatchers(HttpMethod.GET, "/servicea/test").hasAuthority("WRIGHT_READ");}@Overridepublic void configure(ResourceServerSecurityConfigurer resources) throws Exception {resources.resourceId("WRIGHT").tokenStore(jwtTokenStore());}@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter();tokenConverter.setSigningKey("hahaha");return tokenConverter;}@Beanpublic TokenStore jwtTokenStore() {return new JwtTokenStore(jwtAccessTokenConverter());}
}
复制代码

编写ClientController:

@RestController
@RequestMapping
public class ClientController {@GetMapping("/test")public String test(HttpServletRequest request, HttpServletResponse response) {System.out.println("================header================");Enumeration<String> headerNames = request.getHeaderNames();while (headerNames.hasMoreElements()) {String key = headerNames.nextElement();System.out.println(key + ": " + request.getHeader(key));}System.out.println("================header================");return "hello word!";}
}
复制代码

servicea的启动类:启用资源服务器@EnableResourceServer

@SpringBootApplication
@EnableDiscoveryClient
@EnableResourceServer
public class ServiceAApplication {public static void main(String[] args) {SpringApplication.run(ServiceAApplication.class, args);}
}
复制代码

2.2.4 测试

先启动注册中心Eureka、然后启动Zuul网关、serivce-a、auth-server。

请求访问:

http://localhost:88/service/test

OAuth2 + JWT 实战小总结

这里关于Zuul整合OAuth2 + JWT 的介绍就到这,后面会写一篇详细的Spring Security实现的OAuth2文章。本文这里用到的认证服务器和资源服务器是较为早期的写法了,前年Spring Security开了一个新项目专门来编写认证服务器。

Spring Cloud微服务网关Zuul过滤链和整合OAuth2+JWT入门实战相关推荐

  1. Spring Cloud 微服务网关Gateway组件

    一.网关介绍 大家都知道在微服务架构中,一个系统会被拆分为多个微服务,那么作为客户端如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去用. 这样的架构会存 ...

  2. 微服务网关Zuul迁移到Spring Cloud Gateway

    https://juejin.im/post/5ba8daa56fb9a05cfe486ebf 背景 在之前的文章中,我们介绍过微服务网关Spring Cloud Netflix Zuul,前段时间有 ...

  3. spring cloud微服务治理eureka、hystrix、zuul代码例子

    spring cloud微服务中台服务代码例子,包括eureka.hystrix.zuul https://github.com/birdstudiocn/spring-cloud-sample/tr ...

  4. Spring Cloud(六) 服务网关GateWay 入门

    前文回顾: Spring Cloud(一)Eureka Server-单体及集群搭建 Spring Cloud(二) 配置Eureka Client Spring Cloud(三) 熔断器Hystri ...

  5. Spring Cloud —— Gateway 服务网关

    导航 一.什么是服务网关 二.业界常见网关组件 三.Spring Cloud Gateway 四.Gateway 快速入门 4.1 创建 gateway 服务 4.2 添加 gateway 依赖和 n ...

  6. 《Spring Cloud 微服务架构进阶》读书笔记

    前页 随着 DevOps 和以 Docker 为主的容器技术的发展,云原生应用架构和微服 务变得流行起来. 云原生包含的内容很多,如 DevOps.持续交付.微服务.敏捷等 第一章,微服务架构介绍 架 ...

  7. Spring cloud Gateway 服务网关 实战

    Spring cloud Gateway 服务网关 一.简介 优点: 特性: 总结: 二.核心概念 三.路由规则 1.Path 2.Query 3.Method 4.Datetime 5.Romote ...

  8. Spring Cloud 微服务技术栈

    Spring Cloud 简介 主要内容 微服务简介 SpringCloud 简介 SpringCloud 框架结构 SpringCloud 和 Dubbo 的对比 SpringCloud 版本号说明 ...

  9. Spring Cloud 微服务讲义

    Spring Cloud 微服务讲义 第一部分 微服务架构 第 1 节 互联网应用架构演进 第 2 节 微服务架构体现的思想及优缺点 第 3 节 微服务架构中的核心概念 第二部分 Spring Clo ...

最新文章

  1. pytorch学习笔记(十二):详解 Module 类
  2. matlab中sinks,MATLAB Simulink模块库详解(二)Sinks篇
  3. Linux下运行.cpp文件
  4. 深度学习到底有多难?掌握方法很重要!
  5. 三十八、学Sql,不了解Sql注入怎么行?
  6. load方法引入本地html报错,分享基于plus.downloader的图片懒加载功能,支持本地缓存v1.1.0...
  7. cl_ibase_ibintx_buf buffer class
  8. 网络数据包收发流程(四):协议栈之packet_type
  9. PHP array_flip() array_merge() array+array的使用总结
  10. C++ 基于 Visual C++6.0 的 DLL 编程实现
  11. IE8,9下的ajax缓存问题
  12. SpaceX星舰飞船首次试飞成功着陆!但没想到还是爆炸了...
  13. php cii订单导出,使用 kingcms.php 建立一个留言本【附模型代码】
  14. IntelliJ IDEA快速自动生成Junit测试类
  15. 英语六级翻译训练:教育专题
  16. linux笔记:使用conda命令管理包、管理环境详细讲解
  17. [打新技巧]打新股产品跷跷板定律
  18. 【UE4】获取13位时间戳
  19. html会员积分模板,人人商城会员中心头部模板显隐会员积分等项 - YangJunwei
  20. To C/To B/To G分别是什么

热门文章

  1. web网页设计实例作业 ——中国水墨风的小学学校网站(6页) 专题网页设计作业模板 学校物静态HTML网页模板下载
  2. android怎样添加图片锐化功能,如何在android中锐化图像?
  3. matlab 生成lte信号,【求助】LTE PRS基带信号到射频信号如何做?具体代码如下
  4. 从入门到熟悉,Android开发应该如何高效学习?
  5. Java工程师成长之路:讲师介绍
  6. css3 --- 实现动画线条运动效果实例集合
  7. 配置 default Jenkinsfile 文件
  8. 自考2020计算机系统结构题,2020年度10月自考02325计算机系统结构试卷及答案解释.doc...
  9. ue4菜单栏的碰撞图文_工具栏发生碰撞时
  10. jsonp 跨域接收值接不到的解决方法