【问题标题】:How to provide frontend with JSON web token after server authentication?服务器身份验证后如何为前端提供 JSON Web 令牌?
【发布时间】:2017-06-19 04:01:00
【问题描述】:

到目前为止,我只处理了服务器呈现的应用程序,在用户通过用户名/密码或使用 OAuth 提供程序(Facebook 等)登录后,服务器只是在重定向到相关页面时设置会话 cookie。

但是现在我正在尝试使用更“现代”的方法构建应用程序,前端使用 React,后端使用 JSON API。显然,对此的标准选择是使用 JSON Web 令牌进行身份验证,但是我无法弄清楚如何将 JWT 提供给客户端,以便可以将其存储在会话/本地存储或任何地方。

举例说明:

  1. 用户点击链接 (/auth/facebook) 通过 Facebook 登录

  2. 用户被重定向并显示 Facebook 登录表单和/或权限对话框(如有必要)

  3. Facebook 将用户重定向回/auth/facebook/callback,并附带一个授权码,服务器将其交换为访问令牌和有关用户的一些信息

  4. 服务器使用信息在数据库中查找或创建用户,然后创建一个包含用户数据相关子集(例如 ID)的 JWT

  5. ???

此时我只想将用户重定向到带有 JWT 的 React 应用程序的主页(比如说/app),这样前端就可以接管了。但是我想不出一种(优雅的)方法来做到这一点而不会丢失 JWT,除了将它放在重定向的查询字符串中(/app?authtoken=...) - 但这将显示在地址栏中,直到我使用replaceState() 或其他方式手动删除它,对我来说似乎有点奇怪。

真的,我只是想知道这通常是如何完成的,我几乎可以肯定我在这里遗漏了一些东西。如果有帮助,服务器是 Node(带有 Passport 的 Koa)。

编辑:为了清楚起见,我问的是最好的方法是向客户端提供令牌(以便可以保存)在 OAuth 重定向流程之后使用 Passport。

【问题讨论】:

  • 你找到答案了吗?我正在尝试找到该问题的答案,但找不到任何有用的信息。
  • 我也一直在努力解决这个问题,最佳做法是什么?

标签: node.js authentication passport.js jwt passport-facebook


【解决方案1】:

我最近遇到了同样的问题,但在这里或其他地方都没有找到解决方案,写信给this blog post 并表达了我的深入思考。

TL;DR:我想出了 3 种可能的方法来在 OAuth 登录/重定向后将 JWT 发送到客户端:

  1. 将 JWT 保存在 cookie 中,然后在以后的步骤中将其提取到前端或服务器上(例如,在客户端使用 JS 提取它,或向服务器发送请求,服务器使用 cookie 获取JWT,返回 JWT)。
  2. 将 JWT 作为查询字符串的一部分发回(您在问题中建议)。
  3. 发回带有<script>标签的服务器渲染HTML页面:
    1. 自动将嵌入的JWT保存到localStorage
    2. 之后自动将客户端重定向到您喜欢的任何页面。

