ReactJS - 使用 iframe 静默更新令牌

Posted

技术标签:

【中文标题】ReactJS - 使用 iframe 静默更新令牌【英文标题】:ReactJS - Silently renew token with iframe 【发布时间】:2020-06-06 10:35:03 【问题描述】:

我正在尝试围绕身份验证过程进行包装,并在我的 React 应用程序中每 60 分钟实现一次静默令牌更新,如下所示:

    创建一些watcher函数,检查过期时间 访问令牌。如果令牌即将过期,是时候更新它了。 渲染一个 iframe 标签,src 应该是你的同一个 URL 用于重定向到 Auth 服务器,有一个区别: 将返回 URL 更改为静态文件,我们称之为 redirect.html。 服务器应该知道用户调用这个 URL,从存储的 cookie,所以它应该只是简单地将你重定向到 redirect.html 文件,现在带有新的访问令牌。 在此 redirect.html 中编写一个简短的脚本,将 来自 URL 的令牌,并用您在本地存储中已有的令牌覆盖它。 销毁 iframe。

Spotify 页面:

在 Spotify 开发页面,我保存了我常用的重定向 URL,用于首次从授权 URL 服务器获取令牌时:

http://localhost

然后我还使用 Spotify 为我的 iframe 添加了一个新的重定向 URL:

http://localhost/redirect_html


应用程序

App.jsx

到目前为止,这是我用于静默更新的组件,我正在父组件的 localhost/test-silent-renew 上对其进行测试,如下所示:

 <Route exact path='/test-silent-renew' render=() => (
     <SilentTokenRenew
     />
 ) />

组件

这是真正的刷新组件:

SilentTokenRenew.jsx

import React,  Component  from 'react'


class SilentTokenRenew extends Component 
  constructor(props) 
    super(props)
    this.state =  
      renewing: false,
      isAuthenticated: false
     
    this.currentAttempt = 0
    this.maxNumberOfAttempts = 20
    this.state.renderIframe = this.renderIframe.bind(this);
    this.state.handleOnLoad = this.handleOnLoad.bind(this);
  ;

  shouldComponentUpdate(nextProps, nextState) 
    return this.state.renewing !== nextState.renewing
  

  componentDidMount() 
    this.timeInterval = setInterval(this.handleCheckToken, 20000)
  

  componentWillUnmount() 
    clearInterval(this.timeInterval)
  

  willTokenExpire = () => 
    const accessToken = localStorage.getItem('spotifyAuthToken');
    console.log('access_token', accessToken)
    const expirationTime = 3600
    const token =  accessToken, expirationTime  //  accessToken, expirationTime 
    const threshold = 300 // 300s = 5 minute threshold for token expiration

    const hasToken = token && token.accessToken
    const now = (Date.now() / 1000) + threshold
    console.log('NOW', now)

    return !hasToken || (now > token.expirationTime)
  

  handleCheckToken = () => 
    if (this.willTokenExpire()) 
      this.setState( renewing: true )
      clearInterval(this.timeInterval)
    
  

  silentRenew = () => 
    return new Promise((resolve, reject) => 
      const checkRedirect = () => 
        // This can be e
        const redirectUrl = localStorage.getItem('silent-redirect-url-key')
        console.log('REDIRECT URL', redirectUrl)

        if (!redirectUrl) 
          this.currentAttempt += 1
          if (this.currentAttempt > this.maxNumberOfAttempts) 
            reject(
              message: 'Silent renew failed after maximum number of attempts.',
              short: 'max_number_of_attempts_reached',
            )
            return
          
          setTimeout(() => checkRedirect(), 500)
          return
        
        // Clean up your localStorage for the next silent renewal
        localStorage.removeItem('silent-redirect-url-key') // /redirect.html#access_token=......
        // // Put some more error handlers here
        // // Silent renew worked as expected, lets update the access token
        const session = this.extractTokenFromUrl(redirectUrl) // write some function to get out the access token from the URL
        // // Following your code you provided, here is the time to set
        // // the extracted access token back to your localStorage under a key Credentials.stateKey
        localStorage.setItem(Credentials.stateKey, JSON.stringify(session))
        resolve(session)
      
      checkRedirect()
    )
  

  handleOnLoad = () => 
    this.silentRenew()
      .then(() => 
        this.setState( renewing: false )
        this.currentAttempt = 0
        this.timeInterval = setInterval(this.handleCheckToken, 60000)
        // Access token renewed silently.
      )
      .catch(error => 
        this.setState( renewing: false )
        // handle the errors
      )
  

 generateRandomString(length) 
    let text = '';
    const possible =
      'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    for (let i = 0; i < length; i++) 
      text += possible.charAt(Math.floor(Math.random() * possible.length));
    
    return text;
   

       renderIframe = () => 
    const state = this.generateRandomString(16);
    const url = new URL('https://accounts.spotify.com/authorize?response_type=token&client_id=my_id&scope=user-read-currently-playing%20user-read-private%20user-library-read%20user-read-email%20user-read-playback-state%20user-follow-read%20playlist-read-private%20playlist-modify-public%20playlist-modify-private&redirect_uri=http%3A%2F%2Flocalhost&state=rBZaR9s1gHchWEME')
    console.log('URL HREF', url.href)
    console.log(url.searchParams.get('redirect_uri'))

    url.searchParams.set(Credentials.stateKey, state)
    url.searchParams.set('redirect_uri', 'http://localhost/redirect.html') // the redirect.html file location
    url.searchParams.set('prompt', 'none')
    //window.location = url; 

    return (
      <iframe
        style= width: 0, height: 0, position: 'absolute', left: 0, top: 0, display: 'none', visibility: 'hidden' 
        width=0
        height=0
        title="silent-token-renew"
        src=url.href
        onLoad=this.handleOnLoad
      />
    )
  

  render() 
    const  renewing  = this.state
    return renewing ? this.renderIframe() : null
  


