Flutter三方库适配OpenHarmony【flutter_web_auth】— URI 解析与 Scheme 匹配算法
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net
onNewWant 收到一个 Want 对象,里面有一个 uri 字符串。插件需要从这个字符串里提取出 Scheme,然后在 callbacks Map 中找到对应的 MethodResult。听起来简单,但字符串解析总有各种边界情况——空值、格式错误、编码问题。这篇把 URI 解析的每个细节都过一遍。
一、onNewWant 中 want.uri 的获取
1.1 代码
staticonNewWant(want: Want):void{const uri = want.uri;if(!uri){console.info(TAG,'onNewWant: no uri in want');return;}// ...}1.2 want.uri 的来源
浏览器重定向到 myapp://callback?code=abc123 ↓ 系统创建 Want 对象 ↓ want = { action: 'ohos.want.action.viewData', uri: 'myapp://callback?code=abc123', // ... } ↓ EntryAbility.onNewWant(want) ↓ FlutterWebAuthPlugin.onNewWant(want) ↓ want.uri → "myapp://callback?code=abc123" 1.3 uri 可能为空的场景
| 场景 | want.uri | 处理 |
|---|---|---|
| 正常深度链接 | "myapp://callback?code=abc" | 继续处理 |
| 非深度链接的 Want | undefined | 静默返回 |
| 空字符串 | "" | 被 !uri 捕获 |
| 系统内部 Want | undefined | 静默返回 |
1.4 空值判断
if(!uri){console.info(TAG,'onNewWant: no uri in want');return;}!uri 同时处理了 undefined、null 和空字符串三种情况。这是 JavaScript/ArkTS 的 falsy 值特性。
二、indexOf(‘😕/’) 手动解析 Scheme
2.1 代码
const schemeEnd = uri.indexOf('://');if(schemeEnd <0){return;}const scheme = uri.substring(0, schemeEnd);2.2 解析过程
uri = "myapp://callback?code=abc123" │ │ 0 5 ← indexOf('://') = 5 scheme = uri.substring(0, 5) = "myapp" 2.3 为什么手动解析而不用 URL 类
// 方案1:手动解析(当前实现)const schemeEnd = uri.indexOf('://');const scheme = uri.substring(0, schemeEnd);// 方案2:使用 URL 类const url =newURL(uri);const scheme = url.protocol.replace(':','');// "myapp:" → "myapp"
| 方案 | 优点 | 缺点 |
|---|---|---|
| 手动解析 | 简单、无依赖、不会抛异常 | 不处理复杂 URI |
| URL 类 | 标准、处理各种格式 | 自定义 Scheme 可能抛异常 |
💡 手动解析更安全。new URL("myapp://callback")在某些环境下可能抛异常,因为myapp不是标准的 URL 协议。手动用indexOf不会有这个问题。
2.4 indexOf 返回 -1 的情况
if(schemeEnd <0){return;// URI 中没有 "://",不是有效的深度链接}
| URI | indexOf(‘😕/’) | 处理 |
|---|---|---|
myapp://callback | 5 | 继续 |
https://example.com | 5 | 继续(但不会匹配 callbacks) |
myapp:callback | -1 | 返回 |
just-a-string | -1 | 返回 |
://no-scheme | 0 | scheme = “”,不会匹配 |
三、Scheme 匹配逻辑
3.1 代码
if(FlutterWebAuthPlugin.callbacks.has(scheme)){const pendingResult: MethodResult = FlutterWebAuthPlugin.callbacks.get(scheme)as MethodResult; FlutterWebAuthPlugin.callbacks.delete(scheme); pendingResult.success(uri);console.info(TAG,`Resolved callback for scheme: ${scheme}`);}else{console.info(TAG,`No pending callback for scheme: ${scheme}`);}3.2 匹配流程
scheme = "myapp" │ ├── callbacks.has("myapp")? │ │ │ ├── YES │ │ ├── get("myapp") → pendingResult │ │ ├── delete("myapp") → 从 Map 中移除 │ │ └── pendingResult.success(uri) → 返回完整 URI │ │ │ └── NO │ └── 日志记录,静默忽略 │ └── 完成 3.3 返回完整 URI 而不是只返回参数
pendingResult.success(uri);// 返回 "myapp://callback?code=abc123&state=xyz"// 而不是只返回 "code=abc123&state=xyz"Dart 层负责解析返回的 URI:
final result =awaitFlutterWebAuth.authenticate(...);// result = "myapp://callback?code=abc123&state=xyz"final code =Uri.parse(result).queryParameters['code'];final state =Uri.parse(result).queryParameters['state'];📌 返回完整 URI 是更好的设计——Dart 层可以用 Uri.parse() 方便地提取任何参数,不需要原生层预先知道有哪些参数。四、多 Scheme 并发场景
4.1 正常场景
// 两个不同 Scheme 的认证final google =FlutterWebAuth.authenticate( url: googleAuthUrl, callbackUrlScheme:"com.google.app",);final github =FlutterWebAuth.authenticate( url: githubAuthUrl, callbackUrlScheme:"com.github.app",);callbacks Map: { "com.google.app" → result1, "com.github.app" → result2, } 4.2 回调匹配
onNewWant: uri = "com.google.app://callback?code=google123" → scheme = "com.google.app" → callbacks.has("com.google.app") → YES → result1.success("com.google.app://callback?code=google123") onNewWant: uri = "com.github.app://callback?code=github456" → scheme = "com.github.app" → callbacks.has("com.github.app") → YES → result2.success("com.github.app://callback?code=github456") 每个 Scheme 独立匹配,互不干扰。
4.3 同 Scheme 并发(冲突)
// 两次使用相同 Scheme(不推荐)final first =FlutterWebAuth.authenticate(url: url1, callbackUrlScheme:"myapp");final second =FlutterWebAuth.authenticate(url: url2, callbackUrlScheme:"myapp");callbacks.set("myapp", result1); // 第一次 callbacks.set("myapp", result2); // 第二次,覆盖了 result1! // result1 丢失,first Future 永远不会 complete // 直到 cleanUpDanglingCalls 清理
| 结果 | 说明 |
|---|---|
| first | 永远不会 complete(直到 cleanUpDanglingCalls) |
| second | 正常工作 |
五、URL 编码与特殊字符
5.1 常见的编码问题
原始 URI: myapp://callback?name=张三&token=abc+def 编码后: myapp://callback?name=%E5%BC%A0%E4%B8%89&token=abc%2Bdef 5.2 对 Scheme 提取的影响
const uri ="myapp://callback?name=%E5%BC%A0%E4%B8%89";const schemeEnd = uri.indexOf('://');// 5const scheme = uri.substring(0, schemeEnd);// "myapp"URL 编码不影响 Scheme 部分——Scheme 只包含 ASCII 字母、数字和少数特殊字符,不需要编码。
5.3 对参数提取的影响
// Dart 层final result ="myapp://callback?name=%E5%BC%A0%E4%B8%89";final uri =Uri.parse(result);final name = uri.queryParameters['name'];// "张三"(自动解码)Uri.parse() 会自动处理 URL 编码,开发者不需要手动解码。
5.4 Fragment(#)的处理
隐式模式的回调可能使用 fragment: myapp://callback#access_token=abc123&token_type=bearer // Dart 层处理 fragmentfinal result ="myapp://callback#access_token=abc123";final uri =Uri.parse(result);final token = uri.fragment.split('&').map((e)=> e.split('=')).firstWhere((e)=> e[0]=='access_token')[1];// token = "abc123"
| 参数位置 | 格式 | Dart 提取方式 |
|---|---|---|
| Query | ?key=value | uri.queryParameters['key'] |
| Fragment | #key=value | 手动解析 uri.fragment |
六、安全性考虑
6.1 Scheme 劫持
攻击场景: 1. 恶意 App 也注册了 myapp:// Scheme 2. 用户完成认证后,系统可能把回调发给恶意 App 3. 恶意 App 获取了 authorization code 6.2 防御措施
| 措施 | 说明 | 实现 |
|---|---|---|
| 使用反向域名 Scheme | 降低冲突概率 | com.example.myapp |
| PKCE | 即使 code 被截获也无法使用 | 需要 code_verifier |
| State 参数 | 验证回调的合法性 | 随机字符串比对 |
// 使用 PKCE + State 的安全认证import'dart:math';import'dart:convert';import'package:crypto/crypto.dart';final codeVerifier =_generateCodeVerifier();final codeChallenge =_generateCodeChallenge(codeVerifier);final state =_generateRandomString(32);final url =Uri.https('auth.example.com','/authorize',{'response_type':'code','client_id': clientId,'redirect_uri':'$callbackUrlScheme:/','code_challenge': codeChallenge,'code_challenge_method':'S256','state': state,});final result =awaitFlutterWebAuth.authenticate( url: url.toString(), callbackUrlScheme: callbackUrlScheme,);// 验证 statefinal returnedState =Uri.parse(result).queryParameters['state'];if(returnedState != state){throwException('State mismatch! Possible CSRF attack.');}📌 PKCE 和 State 参数是 flutter_web_auth 使用者应该实现的安全措施,不是插件本身的功能。插件只负责传递 URL 和接收回调。
七、与其他解析方案的对比
7.1 三种解析方案
// 方案1:indexOf(当前实现)const scheme = uri.substring(0, uri.indexOf('://'));// 方案2:正则表达式const match = uri.match(/^([a-z][a-z0-9+.-]*):/);const scheme = match ? match[1]:null;// 方案3:URL 类const scheme =newURL(uri).protocol.slice(0,-1);7.2 对比
| 方案 | 性能 | 健壮性 | 复杂度 |
|---|---|---|---|
| indexOf | 最快 | 中 | 最低 |
| 正则 | 中 | 高 | 中 |
| URL 类 | 最慢 | 最高 | 低 |
当前的 indexOf 方案在性能和简洁性之间取得了好的平衡。对于 flutter_web_auth 的场景(URI 格式可预测),这个方案足够了。
总结
本文详细分析了 flutter_web_auth 的 URI 解析与 Scheme 匹配:
- want.uri 获取:空值判断覆盖 undefined/null/空字符串
- indexOf 解析:简单高效,不依赖 URL 类
- Scheme 匹配:从 callbacks Map 中查找对应的 MethodResult
- URL 编码:不影响 Scheme 提取,Dart 层自动解码参数
- 安全性:PKCE + State 防御 Scheme 劫持
下一篇我们讲错误处理——NO_CONTEXT、LAUNCH_FAILED、CANCELED 三种错误的完整梳理。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- RFC 3986 URI 语法
- URL 编码规范
- PKCE RFC 7636
- Dart Uri 类
- flutter_web_auth OpenHarmony 源码
- JavaScript String.indexOf
- OAuth 2.0 安全最佳实践
- 开源鸿蒙跨平台社区
URI 语法结构图(来自 Wikipedia)