WKWebView跨域的Cookie问题

发现问题

业务场景

在微信里打开一个页面,用户输入手机号,通过图片验证码获取短信验证码进行身份验证。

差异性的表现

在iPhone多款手机里发现验证失败,Android手机没有发现问题。在开发用的Mac电脑上,没有发现Safari或者Chrome出现类似的问题。通过手机抓包+后端联调,发现是Cookie有问题。

根据表现分析

  1. 获取图片验证码的时候,服务端用Session机制追踪身份。
  2. 将手机号和用户输入的图片验证码发送给服务端以请求发送短信验证码的时候,本应作为Session支持的Cookie丢失,服务器没有获得JSESSIONID,从而认定此时的用户是另外一个新用户
  3. 同一个用户在短信验证码的验证阶段被服务端判定和之前获取了图片验证码的不是同一个用户。

寻找问题原因

  1. 确认了跨域请求的前后端代码是ok的,Mac上和Android手机上正常表现证明了这一点。那么问题只可能出现在iPhone的微信上。
  2. 直扑微信公众平台技术文档开篇第一句就让我觉得找对方向了。

    微信iOS客户端将于2017年3月1日前逐步升级为WKWebview内核,需要网页开发者提前做好网站的兼容检查和适配。

  3. 和本篇问题相关的最终要的内容是:

    变化1:跨域存取Cookie
    问题说明:在访问一个页面A时,如果页面A引用了另一个页面B的资源(页面A和B为不同的域名),这时页面B就被认为是第三方页面。若在页面B中设置Cookie,就会命中WKWebview下阻止第三方跨域设置Cookie的安全策略,导致问题出现。
    适配建议:
    在WKWebview中是默认阻止跨域的第三方设置Cookie。所有通过Cookie传递的信息,可通过业务后台存储需要传递的信息,然后给页面一个存储信息相对应的access_token加密码,再通过Url中加入自己业务的access_token进行页面间的信息传递。

  4. 问题基本就清楚了,获取图片验证码的时候服务端在Response Header里Set-Cookie,但是图片资源和页面本身是不同的域名,就触发了上面说的阻止第三方跨域问题。

移动端Web跨域问题和WebView的关系

  1. 第三方Cookie的隐私策略:Android5.0之后,对于WebView需调用setAcceptThirdPartyCookies方法;而IOS7.0之后,对于WebView需设置setCookieAcceptPolicy配置,从而允许第三方cookie存储。
  2. 可以想见的是,Android端的微信应该是做了相应的配置的,至于那个封闭的帝国…[self.webView.configuration.processPool performSelector:@selector(_setCookieAcceptPolicy:) withObject:NSHTTPCookieAcceptPolicyAlways afterDelay:0];私有方法,一般是不会允许开发者去修改的,想来微信也是无能为力所以只能发文档说明了。
  3. 由此也弄清了,为什么只有iPhone里会有问题,而Android机和PC端都没有问题

解决问题

反抗WKWebView的机制

  1. 换回UIWebView——这不是你我说的算的
  2. iPhone手机里其实可以设置允许第三方Cookie控制,默认是不允许,而用户的手机——也不是你我能说的算的

修改身份验证机制

  1. 目的是不依赖Cookie。
  2. 身份凭证从服务端返回后,在下一次请求里主动发送过去。
  3. 需要前后端都增加相应的逻辑支持。

代理转发

  1. 可以考虑用中间页面来转发,但更简单的是方法是在页面的前端服务器上配置Nginx,最后也是这么选择的。
  2. 在前端代码里,把页面请求的接口的域名全部替换成和页面同一个域名。
  3. Nginx里的配置例子如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    location  /regapi/ {
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods "POST, GET, OPTIONS";
    add_header Access-Control-Allow-Headers "Origin, Authorization, Accept";
    add_header Access-Control-Allow-Credentials true;
    proxy_pass https://api.realserverhost.com/serverapi/;
    proxy_redirect off;
    proxy_cookie_path /serverapi /regapi;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Cookie $http_cookie;
    proxy_set_header User-Agent $http_user_agent;
    proxy_set_header Referer $http_referer;
    }
  4. 强调一些细节:

    • 不能使用rewrite,会导致post请求变成get请求
    • 只改host是不会有Cookie问题的,但是如果path都改了,就必须增加proxy_cookie_path
    • 一个有意思的地方在于proxy_pass https://api.realserverhost.com/serverapi/末尾的slash符号。这个符号表示“根”,如果没有这个符号,会变成转发到https://api.shanhulicai.cn/serverapi/regapi/
    • 如果页面本身也有Nginx代理并且和转发的代理有共同之处(指’location’后面那个正则),强烈建议把请求的转发配置写在前面。在实际项目中,遇到了请求转发被代理到页面上的情况。排查发现请求的正则含有页面的正则(这算起名的锅,没错,但是约定的做法能避免糊涂的命名问题)。有趣的是,测试服务器上不会产生这个问题(生产服务器上的Nginx的配置很繁杂,不仅仅有这次的项目,所以问题的细节不太容易分析。)