(由于使用 JWT 登录本质上等同于“将 JWT 保存到 localStorage,因此我最喜欢的选项是 #3,但可能存在我没有考虑到的缺点。我很想听听其他人的想法在这里。)

希望有帮助!

【讨论】:

  • 太棒了。一直在一个和两个选项之间挣扎。第三个没有出现。出色的工作。
  • 感谢您正确回答这个问题!并写一篇关于它的博客文章!非常有帮助
  • 这些答案是否仍然有效,或者是否发现了另一种方法?
  • 您也可以在自定义标头中发送。除此之外,我不建议将您的 jwt 保存到本地存储,因为它可能容易出现 xss。
【解决方案2】:
  1. 客户端:通过 $auth.authenticate('provider name') 打开一个弹出窗口。
  2. 客户端:如有必要,使用该提供商登录,然后授权应用程序。
  3. 客户端:授权成功后,弹窗将重定向回您的应用,例如http://localhost:3000,带code(授权码)查询字符串参数。
  4. 客户端:将代码参数发送回打开弹出窗口的父窗口。
  5. 客户端:父窗口关闭弹出窗口并向 /auth/provider 发送 POST 请求,并带有code 参数。
  6. 服务器:授权码被交换为访问令牌。
  7. 服务器:使用步骤 6 中的访问令牌检索用户信息。
  8. 服务器:通过用户的唯一提供商 ID 查找用户。如果用户已存在,则获取现有用户,否则创建一个新用户帐户。
  9. 服务器:在第 8 步的两种情况下,创建 JSON Web Token 并将其发送回客户端。
  10. 客户端:解析令牌并将其保存到本地存储以供页面重新加载后使用。

    退出

  11. 客户端:从本地存储中删除令牌

【讨论】:

  • 第 4 步是如何用代码完成的?我知道我们可以通过 window.opener 访问父窗口,但是我们从那里去哪里呢?
【解决方案3】:

这是来自服务器端的登录请求。它将令牌存储在标头中:

router.post('/api/users/login', function (req, res) {
  var body = _.pick(req.body, 'username', 'password');
  var userInfo;

models.User.authenticate(body).then(function (user) {
      var token = user.generateToken('authentication');
      userInfo = user;

      return models.Token.create({
        token: token
      });
    }).then(function (tokenInstance) {
      res.header('Auth', tokenInstance.get('token')).json(userInfo.toPublicJSON());
    }).catch(function () {
      res.status(401).send();
    });
});

这是反应端的登录请求,一旦用户名和密码通过身份验证,我将从标头中获取令牌并在本地存储中设置令牌:

handleNewData (creds) {
    const { authenticated } = this.state;
    const loginUser = {
        username: creds.username,
        password: creds.password
    }
    fetch('/api/users/login', {
        method: 'post',
        body: JSON.stringify(loginUser),
        headers: {
            'Authorization': 'Basic'+btoa('username:password'),
            'content-type': 'application/json',
            'accept': 'application/json'
        },
        credentials: 'include'
    }).then((response) => {
        if (response.statusText === "OK"){
            localStorage.setItem('token', response.headers.get('Auth'));
            browserHistory.push('route');
            response.json();
        } else {
            alert ('Incorrect Login Credentials');
        }
    })
}

【讨论】:

  • 这没有解决 OP 的问题,这是本地身份验证的身份验证策略,而不是社交提供商 oauth 登录流程,在成功身份验证后重定向回应用程序。在 OP 的情况下,没有发出可以将响应发送回前端的登录发布请求,只有通过第三方提供商成功完成身份验证流程的重定向
【解决方案4】:

当您从任何护照身份验证站点获取令牌时,您必须将令牌保存在浏览器的localStorage 中。 Dispatch 是 Redux 的中间件。如果您的应用程序中没有使用 redux,请忽略 dispatch。你可以在这里使用setState(没有redux有点奇怪)。

客户端:

这是我的类似 API,它返回令牌。

保存令牌

axios.post(`${ROOT_URL}/api/signin`, { email, password })
        .then(response => {

            dispatch({ type: AUTH_USER }); //setting state (Redux's Style)
            localStorage.setItem('token', response.data.token); //saving token
            browserHistory.push('/home'); //pushes back the user after storing token
        })
        .catch(error => {
            var ERROR_DATA;
            try{
                ERROR_DATA = JSON.parse(error.response.request.response).error;
            }
            catch(error) {
                ERROR_DATA = 'SOMETHING WENT WRONG';
            }
            dispatch(authError(ERROR_DATA)); //throw error (Redux's Style)
        });

所以当你发出一些经过身份验证的请求时,你必须以这种形式在请求中附加令牌。

经过身份验证的请求

axios.get(`${ROOT_URL}/api/blog/${blogId}`, {
        headers: { authorization: localStorage.getItem('token') } 
//take the token from localStorage and put it on headers ('authorization is my own header')
    })
        .then(response => {
            dispatch({
                type: FETCH_BLOG,
                payload: response.data
            });
        })
        .catch(error => {
            console.log(error);
        });

这是我的 index.js: 每次都会检查令牌,因此即使浏览器刷新,您仍然可以设置状态。

检查用户是否通过身份验证

const token = localStorage.getItem('token');

if (token) {
   store.dispatch({ type: AUTH_USER })
}

ReactDOM.render(
  <Provider store={store}>
    <Router history={browserHistory}>
      <Route path="/" component={App}> 
..
..
..
      <Route path="/blog/:blogid" component={RequireAuth(Blog)} />
//ignore this requireAuth - that's another component, checks if a user is authenticated. if not pushes to the index route
      </Route>
    </Router>
  </Provider>
  , document.querySelector('.container'));

调度动作所做的只是设置状态。

我的 reducer 文件(仅限 Redux)否则你可以在索引路由文件中使用 setState() 来为整个应用程序提供状态。 每次调用 dispatch 时,它都会运行一个类似的 reducer 文件来设置状态。

设置状态

import { AUTH_USER, UNAUTH_USER, AUTH_ERROR } from '../actions/types';

export default function(state = {}, action) {
    switch(action.type) {
        case AUTH_USER:
            return { ...state, error: '', authenticated: true };
        case UNAUTH_USER:
            return { ...state, error: '', authenticated: false };
        case AUTH_ERROR:
            return { ...state, error: action.payload };
    }

    return state;
} //you can skip this and use setState() in your index route instead

从您的 localStorage 中删除令牌以注销。

注意:使用任何不同的名称而不是 token 将令牌保存在浏览器的 localStorage

服务器端:

考虑您的护照服务文件。您必须设置标题搜索。 这是 passport.js

const passport = require('passport');
const ExtractJwt = require('passport-jwt').ExtractJwt;
const JwtStrategy = require('passport-jwt').Strategy;
..
.. 
..
..
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromHeader('authorization'), //client's side must specify this header
secretOrKey: config.secret
};

const JWTVerify = new JwtStrategy(jwtOptions, (payload, done) => {
    User.findById(payload._id, (err, user) => {
        if (err) { done(err, null); }

        if (user) {
            done(null, user);
        } else {
            done(null, false);
        }
    });
});

passport.use(JWTVerify);

在我的 router.js

const passportService = require('./services/passport');
const requireAuthentication = passport.authenticate('jwt', { session: false });
..
..
..
//for example the api router the above react action used
app.get('/api/blog/:blogId', requireAuthentication, BlogController.getBlog);

【讨论】:

  • 我了解如何使用令牌从前端进行身份验证,我只对如何首先将其发送给客户端感兴趣。在 保存令牌 中,您从前端发出 Ajax 请求以使用用户名和密码登录,但是当使用像 passport-facebook(或 passport-twitter 等)这样的 OAuth 策略时.) 由于重定向流程,这不是一个选项。除非我错过了什么。
  • 没有解决如何将身份验证凭据从服务器发送到前端的原始问题。
  • 这是护照本地身份验证策略的流程,OP 的问题专门针对护照社交 oauth 策略,其中用户通过重定向到社交提供商的身份验证流程登录,然后重定向回来到应用程序发布身份验证。 OP 的情况不涉及 post 请求,他们的问题是在用户被重定向到回调路由后如何将令牌传递回前端
猜你喜欢
  • 2021-04-29
  • 2018-07-30
  • 1970-01-01
  • 2022-08-16
  • 1970-01-01
  • 1970-01-01
  • 2012-06-05
  • 2013-03-07
  • 2015-10-19
相关资源
最近更新 更多