export default SilentTokenRenew;

HTML

这是我的 iframe 的代码:

 <!DOCTYPE html>
<html>

<head>
    <title>OAuth - Redirect</title>
</head>

<body>
<p>Renewing...</p>
<script>
  // Get name of window which was set by the parent to be the unique request key
  // or if no parameter was specified, we have a silent renew from iframe
  const requestKey = 'silent-redirect-url-key'
  // Update corresponding entry with the redirected url which should contain either access token or failure reason in the query parameter / hash
  window.localStorage.setItem(requestKey, window.location.href);
  window.close();
</script>
</body>

</html>

如果我执行以下操作,我可以看到:

url.searchParams.set('prompt', 'none')
window.location = url; /// <-------

新令牌在那里,在浏览器 url 重定向处。

但我似乎无法让我的 &lt;script&gt; 在与组件位于同一根目录下的 localhost/redirect.html 文件位置工作。

我的redirect.html 脚本或文件一定有问题,设置我的requestKey,因为我将控制台记录redirectUrlundefinednull,如果我使用任何一个

    const redirectUrl = localStorage.getItem('silent-redirect-url-key')

    const redirectUrl = localStorage['silent-redirect-url-key']


编辑

Chrome 沉默了,但 Firefox 告诉我:

Load denied by X-Frame-Options: “deny” from “https://accounts.spotify.com/login?continue=https%3A%2F%2Fac…prompt%3Dnone%26client_id%my_id”, site does not permit any framing. Attempted to load into “http://localhost/test”

我有一个nginx 代理,其中客户端配置如下:

server 

  listen 80;

  location / 
    proxy_pass        http://client:3000;
    proxy_redirect    default;
    proxy_set_header  Upgrade $http_upgrade;
    proxy_set_header  Connection "upgrade";
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Host $server_name;
    proxy_read_timeout 86400s;
    proxy_send_timeout 86400s;
  

  location /redirect.html 
    proxy_pass        http://client:3000;
    proxy_redirect    default;
    proxy_set_header  Upgrade $http_upgrade;
    proxy_set_header  Connection "upgrade";
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Host $server_name;
    proxy_read_timeout 86400s;
    proxy_send_timeout 86400s;
  

改变我上面的 nginx 配置是否有解决该限制的方法?


或者,如果不是,并且只有在没有框架的情况下,我似乎会定期获得这个新令牌,如下所示:

const url = new URL('https://accounts.spotify.com/authorize?response_type=token&client_id=my_id&scope=user-read-currently-playing%20user-read-private%20user-library-read%20user-read-email%20user-read-playback-state%20user-follow-read%20playlist-read-private%20playlist-modify-public%20playlist-modify-private&redirect_uri=http%3A%2F%2Flocalhost&state=rBZaR9s1gHchWEME')

window.location = url // <----

我可以在不影响应用可用性的情况下使用抓取等方法提取此令牌吗?

【问题讨论】:

