You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Web端替代InAppWebView实现SAML登录方案求助

Flutter Web端SAML身份认证替代方案

核心思路

由于flutter_inappwebview不支持Web平台,推荐使用官方维护的webview_flutter插件,它对Web端提供了基于iframe的实现,能适配你的SAML登录流程。

步骤1:添加依赖

在pubspec.yaml中替换原有依赖:

dependencies:
  flutter:
    sdk: flutter
  webview_flutter: ^4.4.0
  webview_flutter_web: ^0.2.2+1 # Web端适配包

步骤2:修改登录页面代码

替换原有InAppWebView相关实现,适配Web端:

import 'dart:convert';
import 'package:feeta/services/TokenManager.dart';
import 'package:feeta/views/dashboard/MyDashboard.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
// Web端需要导入适配包
import 'package:webview_flutter_web/webview_flutter_web.dart';

class LoginWebView extends StatefulWidget {
  const LoginWebView({super.key});

  @override
  State<LoginWebView> createState() => _LoginWebViewState();
}

class _LoginWebViewState extends State<LoginWebView> {
  final tokenManager = TokenManager();
  WebViewController? webViewController;
  bool isShowingError = false;
  final initialUrl = "https://accounts.google.com/o/saml2/initsso?idpid=C02qtgmcd&spid=859837136968&forceauthn=false";

  @override
  void initState() {
    super.initState();
    // 初始化Web端控制器
    if (kIsWeb) {
      WebViewPlatform.instance = WebWebViewPlatform();
    }
    // 创建控制器并配置核心参数
    webViewController = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..loadRequest(Uri.parse(initialUrl))
      ..setNavigationDelegate(
        NavigationDelegate(
          onProgress: onWebViewProgressChanged,
        ),
      );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Login"),
      ),
      body: Column(
        children: [
          Expanded(
            child: WebViewWidget(controller: webViewController!),
          ),
        ],
      ),
    );
  }

  void onWebViewProgressChanged(int progress) async {
    if (progress == 100 && webViewController != null) {
      String html = await getHtmlFromWebViewController(webViewController!);
      if (!isShowingError && html.contains('not_a_saml_app')) {
        isShowingError = true;
        showErrorDialog();
        return;
      }
      String token = extractTokenFromHTML(html);
      if (kDebugMode) {
        print("My Token: $token");
      }
      if (token.isNotEmpty) {
        tokenManager.storeToken(token);
        navigateToNewScreen();
      }
    }
  }

  void showErrorDialog() {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) {
        return AlertDialog(
          title: const Text("Login Error"),
          content: const Text(
              "Oops! It seems like you're trying to login with a personal or different email address. Please make sure to use your Lattice email ID for authentication."),
          actions: [
            TextButton(
              child: const Text("Retry"),
              onPressed: () {
                Navigator.pop(context);
                isShowingError = false;
                logout();
              },
            ),
          ],
        );
      },
    );
  }

  Future<String> getHtmlFromWebViewController(WebViewController controller) async {
    try {
      // Web端和移动端都支持该方法执行JS
      final result = await controller.runJavaScriptReturningResult(
          'document.documentElement.outerHTML;');
      return result.toString();
    } catch (e) {
      if (kDebugMode) {
        print('Error retrieving HTML: $e');
      }
      return '';
    }
  }

  String extractTokenFromHTML(String html) {
    try {
      String jsonString =
          html.substring(html.indexOf('{'), html.lastIndexOf('}') + 1);
      Map<String, dynamic> json = jsonDecode(jsonString);
      String _token = json['token'] as String;
      return _token;
    } catch (e) {
      if (kDebugMode) {
        print('Error extracting token from HTML: $e');
      }
      return '';
    }
  }

  void logout() async {
    if (webViewController != null) {
      // 清除缓存并重新加载登录页
      await webViewController!.clearCache();
      await webViewController!.clearLocalStorage(); // 额外清除本地存储
      webViewController!.loadRequest(Uri.parse(initialUrl));
    }
    if (kDebugMode) {
      print("came back on this method");
    }
  }

  void navigateToNewScreen() async {
    Navigator.pushReplacement(
      context,
      MaterialPageRoute(builder: (context) => const MyDashboard()),
    );
  }
}

关键注意事项

  • 跨域限制:Web端如果SAML登录页面的X-Frame-Options设置为DENY或SAMEORIGIN,会导致iframe无法加载。需要联系SAML服务提供商(如Google Workspace)配置允许你的Flutter Web域名嵌入,或者改用后端代理中转请求。
  • JavaScript权限:必须开启JavaScriptMode.unrestricted,否则无法执行获取HTML的脚本。
  • 缓存处理:Web端可根据需求添加本地存储清除逻辑,避免残留登录状态。

备选方案:浏览器重定向模式

如果iframe嵌入遇到跨域问题无法解决,可采用浏览器重定向方式:

  1. 使用url_launcher插件引导用户在外部浏览器打开SAML登录URL。
  2. SAML登录完成后,后端生成token并跳转回你的Flutter Web应用的回调URL(如https://yourapp.com/login-callback?token=xxx)。
  3. 在Flutter Web中监听URL参数,提取token并完成登录流程。

这种方式需要后端配合处理SAML回调,但能彻底避免iframe的跨域限制。

内容的提问来源于stack exchange,提问作者Saurabh Jain

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 02:25:11