大家好,我是 17。
Flutter WebView 一共三篇文章
- 在 Flutter 中使用 webview_flutter 4.0 | js 交互
- Flutter WebView 性能优化,让 h5 像原生页面一样优秀
- Flutter WebView 如何与 h5 同步登录状态
本篇是第 3 篇,讲下 Flutter WebView 与 h5 如何同步状态。
cookie 简介
为什么要介绍 cookie
cookie 对于前端小伙伴来说 cookie 再熟悉不过了,但其它端如果没有用过类似的东西不知道也很正常。虽然网上的资料很多,但是对于新手来说,还是无从分辨的,不知道应该看哪些内容,我就一起都说了吧。
cookie 定义
HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器会存储 cookie 并在下次向同一服务器再发起请求时携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器——如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。
HTTP 是无状态的:在同一个连接中,两个执行成功的请求之间是没有关系的。这就带来了一个问题,用户没有办法在同一个网站中进行连续的交互,比如在一个电商网站里,用户把某个商品加入到购物车,切换一个页面后再次添加了商品,这两次添加商品的请求之间没有关联,浏览器无法知道用户最终选择了哪些商品。而使用 HTTP 的标头扩展,HTTP Cookie 就可以解决这个问题。把 Cookie 添加到标头中,创建一个会话让每次请求都能共享相同的上下文信息,达成相同的状态。
注意,HTTP 本质是无状态的,使用 Cookie 可以创建有状态的会话。
以上内容节选自 MDN
保存在本地的意思是保存在本地的磁盘上,当你关闭浏览器甚至关机都没关系,下次打开浏览器的时候它还在。因为每次都会随域名自动发送,cookie 不宜太大,一般不超过 4KB。
token 的作用
token 的作用我举个例子,就是我去你家串门,第一次到你家,你不认识我,问:你是谁?我回答:我是 17。然后你让我进门了。第二次去你家,你还是不认识我,因为每次串门都是完全独立的,可以当作从没来过你家。你依旧问:你是谁?我回答:我是 17,你又让我进门了。如果我经常去你家串门每次都问还是很麻烦的。所以你就给我了一个能证明我身份的令牌(token),我再次到你家串门的时候,直接拿出令牌,令牌上可能是这样写的:我是 17,我们见过的。你一看,啊,原来是 17 啊,快请进。
token 就相当于是这样的令牌。一个用户在网站的一个页面中登录了,他的登录信息会保存在 cookie 里,在访问下一个页面的时候,浏览器会带上 cookie 的信息向服务器请求页面,服务器根据 cookie 的信息就知道你是否登录了。
token 相当于一把钥匙,能开锁。通过 token 无法反解出用户名和密码。密码不要直接放在 cookie 中。
cookie 可以有很多 key value,token 只是这众多的 key value 中的一个
Flutter WebView 中 cookie 的基本操作。
在 webview_flutter 4 中有 cookieManager 专门用来管理 WebView 中的 cookie,我们先来学习下用法。
用 WebViewCookie 设置 cookie
cookieManager.setCookie(
const WebViewCookie(
name: 'IAM17',
value: 'FE',
domain: 'juejin.cn',
),
);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
这样就行了,很简单吧。还有一个参数 path,默认是 ‘/’。 setCookie 返回的是 Future<void>
。
用 cookieManager 清除 cookie
cookieManager.clear()
这个就太简单了。直接清除就好了。需要注意的是,这个方法杀伤力有点大,是无差别攻击,所有 WebView 实例中的 cookie 都会被清除,慎用!。
cookieManager 的能力也就到此为止了,即不能读 cookie,也不能删除单条 cookie,剩下的还指望万能的 javascript。
用 js 设置 cookie
虽然 cookieManager.setCookie
的方法很好用,但有的时候,你可能需要用 js 来设置 cookie。
最简单的只要一个key,一个value,这样设置的 cookie 是一个会话 cookie,浏览器关闭后 cookie 失效。
var key='name',value='IAM17';
document.cookie = `${key}=${encodeURIComponent(value)}`;
- 1
- 2
要想在一段时间内有效,加上 expires 参数。
var key='name',value='IAM17';
var now = new Date();
// 设置一天有效期
now.setTime(now.getTime() + 1000*60*60*24);
var expires=now.toGMTString();
document.cookie = `${key}=${encodeURIComponent(value)};expires=${expires}`;
- 1
- 2
- 3
- 4
- 5
- 6
设置 cookie 在整个网站有效
var key='name',value='IAM17';
document.cookie = `${key}=${encodeURIComponent(value)};path=/`;
- 1
- 2
还有三个与安全相关的参数,了解下含义,后面会详细讲。
- httponly 禁止 js 读取
- secure 只能通过 https 传输
- SameSite 现代浏览器支持的安全相关的属性
准备好 js 后,这样执行
final controller = WebViewController();
controller.runJavaScript('你的 js ');
- 1
- 2
用 js 读取 cookie
final cookies = await controller
.runJavaScriptReturningResult('document.cookie') as String;
- 1
- 2
这样读出来的是一个字符串,用起来并不方便, 17 准备了一方法,把字符串解析成 map,贴心吧!
getCookie(WebViewController controller) async {
final controller = WebViewController();
var cookies = await controller
.runJavaScriptReturningResult('document.cookie') as String;
var list = cookies.split('; ');
var map = <String, String>{};
for (var item in list) {
var cookieItem = item.split('=');
map[cookieItem[0]] = cookieItem[1];
}
return map;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
;(分号)后面是一个英文空格
用 js 删除单条 cookie
比如你想删除一个 key 为 token 的 cookie
controller.runJavaScript(
'document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"');
- 1
- 2
可以不指定 value,指定了也没关系。
js 清除本域名下所有 cookie
js 可以删除本域名下的 cookie。
function clearAllCookie() {
var keys = document.cookie.match(/[^ =;]+(?=\=)/g);
if(keys) {
for(var i = keys.length; i--;)
document.cookie = keys[i] + '=;expires=' + new Date(0).toUTCString()
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
^ 后面是一个英文空格
可能有的同学对正则不是很熟悉,我把正则表达式解释一下。已经掌握的同学自行跳过。
[^\s=;]+
含义为匹配 除空格等号分号外的至少一个连续字符。比如 a
,11a
,都可以,但是空字符串不行,因为至少要一个字符;a b
不行,因为有空格;a=b
不行;因为有等号,;c
不行,因为有分号。
(?=\=)
这是一个零宽肯定先行断言,听术语有点迷糊,还是举例说明。name=17
用这个零宽肯定先行断言匹配的结果就是 e
和 =
之前的那个位置。零宽的意思是不匹配任何字符,只匹配位置。
零宽断言正如它的名字一样,是一种零宽度的匹配,它匹配到的内容不会保存到匹配结果中去,最终匹配结果只是一个位置而已。给指定位置添加一个限定条件,用来规定此位置之前或者之后的字符必须满足限定条件才能使正则中的字表达式匹配成功。
现在把它们合在一起 [^\s=;]+(?=\=)
,可以匹配 除空格等号分号外的至少一个连续字符并且后面紧跟着等号。
\
是转义符,这里不加转义其实也可以,因为前面有 ?=
,加一个 \
会显得清晰一些。
g
表示全局匹配,否则只匹配第一个。
最后把这个函数用 runJavaScript 执行一下就好了。
基础用法已经讲完了,下面我们实战一下。
登录状态同步
用到 cookie 的最经典的应用当属登录状态保持了。 h5 可以单独登录,app 也可以单独登录,理想状态下,两端的登录状态应该保持一致,就需要登录状态的同步了。
app 同步登录状态到 h5
WebView 刚打开的时候,需要把 app 的登录状态同步到 h5。
先写入准备好的 token,再打开对应的 h5 就行了。
setCookie() async {
final manager = WebViewCookieManager();
await cookieManager.setCookie(
const WebViewCookie(
name: 'token',
value: 'IAM17',
domain: 'csdn.net',
),
);
await controller.loadRequest(Uri.parse(
'https://csdn.net'));
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
如果没有域名,用 IP 也是可以的,内网外网的 IP 都行。
当然了,调用 js 写入 cookie 也是可以的,但既然有 cookieManager.setCookie
,用这个方法更方便,还不用等页面加载完成。
h5 同步登录状态到 app
app 的状态是未登录,打开 h5 后, h5 自己在网页中登录了。如何同步到 app 呢?
二个办法
- 如果是单独的登录页面,app 拦截登录页面的请求后,在 app 中登录,再把状态同步到 h5。
- js 调用 JavaScriptChannel 设置的对象方法。
第 1 种方法可以拦截真正的页面地址,也可以拦截和 app 约定好的地址。
第 2 种方法 js 调用登录接口,登录后,再调用 JavaScriptChannel 方法把 token 同步给 app。
当然了,js 调用 JavaScriptChannel 方法通知 app 登录,app 再同步回 h5 也是可以的。
如何拦截和交互请参见 在 Flutter 中使用 webview_flutter 4.0 | js 交互
这 2 种方案在前文都已经讲过,不再赘述。
虽然各种方法都可以实现,17 不推荐拦截真正的页面地址,因为这样页面地址就被定死了,不方便修改了。万一哪天域名变更,或路径变更,那就很麻烦了。
登录 token 安全保障
既然 token 是钥匙,那么钥匙被别人拿去了,可就麻烦了,所以要保证 token 的安全。要想知道如何保证安全,就得先知道如何获取 token。
如何获取 token
- HTTP 劫持
一般 HTTP 劫持是劫持 http 请求,篡改HTTP响应体,使用户收到劫持者篡改后的内容。当然 HTTP 劫持也能拿到 cookie 的信息。 - 跨站脚本攻击(Cross-site scripting,XSS),CSRF(跨站请求伪造)来盗取 cookie。
XSS 是指攻击者可以利用漏洞在网站上注入恶意的客户端代码。若受害者运行这些恶意代码,攻击者就可以突破网站的访问限制并冒充受害者。
Cookie 往往用来存储用户的身份信息,恶意网站可以设法伪造带有正确 Cookie 的 HTTP 请求,这就是 CSRF 攻击。
预防措施
要防范这两种攻击也很简单。
网站启用 https 就可以预防 HTTP 劫持。如果网页使用 https 传输,cookie 也会使用 https 传输。如果你的网站只支持 Https,cookie 不需要单独设置。如果你的网站还支持 http, 为了安全,可以阻止 cookie 在 http 协议下传输。
Set-Cookie: token=IAM17; Secure
- 1
Secure 代表本条 cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端,它永远不会使用不安全的 HTTP 发送(本地主机除外),这意味着中间人攻击者无法轻松访问它。无法轻松这样的表述就显示很严谨了。道高一尺,魔高一丈,永远不要把话说满。我曾发过沸点证明自己是程序员:再难的需求也不敢说实现不了。有些小伙伴不理解,直接回实现不了不就得了? 以前我也多次说过“实现不了”,结果每次都被打脸。所以对于有难度的需求,这样回答会比较稳妥:技术方案我还不是很确定,需要调研一下。调研范围不能仅局限于你自己的领域,要全范围调研,首先要搞明白,在全领域范围内,能不能实现。如果不是自己的领域,就可以交差了。凡事都自己亲自调研,既费时还费力,还可能得出错误的结论,所以最好的方案是,每个领域都认识相关的专家,专业的事,专业的人来办。尤其现在人工智能的出现,让很多不可能成为了可能,要自己亲自去补习吗?NO,普通人没有这个精力,你要做的就是成为自己领域的专家。当你可以成为别人的依靠时,别人也会成为你的依靠。
XSS,CSRF 有多种攻击手段,但无论怎样,不管你几路来,我只一路去。预防攻击把 cookie 中的 HttpOnly,Secure,SameSite 一股脑加上就行了。这样就把用 js,传输,请求链接几种可能的获得 token 的路都堵死了。攻击很难再有机会获得 token。
Set-Cookie: token=IAM17; Secure; HttpOnly; SameSite=Strict;
- 1
Cookie 的 SameSite 属性用来限制第三方 Cookie,从而减少安全风险。
SameSite 有三个值可以选
- Strict 最为严格,完全禁止第三方 Cookie。
- Lax 是默认值,除导航到目标网址的 Get 请求除外,也是禁止第三方Cookie
- None 陏便发第三方 cookie,但必须同时设置 Secure
第三方Cookie 是指由非当前网站设置的Cookie
比如有两个网站 A,B,A 请求 B 的页面,带上 A 的 cookie 发送给 B ,对于 B 的页面来说,接收到的 cookie 就是 第三方 cookie。SameSite=Strict
禁止 cookie 发送到第三方。也就是说 A 网站的 cookie 这个时候不可能会自动发到 B 网站。
如果 WebView 不支持 SameSite 怎么办。其实不用担心,就算 cookie 没有这些安全设置,h5 也应该有能力保证 token 的安全,防止 XSS,CSRF 的攻击。
js 无法设置 HttpOnly,只能用设置 header 的方式来设置。
如何验证 cookie 有没有设置成功?
用 chrome 打开网页,F12 打开 控制面板。找到应用菜单下的存储下面的 cookie,如果 Secure 处打 ✓ 就说明设置成功了。同样的,如果 httpOnly 设置成功了,HttpOnly处也会打 ✓。Samesite 会显示相应的值。
知识扩展, LocalStorage
每次都随着请求自动发送,虽然非常便利,但也带来了麻烦,增加了网络传输的负担。有些数据是没有必要每次都传输的。在早期,程序员们想出了分域的办法。比如 js,css,图片用不同的域名。但是 cookie 还有一个问题,容量太小。为了解决这两个问题,LocalStorage 隆重登场了。一起的还有一个 SessionStorage,数据只在一个会话期间内有效。了解下即可,Flutter 这边一般不会用到,如果真的需要存大量数据,流程应该是这样的:js 通过 javascriptChannel 的方法向 flutter 请求数据,由 js 把数据写入 LocalStorage。这样我们可以保持一定的灵活性,比如修改为把数据写入数据库也是很容易的。
cookie 另外一个主要用途是跟踪分析用户。P3P 可以跨域设置 cookie,了解下即可。
番外
周五接到通知,周六日要开会。我想这下写文的计划是不成了。但是无巧不成书,周五下午就开始不舒服,晚上没吃饭,头痛。周六起来后更严重了,我只好请假。在家里随时可以躺下来,状态好的时候起来写文,于是这篇 《Flutter WebView 如何与 h5 同步登录状态》还是如约而至了。