当前位置: 首页 > news >正文

AtomGit Flutter鸿蒙客户端:OAuth2认证与登录

认证方式概览

AtomGit 支持两种标准的 API 认证机制:

方式认证 Header适用场景实现路径
OAuth2 授权码模式Authorization: Bearer <token>第三方应用标准登录AuthService + OhosPlatform 浏览器授权
Personal Access TokenAuthorization: Bearer <token>开发者快速接入AuthProvider.setTokenFromManualInput

两种方式最终都使用Authorization: Bearer的 Header 格式,差异仅在于 Token 的获取路径。OAuth2 需要完整的浏览器跳转 → 用户授权 → 回调拦截 → 换取 Token 的流程,而 PAT 是用户在 AtomGit 网站手动生成后粘贴到应用中。

为什么最终采用 Token 优先策略

在项目初期,OAuth2 是默认的登录方案。但在实际开发和测试过程中暴露了几个问题:

  1. OAuth 应用注册门槛:每个开发者需要先在 AtomGit 创建自己的 OAuth 应用获取 Client ID 和 Client Secret。App 内置的密钥无法分发给普通用户。

  2. 回调 URL 限制:AtomGit 要求回调 URL 必须是 HTTPS 地址。自定义 URL Scheme(atomgit://oauth/callback)在开发阶段需要通过 HTTPS 重定向页中转,增加了部署复杂度。

  3. 调试困难:OAuth 流程涉及浏览器 → 服务器 → 原生回调 → Flutter 消息通道的多级跳转,任何一环出问题都难以定位。

基于这些现实因素,项目调整了策略:优先支持 Personal Access Token 直接输入(保证功能完整性),同时保留 OAuth 流程作为高级选项(提供更好的用户体验路径)。

OAuth2 授权码流程详解

OAuth2 授权码模式(Authorization Code Grant)是 AtomGit 官方推荐的第三方应用认证方式,适合运行在用户设备上的客户端应用。

第一步:应用注册

在 AtomGit 开发者设置中创建 OAuth 应用,配置以下信息:

  • 应用名称:AtomGit Flutter Client
  • 回调 URLatomgit://oauth/callback(HarmonyOS 自定义 URL Scheme)
  • 权限范围repo user(仓库读写 + 用户信息)

注册完成后获得 Client ID 和 Client Secret。

第二步:构建授权 URL

AuthService 负责构建完整的 OAuth 授权 URL:

classAuthService{StringbuildAuthorizeUrl({requiredStringclientId,requiredStringclientSecret,}){finalparams={'client_id':clientId,'redirect_uri':ApiConstants.redirectUri,'scope':ApiConstants.scope,'response_type':'code',};finalquery=params.entries.map((e)=>'${Uri.encodeComponent(e.key)}=''${Uri.encodeComponent(e.value)}').join('&');return'${ApiConstants.authorizeUrl}?$query';}}

生成的 URL 示例:

https://atomgit.com/login/oauth/authorize ?client_id=40e71353714f41a4b1a7b48f054cb6fa &redirect_uri=atomgit%3A%2F%2Foauth%2Fcallback &scope=repo+user &response_type=code

每个参数都经过Uri.encodeComponent编码,保证特殊字符(如 URL Scheme 中的://)在 URL 中正确传递。

response_type=code指定使用授权码模式(而非隐式模式)。AtomGit 在用户授权后返回一个临时的授权码(code),应用用这个 code 去交换 access_token。

OAuth 回调 URL 的演变

回调 URL 经历了三个阶段的调整:

阶段一:简化格式atomgit://callback

  • 问题:AtomGit 的表单校验将其识别为非法 URL 格式

阶段二:标准 URI Schemeatomgit://oauth/callback

  • URL 三段式:scheme://host/path,通过了 AtomGit 的表单验证
  • 问题:AtomGit 进一步要求回调 URL 必须是 HTTPS 地址

阶段三:HTTPS 重定向中转

  • 在 GitHub Pages 上部署oauth_callback.html
  • 该页面接收?code=xxx参数,通过 JavaScript 重定向到atomgit://oauth/callback?code=xxx
  • 最终在 AtomGit 注册的回调 URL 为 HTTPS 页面地址
  • HarmonyOS 系统识别到atomgit://scheme 后路由回应用
<!-- oauth_callback.html --><!DOCTYPEhtml><html><body><script>constparams=newURLSearchParams(window.location.search);constcode=params.get('code');if(code){window.location.href='atomgit://oauth/callback?code='+code;}</script></body></html>

第三步:打开系统浏览器

通过 HarmonyOS 平台通道打开系统浏览器:

// Dart 端Future<bool>openBrowser(Stringurl)async{finalmessage=jsonEncode({'method':'openBrowser','url':url,});finalreply=await_channel!.send(message);if(reply!=null){finalresult=jsonDecode(reply)asMap<String,dynamic>;returnresult['success']==true;}returnfalse;}
// ArkTS 端privateopenBrowser(url:string):void{constwant:Want={action:'ohos.want.action.viewData',uri:url};this.context.startAbility(want);}

ohos.want.action.viewData是 HarmonyOS 的通用数据查看动作,系统会根据 URI 的协议类型自动选择合适的应用打开(https:// 会打开系统浏览器)。

第四步:Native 拦截回调

当用户在浏览器中完成授权后,AtomGit 服务器会重定向到回调 URL。HarmonyOS 通过 URL Scheme 机制将控制权交回应用:

onNewWant(want:Want,launchParam:AbilityConstant.LaunchParam):void{consturi:string|undefined=want?.uri;if(uri&&uri.startsWith('atomgit://oauth/callback')){constcode:string|null=this.extractQueryParam(uri,'code');if(code&&this.authChannel){constmessage=JSON.stringify({type:'authCode',code:code});this.authChannel.send(message);}}}

onNewWant是 HarmonyOS Ability 的生命周期回调,在应用收到新的 Want(意图)时触发。EntryAbility 检查 URI 是否是 OAuth 回调,提取code参数,通过 MessageChannel 发送给 Dart 端。

URL 参数的手动解析

HarmonyOS 的 API 不提供类似 JavaScriptURLSearchParams的工具类,需要手动解析查询参数:

privateextractQueryParam(uri:string,param:string):string|null{constqueryIndex=uri.indexOf('?');if(queryIndex===-1)returnnull;constquery=uri.substring(queryIndex+1);constpairs=query.split('&');for(constpairofpairs){const[key,value]=pair.split('=');if(key===param&&value){returndecodeURIComponent(value);}}returnnull;}

分步解析逻辑:

  1. 找到?的位置,截取之后的查询字符串
  2. &分割参数对
  3. 每对按=分割 key 和 value
  4. 对匹配 key 的 value 进行decodeURIComponent解码

第五步:收取授权码

Dart 端通过 Broadcast Stream 接收授权码:

classOhosPlatform{final_authCodeController=StreamController<String>.broadcast();Stream<String>getonAuthCode=>_authCodeController.stream;voidinit(){_channel!.setMessageHandler((String?message)async{if(message!=null){finaldecoded=jsonDecode(message)asMap<String,dynamic>;if(decoded['type']=='authCode'){_authCodeController.add(decoded['code']asString);}}});}}

Broadcast Stream 允许多个监听者同时订阅。LoginScreen 在initState中订阅:

class_LoginScreenStateextendsState<LoginScreen>{StreamSubscription<String>?_authCodeSub;@overridevoidinitState(){super.initState();_authCodeSub=OhosPlatform.instance.onAuthCode.listen((code){_handleAuthCode(code);});}@overridevoiddispose(){_authCodeSub?.cancel();// 必须取消订阅,防止内存泄漏super.dispose();}}

第六步:换取 Access Token

用授权码(code)向 Token 端点换取 access_token:

Future<void>exchangeCode(Stringcode,{String?clientId,String?clientSecret,})async{finalresponse=await_httpClient.post(Uri.parse(ApiConstants.tokenUrl),headers:{'Accept':'application/json','Content-Type':'application/json',},body:jsonEncode({'client_id':clientId??'','client_secret':clientSecret??'','code':code,}),);if(response.statusCode==200){finaldata=jsonDecode(response.body);finalaccessToken=data['access_token']asString;// 1. 设置 API 客户端_apiClient.setAccessToken(accessToken);// 2. 持久化到本地awaitLocalStorage.instance.write('access_token',accessToken);// 3. 更新登录状态_accessToken=accessToken;_isLoggedIn=true;notifyListeners();}}

关键细节:Token 交换的所有参数(client_id、client_secret、code)都放在 JSON Body 中,而非 Query String。这是 AtomGit API 的要求(与 GitHub 的 OAuth 实现不同,GitHub 接受 Query String 参数)。

第七步:Session 恢复

应用重启时,自动从本地存储恢复 Token:

Future<void>tryRestoreSession()async{finaltoken=awaitLocalStorage.instance.read<String>('access_token');if(token!=null&&token.isNotEmpty){_accessToken=token;_apiClient.setAccessToken(token);_isLoggedIn=true;notifyListeners();}}

恢复时机在 App 启动初期——AtomGitApp 创建 AuthProvider 时自动调用tryRestoreSession。如果本地存储中存在有效 Token,用户无需重新登录即可使用全部功能。

登录页面的三重入口设计

LoginScreen 提供三种登录路径,覆盖不同的使用场景:

1. OAuth 浏览器登录(主路径)

用户输入 Client ID 和 Client Secret → 点击浏览器登录 → 跳转系统浏览器 → 授权 → 自动回调 → 换取 Token:

Future<void>_startOAuth()async{finalclientId=_clientIdController.text.trim();finalclientSecret=_clientSecretController.text.trim();if(clientId.isEmpty||clientSecret.isEmpty){_showError('请输入 Client ID 和 Client Secret');return;}setState(()=>_isLoading=true);finalauthUrl=AuthService.buildAuthorizeUrl(clientId:clientId,clientSecret:clientSecret,);finalsuccess=awaitOhosPlatform.instance.openBrowser(authUrl);if(!success){setState(()=>_isLoading=false);_showError('无法打开浏览器');}}

打开浏览器后,页面保持"等待授权中…"的 loading 状态。一旦收到 OAuth 回调(通过onAuthCodeStream),自动调用exchangeCode完成登录。

2. 手动输入授权码(备用路径)

浏览器回调失败时(如网络导致 URL Scheme 未被拦截),用户可以手动从浏览器地址栏复制code参数粘贴:

TextField(controller:_codeController,decoration:constInputDecoration(hintText:'粘贴浏览器返回的 ?code= 参数',),),OutlinedButton(onPressed:(){finalcode=_codeController.text.trim();if(code.isNotEmpty){// 手动输入时必须提供 client_id 和 client_secretcontext.read<AuthProvider>().exchangeCode(code,clientId:_clientIdController.text.trim(),clientSecret:_clientSecretController.text.trim(),);}},child:constText('提交授权码'),),

3. Personal Access Token 直接登录(最快路径)

用户在 AtomGit 网站生成 PAT → 粘贴 → 即时登录:

TextField(controller:_tokenController,decoration:constInputDecoration(hintText:'粘贴 Personal Access Token',),),FilledButton(onPressed:(){finaltoken=_tokenController.text.trim();if(token.isNotEmpty){context.read<AuthProvider>().setTokenFromManualInput(token);Navigator.pop(context);// 返回上一页}},child:constText('使用 Token 登录'),),

Token 登录的代码路径最短:直接调用setTokenFromManualInput→ 设置 API 客户端 → 持久化 → 通知 UI → 关闭登录页。相比 OAuth 流程少了浏览器跳转和 Token 交换两个步骤。

AuthProvider 的完整设计

classAuthProviderextendsChangeNotifier{finalAtomGitApiClient_apiClient;String?_accessToken;bool _isLoggedIn=false;boolgetisLoggedIn=>_isLoggedIn;// Token 直接登录Future<void>setTokenFromManualInput(Stringtoken)async{_accessToken=token;_apiClient.setAccessToken(token);awaitLocalStorage.instance.write('access_token',token);_isLoggedIn=true;notifyListeners();}// OAuth 换取 TokenFuture<void>exchangeCode(Stringcode,{String?clientId,String?clientSecret,})async{try{finalresponse=awaithttp.post(Uri.parse(ApiConstants.tokenUrl),headers:{'Accept':'application/json','Content-Type':'application/json',},body:jsonEncode({'client_id':clientId??'','client_secret':clientSecret??'','code':code,}),);if(response.statusCode==200){finaldata=jsonDecode(response.body);finaltoken=data['access_token']asString;_accessToken=token;_apiClient.setAccessToken(token);awaitLocalStorage.instance.write('access_token',token);_isLoggedIn=true;notifyListeners();}}onExceptioncatch(e){// 错误处理}}// 登出Future<void>logout()async{_accessToken=null;_isLoggedIn=false;_apiClient.setAccessToken(null);awaitLocalStorage.instance.delete('access_token');notifyListeners();}}

关键设计:AuthProvider 持有AtomGitApiClient的引用,每次 Token 变更(登录、登出、恢复)都同步调用_apiClient.setAccessToken()。这保证了认证状态与 API 调用的一致性——用户登录后,所有后续 API 请求自动携带 Bearer Token;用户登出后,API 客户端立即清除认证信息,不会再发送已失效的 Token。

API 认证 Header

AtomGit API 的认证需要通过 HTTP Header 传递:

Map<String,String>get_headers=>{'Accept':'application/json','Content-Type':'application/json','X-Api-Version':'2023-02-21',if(_accessToken!=null)'Authorization':'Bearer$_accessToken',};

四个 Header:

  • Accept: application/json— 期望服务器返回 JSON
  • Content-Type: application/json— 请求体是 JSON(POST 请求时)
  • X-Api-Version: 2023-02-21— AtomGit API 版本标识(必需)
  • Authorization: Bearer <token>— 认证凭证(条件性,仅登录后携带)

if (_accessToken != null)是 Dart 的集合条件语法——只在 Token 非空时添加 Authorization Header。未登录时 API 请求不带认证信息,只能访问公开端点。

登录状态传播机制

AuthProvider 的notifyListeners()会触发所有使用context.watch<AuthProvider>()的 Widget 重建。这驱动了整个应用的 UI 变化:

AuthProvider.notifyListeners() ├── MainShell: 无需变化(只是容器) ├── HomeTab: 从欢迎页切换到仓库列表 ├── ExploreTab: 显示搜索功能 ├── NotificationsTab: 从登录引导切换到通知占位 ├── ProfileTab: 创建 UserProvider → 加载用户数据 ├── SettingsScreen: 显示已登录状态 + 退出按钮 └── 所有需要 auth 的页面: 可以正常发起 API 请求

这种一对多的广播机制使得添加新的登录感知组件非常简单——只需在 build 方法中context.watch<AuthProvider>()即可自动响应登录/登出事件。

OAuth 与 PAT 的技术比较

维度OAuth2 授权码Personal Access Token
用户体验浏览器授权,需跳转复制粘贴,需手动生成
实现复杂度6 步流程,涉及平台通道2 步:输入 + 验证
安全性授权码一次性,短有效期Token 长期有效,需妥善保管
Token 粒度按 scope 控制权限Token 创建时指定权限
调试难度高(多级跳转)低(直接输入)
适用场景生产环境开发/测试

两种方式在项目中并存:PAT 保证快速可用,OAuth 提供更好的标准化体验路径。

登出的完整清理

登出操作需要清理四层状态:

Future<void>logout()async{// 1. 清除内存中的 Token_accessToken=null;// 2. 更新登录标志_isLoggedIn=false;// 3. 清除 API 客户端的认证信息_apiClient.setAccessToken(null);// 4. 删除本地持久化的 TokenawaitLocalStorage.instance.delete('access_token');// 5. 通知所有监听者notifyListeners();}

任何一步遗漏都会导致状态不一致。例如:如果只清除_isLoggedIn而不调用_apiClient.setAccessToken(null),UI 会显示未登录状态,但 API 请求仍然携带旧 Token(可能导致 401 错误或对已登出用户的数据泄露)。

http://www.cnnetsun.cn/news/2777708.html

相关文章:

  • AtomGit Flutter鸿蒙客户端:API客户端与网络层
  • 如何快速配置Synology歌词插件:打造完美音乐体验的完整指南
  • 001篇 | 边界是最高级的播种:为什么你越帮别人,别人越讨厌你?一套“菜单式互动”沟通法彻底解决
  • 巴中市30米精度地形高程数据+市级行政边界矢量文件(WGS84)
  • Claude规划结果不可控?揭秘LLM-Reasoning协同框架中的5个确定性锚点设计
  • 企业级教师工作量管理系统管理系统源码|SpringBoot+Vue+MyBatis架构+MySQL数据库【完整版】
  • 显存溢出与延迟激增?Transformer QKV 计算在长序列下的瓶颈剖析与实战调优
  • HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十八):【数据持久化】收藏与浏览历史——让数据在 App 重启后依然“活着”
  • 函数指针数组、回调机制
  • 【独家首发】全球首份《人机创造力配比健康指数》:你的AI依赖度已超标?3分钟自测+干预方案
  • ReadCat:如何在广告泛滥时代重新找回纯净阅读体验?
  • Sora 2科学可视化不是“视频生成”,而是新一代计算叙事引擎(附IEEE VIS 2024预印本验证数据)
  • 手术机器人+AI术中导航协同演进路线图(2024-2027临床转化时间表,含12家头部医企技术栈对比)
  • 亲测真香!2026年5款微软语音转文字免费神器,数据分析师10分钟搞定万字转写!
  • Tiny RDM终极指南:如何5分钟完成Redis可视化管理工具安装配置
  • 094、视频流实时检测管线:FFmpeg 拉流 + YOLO 推理 + Kafka 结果分发架构
  • Kubernetes DaemonSet — 企业级应用场景与实战实例【20260605】001篇
  • 利用快马AI快速构建汇川变频器控制逻辑模拟原型
  • 【Redis】Redis缓存应用实战Day12(2026年)
  • 美陈雕塑构思卡壳?5 个宝藏网站,帮你摆脱创作难题
  • 英语专业论文怎么降低重复率?
  • git status
  • 写mysql数据库日志的时机
  • 2026年实测10款降AI率网站推荐:免费与付费全对比,毕业论文降低ai率必看
  • 如何用LRCGET批量歌词同步工具一键解决离线音乐库歌词管理难题
  • 在Apple Silicon Mac上无缝运行Windows程序的完整指南:Whisky让你的Mac更强大
  • 目标检测调参实战:用CIOU Loss在YOLOv5/v8上提升mAP的完整流程
  • 如何在macOS上获得终极视频预览体验:QLVideo完整指南
  • 计算机小程序毕设实战-基于springboot+微信小程序的视频点播微信小程序【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • 突破JSXBIN加密壁垒:Jsxer如何成为Adobe脚本开发者的得力伙伴