【问题标题】:Implicit Flow with silent refresh in ReactReact 中带有静默刷新的隐式流
【发布时间】:2020-04-20 04:11:53
【问题描述】:

背景

我正在我的 React 应用程序中测试 Implicit Flow auth 并尝试实现所谓的 静默刷新 功能,我会在用户登录时定期请求新的访问令牌,而无需需要向他申请新的授权。

以下是 Flow 架构,在我的例子中,Auth0 Tenant 是 Spotify:

虽然使用隐式授权的 SPA(单页应用程序)不能使用刷新令牌,但还有其他方法可以提供类似的功能:

  • 在调用 /authorize 端点时使用 prompt=none。用户将 看不到登录或同意对话框。

  • 从隐藏的 iframe 调用 /authorize 并 从父框架中提取新的访问令牌。用户不会 查看正在发生的重定向。


另一种方法是实现类似于包axios-auth-refresh 的东西,这是一个

帮助您通过axios拦截器实现自动刷新授权。您可以在失败时轻松拦截原始请求,刷新授权并继续原始请求,无需任何用户交互。

用法

import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';

// Function that will be called to refresh authorization
const refreshAuthLogic = failedRequest => axios.post('https://www.example.com/auth/token/refresh').then(tokenRefreshResponse => {
    localStorage.setItem('token', tokenRefreshResponse.data.token);
    failedRequest.response.config.headers['Authorization'] = 'Bearer ' + tokenRefreshResponse.data.token;
    return Promise.resolve();
});

// Instantiate the interceptor (you can chain it as it returns the axios instance)
createAuthRefreshInterceptor(axios, refreshAuthLogic);

// Make a call. If it returns a 401 error, the refreshAuthLogic will be run, 
// and the request retried with the new token
axios.get('https://www.example.com/restricted/area')
    .then(/* ... */)
    .catch(/* ... */);

设置

这是我的Parent 组件(请注意,isAuthenticated 状态指的是我的应用身份验证,与我需要 静默刷新 的 Spotify 令牌无关):

import SpotifyAuth from './components/spotify/Spotify';

class App extends Component {
  constructor() {
    super();
    this.state = {
      isAuthenticated: false,
      isAuthenticatedWithSpotify: false,
      spotifyToken: '',
      tokenRenewed:'' 
    };
    this.logoutUser = this.logoutUser.bind(this);
    this.loginUser = this.loginUser.bind(this);
    this.onConnectWithSpotify = this.onConnectWithSpotify.bind(this);
  };

  UNSAFE_componentWillMount() {
    if (window.localStorage.getItem('authToken')) {
      this.setState({ isAuthenticated: true });
    };
  };

  logoutUser() {
    window.localStorage.clear();
    this.setState({ isAuthenticated: false });
  };

  loginUser(token) {
    window.localStorage.setItem('authToken', token);
    this.setState({ isAuthenticated: true });
  };

  onConnectWithSpotify(token){
    this.setState({ spotifyToken: token,
                    isAuthenticatedWithSpotify: true
    }, () => {
       console.log('Spotify Token', this.state.spotifyToken)
    });
  }

  render() {
    return (
      <div>
        <NavBar
          title={this.state.title}
          isAuthenticated={this.state.isAuthenticated}
        />
        <section className="section">
          <div className="container">
            <div className="columns">
              <div className="column is-half">
                <br/>
                <Switch>
                  <Route exact path='/' render={() => (
                    <SpotifyAuth
                    onConnectWithSpotify={this.onConnectWithSpotify}
                    spotifyToken={this.state.spotifyToken}
                    />
                  )} />
                  <Route exact path='/login' render={() => (
                    <Form
                      formType={'Login'}
                      isAuthenticated={this.state.isAuthenticated}
                      loginUser={this.loginUser}
                      userId={this.state.id} 
                    />
                  )} />
                  <Route exact path='/logout' render={() => (
                    <Logout
                      logoutUser={this.logoutUser}
                      isAuthenticated={this.state.isAuthenticated}
                      spotifyToken={this.state.spotifyToken}
                    />
                  )} />
                </Switch>
              </div>
            </div>
          </div>
        </section>
      </div>
    )
  }
};

export default App;

以下是我的SpotifyAuth 组件,用户在登录时单击一个按钮,以便在应用程序中授权和验证他的 Spotify 帐户。

import Credentials from './spotify-auth.js'
import './Spotify.css'