这可能是你的情况:return !hasToken || (now &gt; token.accessToken.expirationTime) 应该是:return !hasToken || (now &gt; token.expirationTime) in SilentTokenRenew .willTokenExpire() 吗? 这里到底是什么问题,你不能重定向到那个redirect.html或者你被重定向了,但是你需要帮助编写脚本来解析令牌并将其存储到LS?跨度> 【参考方案1】:

我认为您的问题可能在于您在SilentTokenRenew.willTokenExpire() 中的退货条件,特别是:(now &gt; token.accessToken.expirationTime)now > token.accessToken.expirationTime 产生 false,因此您的函数可能总是返回 false,除非令牌不存在。

token 对象看起来像:


  accessToken: TOKEN,
  expirationTime: TIME,

条件的那部分应该改为:(now &gt; token.expirationTime)

希望对你有所帮助。

【讨论】:

奇怪...如果我按照您的建议进行操作,则会收到上述错误。如果我保留token.accessToken.expirationTimewillTokenExpire() 至少记录时间和令牌。 我会检查docs for URLSearchParams.set()。可能需要从您的 url 字符串创建一个 URL 对象。【参考方案2】:

    silenwRenew 方法中,redirectUrl 需要从localStorage 中检索,这是您要存储在redirect.html 文件中的URL,在相同的键下>。因此,创建一个您将用于这两个部分的密钥。例如:

    const redirectUrl = localStorage['silent-derirect-url-key']

    您可能需要使用localStorage.getItem('silent-derirect-url-key'),但它应该可以同时使用这两种方式。

    redirect.html 文件中,使用相同的密钥,因此要设置商店的 URL,请使用:

    const requestKey = 'silent-derirect-url-key'

最后,extractTokenFromUrl 方法应该很简单,如下所示:

extractTokenFromUrl(redirectUrl = '') 
    let accessToken = null
    let decodedAccessToken = null
    const accessTokenMatch = redirectUrl.match(/(?:[?&#/]|^)access_token=([^&]+)/)
    if (accessTokenMatch) 
      accessToken = accessTokenMatch[1]
      decodedAccessToken = JSON.parse(atob(accessToken.split('.')[1]))
    

    let expireDurationSeconds = 3600
    const expireDurationSecondsMatch = redirectUrl.match(/expires_in=([^&]+)/)
    if (expireDurationSecondsMatch) 
      expireDurationSeconds = parseInt(expireDurationSecondsMatch[1], 10)
    

    return 
      accessToken,
      decodedAccessToken,
      expireDurationSeconds,
    
  

你可以使代码更好,但你明白了。

【讨论】:

谢谢,我的朋友。我的redirect.html 脚本或文件一定有问题,设置我的requestKey,因为我正在控制台记录redirectUrl = undefinednull,如果我使用const redirectUrl = localStorage.getItem('silent-redirect-url-key')const redirectUrl = localStorage['silent-redirect-url-key']。有什么想法吗? 如果我在 url.searchParams.set('prompt', 'none') 之后使用 window.location = url; ,重定向会使用新令牌发生,我会在 http://localhost/redirect.html#access_token=... 得到它,但如果没有该行,redirect.html 不会发生任何事情 首先,该键下的localStorage 是未定义的,这是正确的,因为您还没有在那里设置任何内容。您将它设置在redirect.html 文件中,这就是为什么您在该silentRenew 方法中有currentAttempt 的原因,如果它已经设置,则可以多次检查它。我仍然认为您的问题是 iframe 无法正确加载该 redirect.html 文件。在您的网络选项卡中检查它,查找redirect.html,如果您单击它,其内容应该是OAuth - Redirect 也许您忘记在redirect.html url 中输入端口号?比如http://localhost:3000/redirect.html,或者你忘了把redirect.html 文件放到public 文件夹中?或者,您忘记在您的 package.json 中为 redirect.html 设置代理。只是在这里提出一些想法。 我尝试使用 Firefox 而不是 Chrome,我得到了这个:Load denied by X-Frame-Options: site does not permit any framing. Attempted to load into “http://localhost/test”. 有什么解决方法吗?

以上是关于ReactJS - 使用 iframe 静默更新令牌的主要内容,如果未能解决你的问题,请参考以下文章

带有 Iframe 事件和更改处理的 ReactJs

使用刷新令牌 adal azure AD 静默更新访问令牌

如何静默自动更新通过 NSIS 为所有用户/每台机器安装的电子应用程序?

身份服务器 4 中的静默令牌更新,js 客户端应用程序未按预期工作

使用 websocket 更新 ReactJS 应用程序

ReactJS进度条使用状态动态更新?