【问题标题】:Flutter 中的路由守卫
【发布时间】:2018-12-04 06:54:44
【问题描述】:

在 Angular 中,可以使用canActivate 进行路由保护。

在 Flutter 中,我们将如何处理它?守卫放在哪里?你如何保护路线?

我的想法是这样的:

  • 用户已登录。他们的令牌存储在 Shared Preference 中(存储令牌的正确方式?)
  • 用户关闭了应用。
  • 用户再次打开应用程序。当应用程序启动时,它会确定用户是否已登录(可能是一个检查存储以获取令牌的服务),然后
  • 如果已登录,则加载主页路由
  • 如果未登录,请加载登录页面

【问题讨论】:

    标签: dart flutter


    【解决方案1】:

    我也被这个问题绊倒了,最终为此使用了FutureBuilder。看看我的路线:

    final routes = {
      '/': (BuildContext context) => FutureBuilder<AuthState>(
        // This is my async call to sharedPrefs
        future: AuthProvider.of(context).authState$.skipWhile((_) => _ == null).first,
        builder: (BuildContext context, AsyncSnapshot<AuthState> snapshot) {
          switch(snapshot.connectionState) {
            case ConnectionState.done:
              // When the future is done I show either the LoginScreen 
              // or the requested Screen depending on AuthState
              return snapshot.data == AuthState.SIGNED_IN ? JobsScreen() : LoginScreen()
            default:
              // I return an empty Container as long as the Future is not resolved
              return Container();
          }
        },
      ),
    };
    

    如果您想跨多个路由重用代码,您可以扩展 FutureBuilder。

    【讨论】:

    • 这让我想到了 Angular 路由器守卫接受 observables 我一直认为它们是流(注销 > 登录 > 注销 > 等),但据我所知,你需要一个 StreamBuilder
    • 很好的解决方案。提供强大的动力。
    • 如何使用locator&lt;NavigationService&gt;().navigateTo(LoginViewRoute); insted of JobsScreen()
    【解决方案2】:

    我认为本身没有路由保护机制,但您可以在加载应用程序之前在main 函数中执行逻辑,或者使用MaterialApponGenerateRoute 属性。在您的情况下,一种方法是等待一个异步函数,该函数在加载初始路由之前检查用户是否已登录。类似的东西

    main() {
      fetchUser().then((user) {
        if (user != null) runApp(MyApp(page: 'home'));
        else runApp(MyApp(page: 'login'));
      });
    }
    

    但您可能也对 Shrine 应用的执行方式感兴趣。在任何情况下,他们都将登录页面作为初始路由,如果用户登录则将其删除。这样,用户会看到登录页面,直到确定他们是否登录。我已经包含了relevant snippet下面。

    class ShrineApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Shrine',
          home: HomePage(),
          initialRoute: '/login',
          onGenerateRoute: _getRoute,
        );
      }
    
      Route<dynamic> _getRoute(RouteSettings settings) {
        if (settings.name != '/login') {
          return null;
        }
    
        return MaterialPageRoute<void>(
          settings: settings,
          builder: (BuildContext context) => LoginPage(),
          fullscreenDialog: true,
        );
      }
    }
    

    如果您不希望他们在登录后看到登录页面,请使用第一种方法,您可以通过探索 this answer 来控制在 runApp 具有 UI 之前显示的启动屏幕。

    【讨论】:

    • 有趣。会试一试的。
    • 如果您想本地化应用程序,您必须小心在延迟后调用runApp(例如在Future 回调中)。当runApp 未被立即调用时,Flutter 无法切换语言环境或加载其他语言环境。否则,我认为这个答案很棒!
    • @boformer 在这种情况下,最好将加载屏幕作为初始路线,并在知道用户是否经过身份验证后选择去哪里。
    【解决方案3】:

    您可以使用auto_route 扩展名。这个扩展非常适合处理路由,并提供了使用guards 的好方法。请参考文档: https://pub.dev/packages/auto_route#route-guards

    【讨论】:

    • 谢谢。这绝对是我会记住的。
    • 虽然您提供了可能的解决方案,但不鼓励仅提供链接的答案。与您的情况一样,链接似乎已更改并且不再指向路由守卫,因此答案无助于理解如何实现问题的目标。请更新链接并提供相应代码的解释。
    【解决方案4】:

    我为一个网络项目提出了以下解决方案,它使我能够轻松引入受保护的路由,而不必担心未经授权的用户能够访问敏感信息。

    GuardedRoute 类如下所示:

    import 'package:flutter/material.dart';
    import 'package:flutter/widgets.dart';
    import 'package:kandabis_core/core.dart' as core;
    
    Widget _defaultTransitionsBuilder(
        BuildContext context,
        Animation<double> animation,
        Animation<double> secondaryAnimation,
        Widget child) {
    
        return child;
    }
    
    class GuardedRoute extends PageRouteBuilder {
    
        GuardedRoute({
            @required final String guardedRoute,
            @required final String fallbackRoute,
            @required final Stream<bool> guard,
            @required final core.Router router,
            final RouteTransitionsBuilder transitionsBuilder = _defaultTransitionsBuilder,
            final bool maintainState = true,
            final Widget placeholderPage,
        })
        : super(
            transitionsBuilder: transitionsBuilder,
            maintainState: maintainState,
            pageBuilder: (context, animation, secondaryAnimation) =>
    
                StreamBuilder(
                    stream: guard,
                    builder: (context, snapshot) {
    
                        if (snapshot.hasData) {
    
                            // navigate to guarded route
                            if (snapshot.data == true) {
    
                                return router.routes[guardedRoute](context);
                            }
    
                            // navigate to fallback route
                            return router.routes[fallbackRoute](context);
                        }
    
                        // show a placeholder widget while the guard stream has no data yet
                        return placeholderPage ?? Container();
                    }
                ),
        );
    }
    

    使用受保护的路由很容易。您可以定义受保护的路由和回退路由(如登录页面)。 Guard 是一个 Stream,它决定用户是否可以导航到受保护的路线。这是我的路由器类,它展示了如何使用 GuardedRoute 类:

    class BackendRouter extends core.BackendRouter {
    
        BackendRouter(
            this._authenticationProvider,
            this._logger
            );
    
        static const _tag = "BackendRouter";
    
        core.Lazy<GlobalKey<NavigatorState>> _navigatorKey =
    
            core.Lazy(() => GlobalKey<NavigatorState>());
    
        final core.AuthenticationProvider _authenticationProvider;
        final core.Logger _logger;
    
        @override
        Map<String, WidgetBuilder> get routes => {
    
            core.BackendRoutes.main: (context) => MainPage(),
            core.BackendRoutes.login: (context) => LoginPage(),
            core.BackendRoutes.import: (context) => ImportPage(),
        };
    
        @override
        Route onGenerateRoute(RouteSettings settings) {
    
            if (settings.name == core.BackendRoutes.login) {
    
                return MaterialPageRoute(
                    settings: settings,
                    builder: routes[settings.name]
                );
            }
    
            return _guardedRoute(settings.name);
        }
    
        @override
        GlobalKey<NavigatorState> get navigatorKey => _navigatorKey();
    
        @override
        void navigateToLogin() {
    
            _logger.i(_tag, "navigateToLogin()");
    
            navigatorKey
                .currentState
                ?.pushNamed(core.BackendRoutes.login);
        }
    
        @override
        void navigateToImporter() {
    
            _logger.i(_tag, "navigateToImporter()");
    
            navigatorKey
                .currentState
                ?.pushReplacement(_guardedRoute(core.BackendRoutes.import));
        }
    
        GuardedRoute _guardedRoute(
             String route,
             {
                 maintainState = true,
                 fallbackRoute = core.BackendRoutes.login,
             }) =>
    
             GuardedRoute(
                 guardedRoute: route,
                 fallbackRoute: fallbackRoute,
                 guard: _authenticationProvider.isLoggedIn(),
                 router: this,
                 maintainState: maintainState,
                 placeholderPage: SplashPage(),
             );
    }
    

    您的应用程序类如下所示:

    class BackendApp extends StatelessWidget {
    
        @override
        Widget build(BuildContext context) {
    
            // get router via dependency injection
            final core.BackendRouter router = di.get<core.BackendRouter>();
    
            // create app
            return MaterialApp(
                onGenerateRoute: (settings) => router.onGenerateRoute(settings),
                navigatorKey: router.navigatorKey,
            );
        }
    }
    

    【讨论】:

    • 如何使用locator&lt;NavigationService&gt;().navigateTo(LoginViewRoute); insted of router.routes[guardedRoute](context)
    【解决方案5】:

    我的解决方案是构建一个路由保护系统,就像其他库一样,但我们仍然可以在需要的地方使用原始导航器,打开一个模式作为命名路由,链保护,并添加重定向。它非常基础,但可以很容易地构建。

    看起来很多,但你只需要 3 个新文件来维护你的新守卫:

    - router/guarded_material_page_route.dart
    - router/route_guard.dart
    - router/safe_navigator.dart
    
    // Your guards go in here
    - guards/auth_guard.dart
    ...
    

    首先创建一个扩展MaterialPageRoute 的新类,或者如果您像我一样想要打开Modal Bottom Sheet packageMaterialWithModalsPageRoute。我已经调用了我的 GuardedMaterialPageRoute

    class GuardedMaterialPageRoute extends MaterialWithModalsPageRoute {
      final List<RouteGuard> routeGuards;
    
      GuardedMaterialPageRoute({
        // ScrollController is only needed if you're using the modals, as i am in this example.
        @required Widget Function(BuildContext, [ScrollController]) builder,
        RouteSettings settings,
        this.routeGuards = const [],
      }) : super(
        builder: builder,
        settings: settings,
      );
    }
    

    您的路线守卫将如下所示:

    class RouteGuard {
      final Future<bool> Function(BuildContext, Object) guard;
    
      RouteGuard(this.guard);
    
      Future<bool> canActivate(BuildContext context, Object arguments) async {
        return guard(context, arguments);
      }
    }
    

    您现在可以像这样将GuardedMaterialPageRoutes 添加到您的路由器文件中:

    class Routes {
      static Route<dynamic> generateRoute(RouteSettings settings) {
        switch (settings.name) {
          case homeRoute:
            // These will still work with our new Navigator!
            return MaterialPageRoute(
              builder: (context) => HomeScreen(),
              settings: RouteSettings(name: homeRoute),
            );
    
          case locationRoute:
            // Following the same syntax, just with a routeGuards array now.
            return GuardedMaterialPageRoute(
              // Again, scrollController is only if you're opening a modal as a named route.
              builder: (context, [scrollController]) {
                final propertiesBloc = BlocProvider.of<PropertiesBloc>(context);
                final String locationId = settings.arguments;
    
                return BlocProvider(
                  create: (_) => LocationBloc(
                    locationId: locationId,
                    propertiesBloc: propertiesBloc,
                  ),
                  child: LocationScreen(),
                );
              },
              settings: RouteSettings(name: locationRoute),
              routeGuards: [
                // Now inject your guards, see below for what they look like.
                AuthGuard(),
              ]
            );
         ...
    

    像上面那样创建你的异步保护类,就像我们在路由器中使用的那样。

    class AuthGuard extends RouteGuard {
      AuthGuard() : super((context, arguments) async {
        final auth = Provider.of<AuthService>(context, listen: false);
        const isAnonymous = await auth.isAnonymous();
        return !isAnonymous;
      });
    }
    

    现在您需要一个新的类来处理您的导航。在这里你检查你是否有访问权限并简单地穿过每个守卫:

    class SafeNavigator extends InheritedWidget {
    
      static final navigatorKey = GlobalKey<NavigatorState>();
    
      @override
      bool updateShouldNotify(SafeNavigator oldWidget) {
        return false;
      }
    
      static Future<bool> popAndPushNamed(
        String routeName, {
        Object arguments,
        bool asModalBottomSheet = false,
      }) async {
        Navigator.of(navigatorKey.currentContext).pop();
        return pushNamed(routeName, arguments: arguments, asModalBottomSheet: asModalBottomSheet);
      }
    
      static Future<bool> pushNamed(String routeName, {
        Object arguments,
        bool asModalBottomSheet = false,
      }) async {
        // Fetch the Route Page object
        final settings = RouteSettings(name: routeName, arguments: arguments);
        final route = Routes.generateRoute(settings);
    
        // Check if we can activate it
        final canActivate = await _canActivateRoute(route);
    
        if (canActivate) {
          // Only needed if you're using named routes as modals, under the hood the plugin still uses the Navigator and can be popped etc.
          if (asModalBottomSheet) {
            showCupertinoModalBottomSheet(
                context: navigatorKey.currentContext,
                builder: (context, scrollController) =>
                    (route as GuardedMaterialPageRoute)
                        .builder(context, scrollController));
          } else {
            Navigator.of(navigatorKey.currentContext).push(route);
          }
        }
    
        return canActivate;
      }
    
      static Future<bool> _canActivateRoute(MaterialPageRoute route) async {
        // Check if it is a Guarded route
        if (route is GuardedMaterialPageRoute) {
          // Check all guards on the route
          for (int i = 0; i < route.routeGuards.length; i++) {
            // Run the guard
            final canActivate = await route.routeGuards[i]
                .canActivate(navigatorKey.currentContext, route.settings.arguments);
    
            if (!canActivate) {
              return false;
            }
          }
        }
    
        return true;
      }
    }
    
    

    要使这一切正常工作,您需要将 SafeNavigator 密钥添加到您的 Material 应用程序:

    MaterialApp(
      navigatorKey: SafeNavigator.navigatorKey,
      ...
    )
    

    现在您可以导航到您的路线并检查您是否可以像这样访问它们:

    // Opens a named route, either Guarded or not.
    SafeNavigator.pushNamed(shortlistRoute);
    // Opens a named route as a modal
    SafeNavigator.pushNamed(shortlistRoute, asModalBottomSheet: true);
    // Pops the current route and opens a named route as a modal
    SafeNavigator.popAndPushNamed(shortlistRoute, asModalBottomSheet: true);
    

    【讨论】:

    • 无法从方法“generateRoute”返回“GuardedMaterialPageRoute”类型的值,因为它的返回类型为“Route
    猜你喜欢
    • 2018-11-14
    • 2019-02-04
    • 1970-01-01
    • 1970-01-01
    • 2017-10-01
    • 2021-06-01
    • 2018-01-10
    • 1970-01-01
    • 2019-03-26
    相关资源
    最近更新 更多