class SpotifyAuth extends Component {  
  constructor (props) {
    super(props);
    this.state = {
      isAuthenticatedWithSpotify: this.props.isAuthenticatedWithSpotify
    };
    this.state.handleRedirect = this.handleRedirect.bind(this);
  };

  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;
    } 

  getHashParams() {
    const hashParams = {};
    const r = /([^&;=]+)=?([^&;]*)/g;
    const q = window.location.hash.substring(1);
    let e = r.exec(q);
    while (e) {
      hashParams[e[1]] = decodeURIComponent(e[2]);
      e = r.exec(q);
    }
    return hashParams;
  }

  componentDidMount() {
    //if (this.props.isAuthenticated) {
    const params = this.getHashParams();

    const access_token = params.access_token;
    const state = params.state;
    const storedState = localStorage.getItem(Credentials.stateKey);
    localStorage.setItem('spotifyAuthToken', access_token);
    localStorage.getItem('spotifyAuthToken');

    if (window.localStorage.getItem('authToken')) {
      this.setState({ isAuthenticatedWithSpotify: true });
    };
    if (access_token && (state == null || state !== storedState)) {
      alert('Click "ok" to finish authentication with Spotify');
    } else {
      localStorage.removeItem(Credentials.stateKey);
    }
    this.props.onConnectWithSpotify(access_token); 
  };


  handleRedirect(event) {
    event.preventDefault()
    const params = this.getHashParams();
    const access_token = params.access_token;
    console.log(access_token);

    const state = this.generateRandomString(16);
    localStorage.setItem(Credentials.stateKey, state);

    let url = 'https://accounts.spotify.com/authorize';
    url += '?response_type=token';
    url += '&client_id=' + encodeURIComponent(Credentials.client_id);
    url += '&scope=' + encodeURIComponent(Credentials.scope);
    url += '&redirect_uri=' + encodeURIComponent(Credentials.redirect_uri);
    url += '&state=' + encodeURIComponent(state);
    window.location = url; 
  };

  render() {
      return (
        <div className="button_container">
            <h1 className="title is-4"><font color="#C86428">Welcome</font></h1>
            <div className="Line" /><br/>
              <button className="sp_button" onClick={(event) => this.handleRedirect(event)}>
                <strong>LINK YOUR SPOTIFY ACCOUNT</strong>
              </button>
        </div>
      )
  }
}
export default SpotifyAuth;

然而,静默刷新不需要上面的按钮,也不需要渲染任何东西。


为了完整起见,这是我用于我的应用程序身份验证过程的端点,它使用 jwt -json Web 令牌来加密令牌并通过 cookie 从服务器传递到客户端(但这个加密工具不是到目前为止,用于将 Spotify 令牌传递给我的客户):

@auth_blueprint.route('/auth/login', methods=['POST'])
def login_user():
    # get post data
    post_data = request.get_json()
    response_object = {
        'status': 'fail',
        'message': 'Invalid payload.'
    }
    if not post_data:
        return jsonify(response_object), 400
    email = post_data.get('email')
    password = post_data.get('password')
    try:
        user = User.query.filter_by(email=email).first()
        if user and bcrypt.check_password_hash(user.password, password):
            auth_token = user.encode_auth_token(user.id)
            if auth_token:
                response_object['status'] = 'success'
                response_object['message'] = 'Successfully logged in.'
                response_object['auth_token'] = auth_token.decode()
                return jsonify(response_object), 200
        else:
            response_object['message'] = 'User does not exist.'
            return jsonify(response_object), 404
    except Exception:
        response_object['message'] = 'Try again.'
        return jsonify(response_object), 500

问题

考虑到上面的选项和代码,我如何使用我的设置来添加静默刷新并处理重定向到 Spotify 并在后台每小时获取一个新令牌?

位于this solution 和我的代码之间的东西?

【问题讨论】:

  • 您是否尝试过使用 iframe 静默刷新您的访问令牌?
  • 不,这就是我要问的:如何准确地做到这一点。
  • 您需要引入一个隐藏的 iframe,并从那里处理重定向流,然后在您的 iframe 哈希中获取 access_token 后,将其存储在您的应用程序中。我目前正在这样做,我获得了一个新令牌,但是 iframe 有问题 检查我今天发布的问题,也许它会给你一个提示。 stackoverflow.com/questions/59656972/…

标签: reactjs axios spotify auth0 refresh-token


【解决方案1】:

所以基本上你需要做以下事情之一:-

假设您的访问令牌会在 1 小时内过期。

选项 1) 设置一个超时,在用户活动 45 分钟后触发以获取新的访问令牌。

