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"继续处理
非深度链接的 Wantundefined静默返回
空字符串""!uri 捕获
系统内部 Wantundefined静默返回

1.4 空值判断

if(!uri){console.info(TAG,'onNewWant: no uri in want');return;}

!uri 同时处理了 undefinednull 和空字符串三种情况。这是 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 中没有 "://",不是有效的深度链接}
URIindexOf(‘😕/’)处理
myapp://callback5继续
https://example.com5继续(但不会匹配 callbacks)
myapp:callback-1返回
just-a-string-1返回
://no-scheme0scheme = “”,不会匹配

三、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=valueuri.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 匹配:

  1. want.uri 获取:空值判断覆盖 undefined/null/空字符串
  2. indexOf 解析:简单高效,不依赖 URL 类
  3. Scheme 匹配:从 callbacks Map 中查找对应的 MethodResult
  4. URL 编码:不影响 Scheme 提取,Dart 层自动解码参数
  5. 安全性:PKCE + State 防御 Scheme 劫持

下一篇我们讲错误处理——NO_CONTEXT、LAUNCH_FAILED、CANCELED 三种错误的完整梳理。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

相关资源:

在这里插入图片描述

URI 语法结构图(来自 Wikipedia)

Could not load content