做 Flutter 开发这两年,要说哪个功能最容易被低估开发量,我第一个想到的就是微信登录。表面上看它只是一句"调起微信、用户点一下确认、回调里拿到 code",真正动手做的时候:开放平台审核、包名签名绑定、iOS 的 URL Scheme、Android 的回调 Activity、再加上 Flutter 与原生之间的通信时序,每一层都能让你半夜对着日志发呆。这篇就完整记录一次 Flutter 在 Android/iOS 双端集成微信登录的全过程,包含我踩过的坑和排查思路,给正要接这个功能的人一份可以直接照做的实操记录。
1. 微信登录的完整链路:前端只是其中一环
1.1 微信 OAuth 授权码模式的移动端版本
微信登录本质上走的是 OAuth 2.0 的 Authorization Code(授权码)模式,但移动端和网页端在执行路径上有很大区别。先把这条链路画清楚,后面排查问题才有地图。
完整流程是这样的:
- 用户点击登录按钮,Flutter 通过插件调起微信 SDK
- 微信 App 被唤起,展示授权确认页面
- 用户在微信里点"允许"
- 微信通过回调把 auth_code 交还给你的 App
- 你的 App 把 code 发给自己的后端
- 后端拿 appid、secret、code 去微信服务器换取 access_token、openid、unionid
- 后端按 user 维度建立账号或登录态,返回自定义 token
- 前端保存 token,完成登录
这里最核心的认知点是:access_token 不应该出现在前端。很多团队第一次做的时候习惯把 token 全放前端,然后发现微信的 access_token 有刷新机制、有有效期限制,前端根本处理不干净。微信移动应用登录里,前端只跟 auth_code 打交道,拿到 code 就完成任务,剩下的脏活累活都应该放在后端。
授权码本身也有讲究:code 有效期只有五分钟,而且只能使用一次。这意味着用户如果点了一次登录拿到 code,请求后端失败了,第二次不能拿同一个 code 重试,只能重新拉起微信再授权一次。这个细节在联调的时候特别容易让人困惑,你会觉得"我刚明明拿到 code 了,为什么后端说无效",大概率就是之前已经消费过一次了。
1.2 前端、SDK、后端的分工边界
从分层视角来看,微信登录的职责分配是这样的:
- 原生 SDK 层:负责和微信 App 通信。Android 上通过 Intent 跳到微信的授权 Activity,iOS 上通过 URL Scheme 唤起微信,然后把用户的选择结果(code 或 errCode)通过插件通道传回 Flutter。
- Flutter 插件层:把原生回调封装成 Dart 的 Future 或 Stream,屏蔽平台差异。
- Dart 业务层:负责发起登录、处理回调、调用后端接口、保存登录态。
这里面容易被忽略的是第二层和第三层之间的"回调时序"问题。微信登录插件正是依赖原生到 Flutter 的通信通道来完成回调的——具体来说,是 MethodChannel 负责 Dart 调用原生,EventChannel 负责把原生的回调结果推回 Dart。如果你把登录结果的监听放在一个会被销毁的 Widget 里,页面一重建,回调就可能"看起来丢了",实际是监听者已经不存在了。
所以我的建议是:登录相关的逻辑不要放在页面级 Widget 的 State 里,而是放到一个全局的 AuthController 或者 ChangeNotifier 里面,这样能保证回调产生时监听者一定还存在。
1.3 移动端登录和扫码登录的关键差异
如果团队同时要做 PC 端"微信扫码登录",千万别拿 App 登录的流程硬套。扫码登录是开放平台的"网站应用",交互方式是网页先展示二维码,再轮询扫码结果;移动端登录是 SDK 直呼微信,不存在轮询。两者在开放平台创建的应用类型就不同:一个是"移动应用",一个是"网站应用",需要分别申请。
这个区分看起来基础,但我真见过有人申请了网站应用,想在 App 里调微信登录,调了半天也没调起来——因为在微信那边,你的 App 身份根本没被登记。还有一个容易混淆的点是个人微信和企业微信:个人微信登录走的是微信开放平台这套流程,企业微信登录完全是另一套体系,参数、回调、SDK 都不一样,别指望换一个 AppID 就能通用。
2. 开放平台注册与参数绑定:包名、签名、应用名一个都不能错
2.1 开通开发者账号与创建移动应用
要调用微信登录,第一步是去微信开放平台注册开发者账号。注意这里有几个"微信"平台的区分:
- 微信公众平台(mp.weixin.qq.com):管公众号、小程序
- 微信开放平台(open.weixin.qq.com):管 App 接入、网站应用、第三方平台
- 微信商户平台(pay.weixin.qq.com):管支付
微信登录属于开放平台,不在公众平台。很多第一次做的人会跑到公众平台里找 App 登录入口,找不到就开始怀疑人生。注册时需要准备邮箱,以及主体信息(个人或企业)。个人主体的开发者账号也能接入 App 登录,但某些需要高级权限的能力会受限,所以企业项目建议直接用企业主体注册。
在开放平台里创建移动应用后,需要填写应用名称、简介、图标、下载链接。这里有一个不少团队踩过的坑:同一个 App 的 Android 和 iOS 版要在开放平台建两个移动应用,分别拿到各自的 AppID 和 AppSecret。代码里 Android 用 Android 的 AppID,iOS 用 iOS 的 AppID。如果图省事只建了一个,另一个平台调微信登录时就会出问题。
2.2 包名、签名的绑定逻辑
微信开放平台创建 Android 应用时,会要求填两个关键信息:包名(PackageName)和签名(MD5)。
- 包名:对应 Android 工程里的 applicationId
- 签名:对应 APK 签名证书的 MD5 值(去掉冒号、大写)
微信为什么要绑这两个信息?因为 Android 的 App 身份就是由包名和签名共同确定的。拿包名相同但签名不同的 APK 安装到同一台手机上,系统都会当成另一个应用。微信在收到调用请求时,会校验调用方的包名和签名是否和开放平台登记的一致,不一致就直接拒绝。
签名这块最经典的坑是:开发时用 debug 签名调试成功了,发版时切到 release 签名,微信登录瞬间失效。debug keystore 和 release keystore 是两套完全不同的证书,MD5 值不一样。正确的做法是:
- 正式项目从一开始就用 release 签名跑联调(可以在 build.gradle 里把 debug 的 signingConfig 指向 release)
- 或者按环境申请两个 AppID:debug 环境一套,release 环境一套,代码里根据 BuildConfig.DEBUG 切换
- 实在只有一套 AppID,那就以线上正式包签名为准,开发期安装的调试包必须使用相同签名的 keystore
2.3 获取签名 MD5 的正确姿势
获取签名 MD5 有几种方式,我按可靠程度排序。
方式一:微信官方"签名获取工具"App。直接在应用市场搜,输入包名就能显示该包名当前安装版本的签名 MD5。这个工具显示的是手机上已安装应用的签名,最直观。
方式二:keytool 命令行。如果是 release keystore:
keytool -exportcert -alias your_alias -keystore release.keystore | openssl dgst -md5输出会带(stdin)=前缀,把这部分去掉,再去除冒号、转大写,就是微信要的 32 位 MD5 字符串。
方式三:用apksigner verify --print-certs从 APK 里提取签名证书信息,再手动算 MD5。
需要注意,微信这里用的算法是MD5,不是 SHA1 也不是 SHA256。Android 应用市场很喜欢用 SHA 系列,微信偏偏要求 MD5,两者别搞混。我去开放平台填签名的时候,就不止一次见过同事把 SHA1 值贴上去,然后调一晚上都调不通。
2.4 审核和测试期间的注意点
新创建的移动应用默认是"开发中"状态,需要提交审核,审核通过后 AppID 才能正式使用。审核注意事项:
- 应用需要有下载链接或应用市场页面,App Store、应用宝、官网链接都行,不能是空链接
- 必须提供包含"微信登录"入口的应用截图
- 审核周期一般 3 到 7 个工作日,不是实时的,项目排期要把这段时间算进去
如果没有线上下载链接,可以先在开放平台传一个测试 APK,过审后再改成正式链接。这里建议提前准备好材料,因为审核一旦被打回,修改后重新提交又是几个工作日,很影响排期。
3. 依赖与初始化:插件选型、双端配置文件
3.1 插件选型理由
Flutter 里做微信登录,社区最常用的是flutter_wechat_auth。选它而不是其他插件的理由:
- 它对 Android/iOS 的原生微信 SDK 做了统一封装,支持调用微信登录、分享等能力
- 内部通过 MethodChannel 调用原生,通过 EventChannel 持续接收回调结果,Flutter 侧拿到的 API 是异步 Future,符合常规使用习惯
- 相比早期常用的
fluttter_wechat和wechat_kit,flutter_wechat_auth的维护状态更活跃,对较新的 Flutter SDK 适配更快一些
在pubspec.yaml里加依赖:
dependencies: flutter_wechat_auth: ^1.3.1然后执行flutter pub get。要注意插件的原生 SDK 版本与 Flutter SDK 版本存在隐性绑定。如果你用的是比较新的 Flutter(3.x 系列),构建时出现类似 "the current configured flutter sdk is not known to be fully supported" 的提示,不要急着升级 Flutter,先确认插件版本是否兼容,或者查一下插件是否有对应新 SDK 的更新版本。
3.2 Android 侧配置顺序
微信 SDK 在 Android 上有几个硬性条件,按顺序排查。
第一步,核对 applicationId。打开android/app/build.gradle,确认applicationId与开放平台登记的包名完全一致,一个字符都不能差。很多项目会在不同 buildType 里加applicationIdSuffix,如果加了.debug后缀,包名就变了,微信授权会失败。
第二步,设置 minSdkVersion。微信 SDK 对 minSdk 有要求,建议不小于 21。具体配置如下:
defaultConfig { applicationId "com.example.myapp" minSdkVersion 21 targetSdkVersion 34 }第三步,改 AndroidManifest.xml。需要在<application>节点里添加微信回调 Activity 和 AppID 配置:
<application> <meta-data android:name="WECHAT_APPID" android:value="wxa1234567890abcdef" /> <activity android:name="com.tencent.wechat.sdk.WechatAuthActivity" android:exported="true" android:launchMode="singleTask" /> </application>第四步,处理 Android 11 的包可见性。如果 targetSdkVersion 是 30 及以上,还需要在 Manifest 里声明微信的包名,否则会出现调不起微信的问题:
<queries> <package android:name="com.tencent.mm" /> </queries>第五步,混淆规则。release 包开了 Proguard/R8 的话,需要在android/app/proguard-rules.pro里加 keep 规则:
-keep class com.tencent.wxop.** { *; } -keep class com.tencent.wechat.sdk.** { *; }不加的话,release 包一混淆,微信 SDK 的类可能被裁剪或改名,表现为 debug 正常、线上授权没反应。
另外提一句,很多项目在升级 AGP 后会在构建日志里看到 "You are applying Flutter's main Gradle plugin imperatively using the apply script" 这行警告。这个警告本身不影响微信登录功能,但如果你同时改过 Gradle 配置,要确认 Flutter 插件正常应用了,否则原生侧代码不会被编译进去,这属于构建层面的问题,优先级在"能跑起来"之后再去管。
3.3 iOS 侧配置
iOS 的配置比 Android 啰嗦一些,但逻辑其实就几条。
第一,Podfile 的平台版本。打开ios/Podfile,确认:
platform :ios, '12.0'微信 SDK 对 deployment target 有要求,太低会编译不过或者出现 API 冲突。
第二,Info.plist 里的 LSApplicationQueriesSchemes。在ios/Runner/Info.plist里加:
<key>LSApplicationQueriesSchemes</key> <array> <string>weixin</string> </array>这个字段用于检测微信是否安装。少了它,canOpenURL返回 false,登录按钮点击后 Flutter 侧会直接走"微信未安装"分支。
第三,URL Types。在 Xcode 的 TARGETS → Runner → Info → URL Types 里添加一个 URL Scheme,格式是wx+ 你的 AppID。例如 AppID 是wxa1234567890abcdef,URL Type 的 Scheme 就是wxwxa1234567890abcdef(全小写)。这一步等效于往 Info.plist 里加 CFBundleURLTypes:
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLName</key> <string>wechat</string> <key>CFBundleURLSchemes</key> <array> <string>wxwxa1234567890abcdef</string> </array> </dict> </array>第四,回调处理。iOS 13 之后如果项目用了 SceneDelegate,微信回调的 openURL 可能不会自动回到原来的路径,插件一般会在 AppDelegate 和 SceneDelegate 里都做 hook。如果你自己改过这两个文件,注意别把插件的回调处理覆盖掉。
第五,universalLink 参数。插件初始化时要求传 universalLink。如果你只做微信登录,这个参数可以暂时填一个占位值;但如果后续要做微信分享、拉起小程序,Universal Links 就得认真配置。在苹果开发者后台关联域名,把 apple-app-site-association 文件放到服务器对应路径,然后把完整链接传给插件。这个配置比较繁琐,但值得提前规划,因为微信开放平台的 AppID 一旦确定,后面改 Universal Links 域名还要重新走微信侧的校验流程。
3.4 初始化代码与通信回调机制
配置文件折腾完,回到 Dart 侧。在main.dart里尽早初始化插件:
import 'package:flutter_wechat_auth/flutter_wechat_auth.dart'; Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await WechatAuth.instance.registerApp( appId: 'wxa1234567890abcdef', universalLink: 'https://your.domain.com/app/', ); runApp(const MyApp()); }registerApp要在应用启动早期完成,不要放到某个页面里才调用。微信登录的回调是基于全局通道的,等到页面级再注册,可能错过回调或者出现状态错乱。
微信 SDK 回调原生后,插件通过 EventChannel 把结果推回 Dart。这个链路本身是稳定的,但前提是 Dart 侧的回调监听器要在合适的时机注册好。好消息是loginWithWechat()返回的是一个 Future,相当于把"发起登录"和"等待回调"封装在了一起,不需要额外手动注册 Stream 监听。
4. 登录态设计:从"调起微信"到"拿到后端 session"
4.1 核心登录方法代码示例与关键行解释
这是登录方法的骨架:
Future<LoginResult> wxLogin() async { if (!await WechatAuth.instance.isWechatInstalled()) { return LoginResult.fail('未检测到微信,请先安装微信'); } final resp = await WechatAuth.instance.loginWithWechat(); if (resp.errCode == 0 && resp.code != null) { return await _exchangeCodeForSession(resp.code!); } else { return LoginResult.fail(_mapWechatError(resp.errCode)); } }几个容易被忽略的点:
isWechatInstalled()会走原生 canOpenURL 或 package manager 查询。如果返回 false,优先怀疑 LSApplicationQueriesSchemes 或 queries 配置,而不是微信真的没装。resp.code是授权码,有效期 5 分钟,只能使用一次。用户如果连续点两次登录,微信每次都会重新产生 code,之前的 code 立即作废。errCode == 0只是"微信侧授权成功",并不等于"登录成功"。真正的成功标志是后端完成了 code 换 token,并返回了你自己的登录态。
后端接口调用示例:
Future<LoginResult> _exchangeCodeForSession(String code) async { try { final data = await dio.post( '/api/auth/wechat-login', data: {'code': code, 'platform': Platform.isIOS ? 'ios' : 'android'}, ); final token = data['token'] as String; await _saveToken(token); return LoginResult.success(user: data['user']); } catch (e) { return LoginResult.fail('登录失败:$e'); } }注意 platform 参数建议传。因为微信开放平台里 Android 和 iOS 是两个应用,后端换 token 时用的 appid 不同,加上这个字段,后端处理起来会舒服很多。
4.2 用户点击登录后发生了什么:状态机设计
很多初学者只把"代码写完"当成功,但在真实 App 里,登录按钮要处理的远不止一个返回值。我把整个交互拆成几个状态:
- idle:未登录,按钮可点
- invoking:正在调起微信(Android 上是 Activity 跳转,iOS 上是 URL 打开,都是从当前 App 切出去)
- waitingBack:已经切到微信,等待用户授权后回跳
- exchanging:拿到 code,正在请求后端
- success:拿到了 session,进入已登录界面
- failure:失败或用户取消,回到 idle,给用户一个"再次尝试"的入口
其中waitingBack最容易出问题,因为这个阶段 App 可能已经退到后台,甚至被系统回收。我的建议是,所有跟"等待"相关的 UI 变化都要放在状态管理容器里,而不是放在某个页面的局部 State。微信登录的回调不会因为 Navigator push 而丢失,因为 Future 是在调用这个方法的地方挂起的;但如果整个页面被 dispose,而状态只存在于那个页面里,回调确实可能"看起来丢了"。
用 Riverpod 举例:
final authControllerProvider = StateNotifierProvider<AuthController, AuthState>((ref) => AuthController()); class AuthController extends StateNotifier<AuthState> { Future<void> loginWithWechat() async { state = AuthState.loading(); final result = await wxLogin(); result.ok ? state = AuthState.authenticated(result.user!) : state = AuthState.error(result.message); } }页面里监听这个 state,按钮 loading、错误提示、登录后跳转都由 state 驱动。这样即使中间发生了页面跳转或 Widget 重建,登录流程的状态不会丢。
4.3 token 存储与后续请求附带
登录成功后,Flutter 侧保存的是后端返回的自定义登录态,而不是微信的 openid 或 access_token。原因很直接:
- 后端 token 有自己的时效和刷新机制,前端只管"带着它访问接口"
- 微信的 access_token 应当只存在于后端,前端保存它既浪费又危险
存储建议用flutter_secure_storage,它在 iOS 上走 Keychain,Android 上走 EncryptedSharedPreferences,比明文存储安全一个档次:
const storage = FlutterSecureStorage(); await storage.write(key: 'session_token', value: token);每次请求在拦截器里附带:
dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) async { final token = await storage.read(key: 'session_token'); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } return handler.next(options); }, ), );冷启动时先读 token,再去后端校验有效性。有效就直接进主界面,不需要再弹微信;无效再走登录流程。这里有个产品层面的原则:不要自动静默调起微信登录,用户没点按钮之前,App 不应该自己唤起微信。这既是体验问题,也是微信开放平台审核时会关注的行为。
4.4 退出登录:一个容易想多的地方
微信登录机制里的"登录"是一次授权关系的确立。用户在微信里同意之后,你的 App 在微信侧就有了长期授权关系;除非用户在微信的"隐私-授权管理"里主动解除,否则后续发起新的登录授权时,微信可能直接返回一个 code,不再每次弹确认页(具体取决于微信版本和用户设置)。
所以 App 内的"退出登录"实际只需要做两件事:清掉本地 token、清掉内存中的用户状态。不要尝试调用微信接口来"撤销登录",微信开放平台并没有提供面向 App 内退出登录的通用接口。如果你确实想让用户完全断开微信授权,只能引导用户去微信的授权管理里操作。
5. 双端实测中的坑与排查思路(含日志定位方法)
5.1 Android 高频错误 1:调起微信提示"应用未注册"或直接无反应
这是我见过次数最多的错误。现象:点击"微信登录",微信 App 被唤起,但弹出"应用未注册"提示,或者干脆白屏一下然后没有反应。
排查顺序:
- 核对开放平台包名。打开开放平台 App 详情,确认绑定的包名和当前 build.gradle 里的 applicationId 严格一致,注意大小写。
- 核对签名 MD5。用签名工具或 keytool 算出的 MD5 与开放平台登记的一致。
- 检查 Manifest 里的 WECHAT_APPID 是否一致。同一个包名下只能有一个微信应用,值必须是开放平台分配的那个 wx 开头的 AppID。
- 如果以上都对,考虑签名缓存问题。微信 App 会缓存它见过的应用签名,如果你之前在开放平台填的签名和现在包里实际签名不同,微信端会按"未注册"处理。更换签名后的调试包,建议先清除微信 App 的缓存数据再重试。
日志定位法,在终端跑:
adb logcat -s WechatSDK WechatAuthActivity复现点击操作,正常流程里能看到微信 SDK 打印出调用方包名和签名信息。如果日志显示签名匹配失败,基本就是签名问题,不用再找别的原因。
5.2 iOS 高频错误 2:点击按钮回到微信,授权页一闪而过或回调不了
iOS 上最常见的翻车点不在 Dart 代码,而在 Xcode 配置。
现象一:点击登录后直接返回"微信未安装"。优先检查 LSApplicationQueriesSchemes 里有没有weixin,其次检查真机是否真的装了微信。
现象二:微信打开了,但授权页没出来,或者授权完成回不来。检查 URL Types:Scheme 必须是wx+ 完整 AppID,全部小写。大小写错、少一位、多一位,回调都不通。
现象三:能授权,但 Dart 侧收不到结果。检查 AppDelegate / SceneDelegate 里的 openURL 处理是否被自己覆盖了。插件往往通过运行时 hook 或依赖 Flutter 的 deep linking 机制来分发回调,如果你手写了一个application(_:open:options:)然后没有走 Flutter 的默认处理,回调就会断在原生层。解决办法是保留插件默认的 AppDelegate 配置,不要做多余拦截。
日志定位法:在 Xcode 的 Console 里按进程过滤,或者直接在 AppDelegate 里打日志,确认openURL有没有触发、url 内容是什么。如果触发了但格式不对,立刻就能定位是 URL Type 的问题。
5.3 跨端通用坑
模拟器问题。Android 模拟器上可以装微信但登录授权受限比较严重;iOS 模拟器装微信也不方便。微信登录这种涉及真实 App 跳转的功能,强烈建议直接真机调试,这是效率最高的做法。
Proguard 混淆。Android release 包出现登录无反应,优先怀疑混淆规则,按前面第 3 节加 keep 规则再打包。
多环境 AppID 串用。项目同时有 dev、prod 环境,不要图省事全用同一个 AppID。否则你在 dev 环境测试时,微信返回的用户身份会被后端和 prod 环境混在一起,账号体系会乱。更稳妥的是每个环境在微信开放平台单独建应用,各用各的包名和签名。
后端验签失败。前端链路全通、用户在微信里也点了允许、code 也拿到了,但后端换 token 失败。检查后端用的 appid 和 secret 是否和当前移动应用一致,微信接口是否用了 POST,code 是否已经被前一次请求消费。一个 code 只能用一次,第二次请求会报无效或已使用。
微信版本过旧。如果用户手机上的微信版本太老,调起登录时也可能出现异常行为。可以在登录按钮页面加一个版本检测提示,但微信本身会做大部分兼容处理,这个坑属于少数情况,遇到了优先换新版本微信测试。
5.4 一个典型的"日志定位法"实操
我自己的排查习惯是固定一个流程,遇到问题按顺序过:
- 确认 AppID 正确:
wx开头,长度和字符别错。 - 确认基础能力:Android 看 Manifest,iOS 看 Info.plist 和 URL Types。
- 确认开放平台配置:包名、签名、平台类型。
- 真机复现,采集原生日志。
- Flutter 侧打点:在调用
loginWithWechat()前后各打一条日志,记录返回值 errCode。
这个流程走完,90% 的问题都能定位。剩下那 10% 通常和微信 App 版本、系统限制有关,换台手机、换个微信号试一下往往就有结果。
这个定位流程是我从几次深夜排查里总结出来的。前两次遇到微信登录问题,都是在"为什么代码没问题但功能不起来"上卡了很久,最后才发现少配了一个 queries 或者填错了签名。微信登录这种东西,真不是写完 Dart 代码就算完,把开放平台后台和双端配置文件当成正式环境来管理,才能减少那类"昨天还好好的今天就不行了"的惊悚事件。最后分享一个小习惯:每次改过签名、包名、AppID 之后,我都会把真机上微信的缓存清掉再测试。微信对应用身份的缓存很顽固,不清理的话,你新配的签名可能还是旧值,容易误导排查方向。