选项 2) 避免设置超时,您将引入一种技术来检测用户活动并静默获取令牌,例如,如果您通过检查令牌过期时间的 getToken 方法保护您的路由,您将在此处添加另一个将触发静默刷新的方法。

method(){
let iframeElement = this.getDocument().getElementById("anyId");
if (iframeElement == null) {
  iframeElement = document.createElement("iframe");
  iframeElement.setAttribute("id", "anyId");
  document.getElementsByTagName("body")[0].appendChild(iframeElement);
}
  iframeElement.setAttribute("src", tokenUrl); //token url is the authorization server token endpoint
},

现在您的 iframe 将在哈希中获得一个新的访问令牌,请注意您的 tokenUrl 需要在参数中包含 prompt=none。

您处理新令牌存储的方式取决于您在应用程序中存储令牌的方式,也许您需要调用 parent.storing_method 来存储它。

【讨论】:

  • github.com/keycloak/keycloak/blob/master/adapters/oidc/js/src/… 您可以查看 checkSsoSilently(),在 keycloak.js 库中,它们执行的操作类似于我正在尝试执行的操作
  • tokenUrl 是 Spotify,上面提到的 url,不是吗?上面的代码是否应该放在js 文件中并在jsx 上下文中导入反应组件?此外,为了您的答案的完整性,非常感谢选项 1) 的 setTimeout() 函数。也许它更简单:将 setTimeout() 与 &lt;SpotifyAuth&gt; 合并或将其与 OnConnectWithSpotify() 一起使用
  • 是的,这是您代码中的 url,但是您需要添加一个额外的查询参数 prompt=none (url+='promot=none') 那是因为您不想重新-输入您的凭据,您想立即获得访问令牌。我个人使用 vanilla JS 在服务中使用授权代码,然后您可以通过导入并使用它的功能将此服务包含在您的反应应用程序中。不幸的是,正如您所看到的,我遇到了超时和 iframe 的问题,所以至少现在我不能给您一个可靠的答案。但我给了你应该关注的主线
  • 例如,您应该使用令牌的有效性来保护您的 React 应用程序。如果您将令牌存储在 cookie 中,在渲染您的路由之前,您将执行类似 myService.checktoken() 的操作,如果您有一个有效令牌,您将渲染路由..componentDidMount(){ myService.checktoken()。 then(//your code)} 在 checktoken() 中,您将检索存储的令牌并检查到期时间,如果令牌将在 15 分钟内到期,您可以调用 method(),该方法将通过设置属性来执行 iframe 角色。明天解决我的问题后,我可以给出准确的答案
  • 好的,我明天或不久将等待您编辑的答案。非常感谢朋友!
【解决方案2】:

我并不清楚您是否能够使用 spotify API 进行静默身份验证请求。根据他们的authorization guide,隐式流程是临时的,不包括刷新令牌。

在 Auth0 的上下文中,您将利用 iFrame 发送带有用户 cookie 的静默请求,其中会话将验证请求并发出新的访问令牌。这是使用上面提到的 prompt=none 选项完成的。

【讨论】:

  • 对...但是您已经提到了问题中存在的两件事。考虑到我到目前为止所拥有的,我的问题要求提供有关如何实现这一目标的代码
  • 更直接地说,您不能在保持最佳实践的同时做您所描述的事情。此外,您不应在 localStorage 中存储令牌或任何敏感数据。如果您需要证明您所请求的内容在 atm 上是不可能的,请参阅此问题:github.com/spotify/web-api/issues/1215 编辑:提交功能请求可能值得,因为这似乎是其他开发人员正在请求的内容。
  • “不可能”和“不推荐”是不同的东西。是的,这是可以做到的。如果我可以通过多次单击我的表单来获得尽可能多的令牌,则可以以编程方式完成。这就是问题所在。就像我在问题上所说的那样,我的令牌将被加密,并且不会泄漏。客户端将处理 cookie 编码令牌,而不是自己处理令牌。无论如何它都不是产品,它只是测试我对静默刷新如何工作的理解。无论如何感谢您的建议..
  • 如果您想发布一些关于如何使用 React 和 iFrame 实现静默刷新的代码,我可以接受您的回答,因为我发现的只是 angular。
猜你喜欢
  • 2021-06-07
  • 2019-06-12
  • 2023-03-27
  • 2020-06-12
  • 2014-01-30
  • 2019-01-19
  • 2020-05-22
  • 2020-03-16
  • 2016-10-07
相关资源
最近更新 更多