需要帮助尝试在本机反应中刷新我的令牌

Posted

技术标签:

【中文标题】需要帮助尝试在本机反应中刷新我的令牌【英文标题】:Need help trying to refresh my token in react native 【发布时间】:2020-10-13 20:38:03 【问题描述】:

我认为这是一个简单的任务,存储我的令牌,设置一个计时器并在计时器到期时获取令牌,我错了,在看了几篇关于如何解决这个问题的文章之后,我非常迷茫现在,我需要帮助来存储我的令牌(或同时存储我的令牌(或同时存储用户名数据和令牌?不确定了),以及在令牌到期时刷新令牌。

是的,我在堆栈溢出方面看到了很多与此相关的问题,但其中许多与特定问题有关,而不是如何解决。

我的应用通过 microsoft graph 从 api (net core 2.0) 连接到 Office 365。

在我的应用程序上,我得到了这段代码来从 api 获取数据,并传递参数我的用户名和密码

  async ApiLogin(loginRequestObject: LoginRequest) 
    var serviceResult = new ServiceResult();
    await NetInfo.fetch().then(async state => 
      var param = JSON.stringify(loginRequestObject);
      if (state.isConnected) 
        try 
          await fetch(ServiceProperties.URLauthentication, 
            method: 'POST',
            headers: 
              Accept: 'application/json',
              'Content-Type': 'application/json',
            ,
            body: param,
          )
            .then(response => 
              return response.json();
            )
            .then(responseJson => 
              if (JSON.stringify(responseJson) != null) 
                serviceResult.Success = true;
                serviceResult.Message = 'Service Authentication ok';
                serviceResult.ResponseBody = responseJson;
                serviceResult.StatusCode = 0;
               else 
                serviceResult.Success = false;
                serviceResult.Message = 'Service Authentication not ok';
                serviceResult.ResponseBody = null;
                serviceResult.StatusCode = -100;
              
            );
         catch (error) 
          serviceResult.Success = false;
          serviceResult.Message = 'Service Authentication not ok';
          serviceResult.ResponseBody = null;
          serviceResult.StatusCode = -999;
        
       else 
        serviceResult.Success = false;
        serviceResult.Message = 'Service internet not ok';
        serviceResult.ResponseBody = null;
        serviceResult.StatusCode = -1;
      
    );
    console.log(JSON.parse(serviceResult.ResponseBody));
    return serviceResult;
  

结果是这样的。

"Username":"sensitive data","DisplayName":"sensitive data","GivenName":"sensitive data","SurName":"sensitive data","Email":"sensitive data","Token":"ZSI6Im42aGRfdDVGRHhrSzBEVklJUXpxV09DWHZ4dWc0RlhWVkI4ZVJ6dEFsWDAiLCJhbGciOiJSUzI1NiIsIng1dCI6IlNzWnNCTmhaY0YzUTlTNHRycFFCVEJ5TlJSSSIsImtpZCI6IlNzWnNCTmhaYm5ldC8zOTBmODU5NS1kZTFlLTRmNmQtYTk1NC0yNWY2N5MjkwMTYsImV4cCI6MTU5MjkzMjkxNiButVBqe3E3QwcBr1P0G_dWyC9ASQU0psGDPnsQphp0T070ROZ_mcPitgquNfsO5JZ8-o056l_aePhXSMO7bHWmUBbVn7TA1UoYIz3lAoOzvE6juadve4aU3goeaBj8PIrhG0M2zEEfKgOL1Al9MSU1GGUmRW9dBofeA4e1cGmlGQrUKnt73n0sHap6","PhotoBase64":null

这几乎是我所得到的,目前,我在这个应用程序上使用了异步存储,但至少可以存储一个带有“无用”数据的对象,我不确定异步存储是否适合要不要这样,如果不行,我该怎么办?

编辑:在阅读了更多内容后,我发现我需要请求第二个令牌,来自 microsoft graph https://massivescale.com/microsoft-v2-endpoint-primer/ 的刷新令牌仍然需要有关如何存储数据和在过期时刷新令牌的帮助,

编辑 2:不幸的是,我没有从 api 获得刷新令牌或 expiresAt 值

【问题讨论】:

也许会有所帮助 - reactnavigation.org/docs/auth-flow 【参考方案1】:

我不熟悉那个 api,但我可以笼统地告诉你,我是如何在我的 react native 应用程序中做到这一点的。

首先,正如您所说,您需要访问令牌和刷新令牌。来自文档:

仅当您在第一个范围列表中包含 offline_access 时,才会返回刷新令牌。

所以基本上你需要在你的身份验证请求中添加offline_access 范围,然后响应应该包含一个刷新令牌令牌道具:

ApiLogin(
    ...otherProps,
    scope: "my other scopes offline_access"
)

收到响应后,您应该存储令牌数据。使用异步存储来做这件事是不安全的。我建议您以安全的方式存储该信息。有somelibraries可以做。

之后,您可以跟踪客户端中的到期日期,然后执行刷新令牌请求,如您所说。您也可以像往常一样继续执行请求,当 api 返回 401 错误响应(未经授权,发生在令牌过期时)您可以尝试使用刷新令牌获取新令牌(如果失败,则可能意味着刷新令牌也已过期并且您可以将用户重定向到登录屏幕)。根据您提供的文档:

要消除您的刷新令牌,我们需要再次向提供者发送 HTTP POST。 POST 的正文必须编码为application/x-www-form-urlencoded。 此正文将发布到https://login.microsoftonline.com/common/oauth2/v2.0/token。此调用的原型应如下所示:

https://login.microsoftonline.com/common/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&
refresh_token=[REFRESH TOKEN]&
client_id=[APPLICATION ID]&
client_secret=[PASSWORD]&
scope=[SCOPE]&
redirect_uri=[REDIRECT URI]

这样你应该能够获得一个新的访问令牌,你应该使用它直到它过期,然后重复这个过程。

如果我的回答有帮助或您可能有任何疑问,请告诉我。同样,我对 api 并不熟悉,我在这里发布的内容只是我对文档的解释,因此它们可能需要一些调整。

【讨论】:

不幸的是,我不再有权访问 api 配置,我得到的只是身份验证令牌,所以我解决这个问题的唯一方法是在每次令牌过期时对用户进行身份验证,而且,我m 使用钥匙串,但仅适用于用户名、密码数据存储 我不确定你的意思,抱歉。我认为您只需将范围 offline_access 添加到您的身体参数。 你确定我只需要在 fetch 上添加范围吗?就这样? api 本身没有配置?,您能否根据我的代码详细说明您的答案? @CarlosJiménez hi carlos,你熟悉 react 导航吗?【参考方案2】:

我无法为特定的身份验证提供程序提供帮助(从未使用过 Office 365),但这是您需要遵循的一般步骤:

    发送请求以获取访问和刷新令牌 将令牌存储在通过重新加载/重新启动保留数据的存储中(对于 Web,它是 localStorage,对于 RN sqlite 或 asyncstorage 或您使用的任何东西) 保存可用于所有组件(Redux、Context API 甚至您自己的解决方案)的令牌和身份验证状态。当用户进行身份验证、取消身份验证或令牌过期时,这是显示/隐藏应用程序部分所必需的 您需要以某种方式知道令牌何时过期(无法说明如何操作,但 API 文档应该有一些信息)并使用 setTimeout 来刷新 刷新令牌时,应将其持久化(参见 n.2)并更新全局身份验证状态(参见 n.3) 当应用刚刚(重新)启动时,检查您是否有访问/刷新令牌保留在存储中(请参阅 n.2)并相应地更新全局身份验证状态(请参阅 n.3) 您的路由应该对身份验证状态更改做出反应(请参阅路由库文档,有关受保护/经过身份验证的路由)。显示敏感内容的组件也应该对身份验证状态更改做出反应。

这是我的 Reactjs 身份验证解决方案(遗憾的是没有 RN 示例),它使用 JWT 针对我自己的 API 对客户端进行身份验证。这种情况下的访问令牌也是刷新令牌。我使用一种没有 Redux 的方法,只使用纯 React 和 JS。我希望这会对你有所帮助。

import  useCallback, useState, useEffect  from "react";
import JWT from "jsonwebtoken";
import  ENV  from "../config";
import  useLanguageHeaders  from "./i18n";

const decodeToken = (token) =>
  typeof token === "string" ? JWT.decode(token) : null;

//This class is responsible for authentication, 
//refresh and global auth state parts
//I create only one instance of AuthProvider and export it, 
//so it's kind of singleton
class AuthProvider 
  //Getter for _authStatus
  get authStatus() 
    return this._authStatus;
  

  constructor( tokenEndpoint, refreshEndpoint, refreshLeeway = 60 ) 
    this._tokenEndpoint = tokenEndpoint;
    this._refreshEndpoint = refreshEndpoint;
    this._refreshLeeway = refreshLeeway;
    //When app is loaded, I load token from local storage
    this._loadToken();
    //And start refresh function that checks expiration time each second
    //and updates token if it will be expired in refreshLeeway seconds
    this._maybeRefresh();
  

  //This method is called in login form
  async authenticate(formData, headers = ) 
    //Making a request to my API
    const response = await fetch(this._tokenEndpoint, 
      method: "POST",
      headers: 
        "Content-Type": "application/json",
        ...headers,
      ,
      redirect: "follow",
      body: JSON.stringify(formData),
    );
    const body = await response.json();
    if (response.status === 200) 
      //Authentication successful, persist token and update _authStatus
      this._updateToken(body.token);
     else 
      //Error happened, replace stored token (if any) with null 
      //and update _authStatus
      this._updateToken(null);
      throw new Error(body);
    
  

  //This method signs user out by replacing token with null
  unauthenticate() 
    this._updateToken(null);
  

  //This is needed so components and routes are able to 
  //react to changes in _authStatus
  addStatusListener(listener) 
    this._statusListeners.push(listener);
  

  //Components need to unsubscribe from changes when they unmount
  removeStatusListener(listener) 
    this._statusListeners = this._statusListeners.filter(
      (cb) => cb !== listener
    );
  

  _storageKey = "jwt";
  _refreshLeeway = 60;
  _tokenEndpoint = "";
  _refreshEndpoint = "";
  _refreshTimer = undefined;
  //This field holds authentication status
  _authStatus = 
    isAuthenticated: null,
    userId: null,
  ;
  _statusListeners = [];

  //This method checks if token refresh is needed, performs refresh 
  //and calls itself again in a second
  async _maybeRefresh() 
    clearTimeout(this._refreshTimer);

    try 
      const decodedToken = decodeToken(this._token);

      if (decodedToken === null) 
        //no token - no need to refresh
        return;
      

      //Note that in case of JWT expiration date is built-in in token
      //itself, so I do not need to make requests to check expiration
      //Otherwise you might want to store expiration date in _authStatus
      //and localStorage
      if (
        decodedToken.exp * 1000 - new Date().valueOf() >
        this._refreshLeeway * 1000
      ) 
        //Refresh is not needed yet because token will not expire soon
        return;
      

      if (decodedToken.exp * 1000 <= new Date().valueOf()) 
        //Somehow we have a token that is already expired
        //Possible when user loads app after long absence
        this._updateToken(null);
        throw new Error("Token is expired");
      

      //If we are not returned from try block earlier, it means 
      //we need to refresh token
      //In my scenario access token itself is used to get new one
      const response = await fetch(this._refreshEndpoint, 
        method: "POST",
        headers: 
          "Content-Type": "application/json",
        ,
        redirect: "follow",
        body: JSON.stringify( token: this._token ),
      );
      const body = await response.json();
      if (response.status === 401) 
        //Current token is bad, replace it with null and update _authStatus
        this._updateToken(null);
        throw new Error(body);
       else if (response.status === 200) 
        //Got new token, replace existing one
        this._updateToken(body.token);
       else 
        //Network error, maybe? I don't care unless its 401 status code
        throw new Error(body);
      
     catch (e) 
      console.log("Something is wrong when trying to refresh token", e);
     finally 
      //Finally block is executed even if try block has return statements
      //That's why I use it to schedule next refresh try
      this._refreshTimer = setTimeout(this._maybeRefresh.bind(this), 1000);
    
  

  //This method persist token and updates _authStatus
  _updateToken(token) 
    this._token = token;
    this._saveCurrentToken();

    try 
      const decodedToken = decodeToken(this._token);

      if (decodedToken === null) 
        //No token
        this._authStatus = 
          ...this._authStatus,
          isAuthenticated: false,
          userId: null,
        ;
       else if (decodedToken.exp * 1000 <= new Date().valueOf()) 
        //Token is expired
        this._authStatus = 
          ...this._authStatus,
          isAuthenticated: false,
          userId: null,
        ;
       else 
        //Token is fine
        this._authStatus = 
          ...this._authStatus,
          isAuthenticated: true,
          userId: decodedToken.id,
        ;
      
     catch (e) 
      //Token is so bad that can not be decoded (malformed)
      this._token = null;
      this._saveCurrentToken();
      this._authStatus = 
        ...this._authStatus,
        isAuthenticated: false,
        userId: null,
      ;
      throw e;
     finally 
      //Notify subscribers that _authStatus is updated
      this._statusListeners.forEach((listener) => listener(this._authStatus));
    
  

  //Load previously persisted token (called in constructor)
  _loadToken() 
    this._updateToken(window.localStorage.getItem(this._storageKey));
  

  //Persist token
  _saveCurrentToken() 
    if (typeof this._token === "string") 
      window.localStorage.setItem(this._storageKey, this._token);
     else 
      window.localStorage.removeItem(this._storageKey);
    
  


//Create authProvider instance
const authProvider = new AuthProvider(ENV.auth);

//This hook gives a component a function to authenticate user
export const useAuthenticate = () => 
  const headers = useLanguageHeaders();

  return useCallback(
    async (formData) => 
      await authProvider.authenticate(formData, headers);
    ,
    [headers]
  );
;

//This hook gives a function to unauthenticate
export const useUnauthenticate = () => 
  return useCallback(() => authProvider.unauthenticate(), []);
;

//This hook allows components to get authentication status 
//and react to changes
export const useAuthStatus = () => 
  const [authStatus, setAuthStatus] = useState(authProvider.authStatus);

  useEffect(() => 
    authProvider.addStatusListener(setAuthStatus);

    return () => 
      authProvider.removeStatusListener(setAuthStatus);
    ;
  , []);

  return authStatus;
;

功能组件内的这行代码可以知道用户是否经过身份验证:const isAuthenticated = useAuthStatus();

【讨论】:

在 React Native 中,使用 AsyncStorage 存储令牌是不安全的,因为理论上其他应用也可以获得令牌值。 @ינוןרחמים 那么你有什么推荐呢? 在 react native docs (reactnative.dev/docs/security#secure-storage) 中有一些安全的存储解决方案。

以上是关于需要帮助尝试在本机反应中刷新我的令牌的主要内容,如果未能解决你的问题,请参考以下文章

如何在本机反应中获取设备令牌

如何使用本机反应创建登录以及如何验证会话

在 JWT 身份验证过程中需要帮助

如何在反应本机中使用 XMLHttpRequest 在 GET 方法的标头中传递授权令牌

如何刷新OAuth2令牌?我是否需要等待令牌过期? (Patreon API)

为啥反应组件需要刷新才能加载其内容,即使在成功登录后也是如此?