React-navigation:与身份验证的深度链接

Posted

技术标签:

【中文标题】React-navigation:与身份验证的深度链接【英文标题】:React-navigation: Deep linking with authentication 【发布时间】:2019-06-18 10:41:02 【问题描述】:

我正在使用 react-native 和 react-navigation 库构建一个移动应用程序,用于管理我的应用程序中的导航。现在,我的应用看起来像这样:

App [SwitchNavigator]
    Splash [Screen]
    Auth [Screen]
    MainApp [StackNavigator]
        Home [Screen]            (/home)
        Profile [Screen]         (/profile)
        Notifications [Screen]   (/notifications)

我已经为屏幕HomeProfileNotifications 集成了深度链接与上述模式,并且它按预期工作。我面临的问题是如何在使用深层链接时管理用户的身份验证。现在,每当我打开深层链接(例如myapp://profile)时,无论我是否经过身份验证,应用程序都会将我带到屏幕上。我想要它做的是在AsyncStorage 之前检查是否有userToken,如果没有或它不再有效,那么只需在Auth 屏幕上重定向。

我以与here 描述的几乎完全相同的方式设置身份验证流程。因此,当我的应用程序启动 Splash 屏幕时,会检查用户的手机是否有有效令牌,并在 Auth 屏幕或 Home 屏幕上发送给他。

我现在想出的唯一解决方案是将每个深层链接指向Splash,验证我的用户,然后解析链接以导航到正确的屏幕。 例如,当用户打开myapp://profile 时,我在Splash 上打开应用程序,验证令牌,然后解析url (/profile),最后重定向到AuthProfile

这是这样做的好方法,还是 react-navigation 提供了更好的方法来做到这一点?他们网站上的Deep linking 页面有点亮。

感谢您的帮助!

【问题讨论】:

您是如何管理深度链接和身份验证流程的?每次我尝试打开深层链接(myApp://profile)时,它都会自动导航到主屏幕(因为我已通过身份验证) 【参考方案1】:

我的设置与您的类似。我按照Authentication flows · React Navigation 和SplashScreen - Expo Documentation 设置了我的身份验证流程,所以我有点失望的是,让深层链接通过它也是一个挑战。我可以通过自定义我的主开关导航器来完成这项工作,该方法类似于您所说的您现在拥有的解决方案。我只是想分享我的解决方案,所以有一个具体的例子来说明如何开始工作。我的主开关导航器是这样设置的(我也在使用 TypeScript,所以如果不熟悉类型定义,请忽略它们):

const MainNavigation = createSwitchNavigator(
  
    SplashLoading,
    Onboarding: OnboardingStackNavigator,
    App: AppNavigator,
  ,
  
    initialRouteName: 'SplashLoading',
  
);

const previousGetActionForPathAndParams =
  MainNavigation.router.getActionForPathAndParams;

Object.assign(MainNavigation.router, 
  getActionForPathAndParams(path: string, params: any) 
    const isAuthLink = path.startsWith('auth-link');

    if (isAuthLink) 
      return NavigationActions.navigate(
        routeName: 'SplashLoading',
        params:  ...params, path ,
      );
    

    return previousGetActionForPathAndParams(path, params);
  ,
);

export const AppNavigation = createAppContainer(MainNavigation);

您要通过身份验证流程路由的任何深层链接都需要以 auth-link 开头,或者您选择的任何前缀。这是SplashLoading 的样子:

export const SplashLoading = (props: NavigationScreenProps) => 
  const [isSplashReady, setIsSplashReady] = useState(false);

  const _cacheFonts: CacheFontsFn = fonts =>
    fonts.map(font => Font.loadAsync(font as any));

  const _cacheSplashAssets = () => 
    const splashIcon = require(splashIconPath);
    return Asset.fromModule(splashIcon).downloadAsync();
  ;

  const _cacheAppAssets = async () => 
    SplashScreen.hide();
    const fontAssetPromises = _cacheFonts(fontMap);
    return Promise.all([...fontAssetPromises]);
  ;

  const _initializeApp = async () => 
    // Cache assets
    await _cacheAppAssets();

    // Check if user is logged in
    const sessionId = await SecureStore.getItemAsync(CCSID_KEY);

      // Get deep linking params
    const params = props.navigation.state.params;
    let action: any;

    if (params && params.routeName) 
      const  routeName, ...routeParams  = params;
      action = NavigationActions.navigate( routeName, params: routeParams );
    

    // If not logged in, navigate to Auth flow
    if (!sessionId) 
      return props.navigation.dispatch(
        NavigationActions.navigate(
          routeName: 'Onboarding',
          action,
        )
      );
    

    // Otherwise, navigate to App flow
    return props.navigation.navigate(
      NavigationActions.navigate(
        routeName: 'App',
        action,
      )
    );
  ;

  if (!isSplashReady) 
    return (
      <AppLoading
        startAsync=_cacheSplashAssets
        onFinish=() => setIsSplashReady(true)
        onError=console.warn
        autoHideSplash=false
      />
    );
  

  return (
    <View style= flex: 1 >
      <Image source=require(splashIconPath) onLoad=_initializeApp />
    </View>
  );
;

我使用routeName 查询参数创建深层链接,这是执行身份验证检查后要导航到的屏幕的名称(您显然可以添加您需要的任何其他查询参数)。由于我的SplashLoading 屏幕处理加载所有字体/资产以及身份验证检查,因此我需要每个深层链接来路由它。我遇到的问题是,我会手动退出应用程序的多任务处理,点击深层链接 url,然后应用程序崩溃,因为深层链接绕过了 SplashLoading,所以没有加载字体。

上面的方法声明了一个action 变量,如果没有设置它将什么都不做。如果routeName 查询参数不是undefined,我设置action 变量。这使得一旦 Switch 路由器根据 auth(OnboardingApp)决定采用哪条路径,该路由就会在退出 auth/splash 加载流程后获取子操作并导航到 routeName

这是我创建的一个示例链接,它在这个系统上运行良好: exp://192.168.1.7:19000/--/auth-link?routeName=ForgotPasswordChange&amp;cacheKey=a9b3ra50-5fc2-4er7-b4e7-0d6c0925c536

希望库的作者将来能将其作为本机支持的功能,这样黑客就没有必要了。我也很想看看你的想法!

【讨论】:

这很有帮助。帮助我解决了非常相似的情况。还想知道,一旦您导航到 Auth 流程,您如何获得嵌套操作。几行代码会很有帮助。 我遇到了和@VikalpJain 一样的问题。登录成功后如何执行子动作?【参考方案2】:

我最终使用自定义 URI 来拦截深度链接启动,然后将这些参数传递给预期的路由。我的加载屏幕处理身份验证检查。

const previousGetActionForPathAndParams = AppContainer.router.getActionForPathAndParams

Object.assign(AppContainer.router, 
  getActionForPathAndParams (path, params) 
    if (path === 'auth' && params.routeName && params.userId ) 
      // returns a profile navigate action for myApp://auth?routeName=chat&userId=1234
      return NavigationActions.navigate(
        routeName: 'Loading',
          params:  ...params, path ,
      )
    
    return previousGetActionForPathAndParams(path, params)
  ,
)

https://reactnavigation.org/docs/en/routers.html#handling-custom-uris

然后,在我的加载路线中,我像往常一样解析参数,然后路由到预期的位置,再次传递它们。

const userId = this.props.navigation.getParam('userId')

https://reactnavigation.org/docs/en/params.html

【讨论】:

【参考方案3】:

在我这边,我实现了这一点,而无需手动解析路径以提取路径和参数。 步骤如下:

获取导航操作返回:getActionForPathAndParams 将导航操作作为参数传递给身份验证视图 然后,当身份验证成功或身份验证已经正常时,我会调度导航操作以继续预期的路线

const previousGetActionForPathAndParams = AppContainer.router.getActionForPathAndParams

Object.assign(AppContainer.router, 
  getActionForPathAndParams(path: string, params: NavigationParams) 
    const navigationAction = previousGetActionForPathAndParams(path, params)
    return NavigationActions.navigate(
      routeName: 'Authentication',
      params:  navigationAction 
    )
  
)

然后在身份验证视图中:

const navigationAction = this.navigation.getParam('navigationAction')
if (navigationAction)
  this.props.navigation.dispatch(navigationAction)

【讨论】:

【参考方案4】:

我找到了一种更简单的方法,我将链接保存在一个单独的文件中,并将其导入主App.js

linking.js

 const config = 
   screens: 
      Home:'home',
      Profile:'profile,
     ,
  ;
    
 const linking = 
  prefixes: ['demo://app'],
  config,
;
     
 export default linking;

App.js

& 在登录期间,我将令牌保存在异步存储中,当用户注销时,令牌被删除。根据令牌的可用性,我将链接附加到导航并使用状态将其分离,当它分离时它回退到 SplashScreen。

确保设置initialRouteName="SplashScreen"

import React, useState, useEffect from 'react';
import Linking from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import createStackNavigator from '@react-navigation/stack';
import NavigationContainer from '@react-navigation/native';
import linking from './utils/linking';
import Home, Profile, SplashScreen from './components';

const Stack = createStackNavigator();

// This will be used to retrieve the AsyncStorage String value
const getData = async (key) => 
  try 
    const value = await AsyncStorage.getItem(key);
    return value != null ? value : '';
   catch (error) 
    console.error(`Error Caught while getting async storage data: $error`);
  
;

function _handleOpenUrl(event) 
  console.log('handleOpenUrl', event.url);


const App = () => 
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  useEffect(() => 
    // Checks if the user is logged in or not, if not logged in then
    // the app prevents the access to deep link & falls back to splash screen.
    getData('studentToken').then((token) => 
      if (token === '' || token === undefined) setIsLoggedIn(false);
      else setIsLoggedIn(true);
    );

    Linking.addEventListener('url', _handleOpenUrl);
    return () => 
      Linking.removeEventListener('url', _handleOpenUrl);
    ;
  , []);

  return (
    //linking is enabled only if the user is logged in
    <NavigationContainer linking=isLoggedIn && linking>
      <Stack.Navigator
        initialRouteName="SplashScreen"
        screenOptions=...TransitionPresets.SlideFromRightios>
        <Stack.Screen
          name="SplashScreen"
          component=SplashScreen
          options=headerShown: false
        />
        <Stack.Screen
          name="Home"
          component=Home
          options=headerShown: false, gestureEnabled: false
        />
        <Stack.Screen
          name="Profile"
          component=Profile
          options=headerShown: false, gestureEnabled: false
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
;

export default App;

当登录用户打开通知中的深层链接时,它会将他带到相应的深层链接屏幕,如果他未登录,则会从启动屏幕打开。

【讨论】:

以上是关于React-navigation:与身份验证的深度链接的主要内容,如果未能解决你的问题,请参考以下文章

无法通过 iOS 15 上的深度链接启动 Google 身份验证器应用程序

深度分析运用非对称加密技术进行去中心化身份验证

带有登录屏幕的反应导航

设置 apollo-client 标头时出现 NetworkError

尚未创建导航器时如何从链接导航(与branch.io的深度链接)?

使用选项卡堆栈时反应导航深度链接不起作用