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 重定向处。
但我似乎无法让我的 <script>
在与组件位于同一根目录下的 localhost/redirect.html
文件位置工作。
我的redirect.html
脚本或文件一定有问题,设置我的requestKey
,因为我将控制台记录redirectUrl
为undefined
或null
,如果我使用任何一个
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 > token.accessToken.expirationTime)
应该是:return !hasToken || (now > token.expirationTime)
in SilentTokenRenew .willTokenExpire()
吗?
这里到底是什么问题,你不能重定向到那个redirect.html
或者你被重定向了,但是你需要帮助编写脚本来解析令牌并将其存储到LS?跨度>
【参考方案1】:
我认为您的问题可能在于您在SilentTokenRenew.willTokenExpire()
中的退货条件,特别是:(now > token.accessToken.expirationTime)
。 now > token.accessToken.expirationTime 产生 false,因此您的函数可能总是返回 false,除非令牌不存在。
token
对象看起来像:
accessToken: TOKEN,
expirationTime: TIME,
条件的那部分应该改为:(now > token.expirationTime)
。
希望对你有所帮助。
【讨论】:
奇怪...如果我按照您的建议进行操作,则会收到上述错误。如果我保留token.accessToken.expirationTime
,willTokenExpire()
至少记录时间和令牌。
我会检查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 = undefined
或null
,如果我使用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 静默更新令牌的主要内容,如果未能解决你的问题,请参考以下文章
如何静默自动更新通过 NSIS 为所有用户/每台机器安装的电子应用程序?