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

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

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

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

  2. 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?跨度>

标签: javascript reactjs access-token


【解决方案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】:
  1. silenwRenew 方法中,redirectUrl 需要从localStorage 中检索,这是您要存储在redirect.html 文件中的URL,在相同的键下>。因此,创建一个您将用于这两个部分的密钥。例如:

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

    • 您可能需要使用localStorage.getItem('silent-derirect-url-key'),但它应该可以同时使用这两种方式。
  2. 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”. 有什么解决方法吗?
猜你喜欢
  • 2018-12-20
  • 2016-10-07
  • 2014-05-18
  • 2017-10-25
  • 1970-01-01
  • 2019-06-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多