如何在服务器身份验证后为前端提供JSON Web令牌?

时间:2017-02-02 07:12:45

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

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

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

更好地说明的例子:

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

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

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

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

  5. ???

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

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

    编辑:要清楚,我问的是,使用Passport在OAuth重定向流后向客户端提供令牌(因此可以保存)的最佳方式是什么。

4 个答案:

答案 0 :(得分:8)

我最近碰到了同样的问题,而且,没有在这里或其他地方找到解决方案,用我深入的想法写了this blog post

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

  1. 将JWT保存在cookie中,然后在未来的步骤中将其提取到前端或服务器上(例如,使用JS在客户端上提取它,或者向服务器发送请求,服务器使用cookie来获取JWT,返回JWT)。
  2. 将JWT作为查询字符串的一部分发回(您在问题中建议)。
  3. 发回一个服务器呈现的HTML页面,其中包含<script>标记:
    1. 自动将嵌入的JWT保存到localStorage
    2. 在此之后自动将客户端重定向到您喜欢的任何页面。
  4. (由于使用JWT登录基本上相当于&#34;将JWT保存到localStorage,我最喜欢的选项是#3,但它有可能存在缺点我还没有我很想听听别人在这里的想法。)

    希望有所帮助!

答案 1 :(得分:0)

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

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

答案 2 :(得分:0)

  1. 客户:通过$ auth.authenticate('提供商名称')打开一个弹出窗口。
  2. 客户:如有必要,请使用该提供商登录,然后授权该应用程序。
  3. 客户端:成功授权后,弹出窗口会重定向回您的应用,例如: http://localhost:3000,带有代码(授权代码)查询字符串参数。
  4. 客户端:代码参数被发送回打开弹出窗口的父窗口。
  5. 客户端:父窗口关闭弹出窗口并使用code参数向/ auth / provider发送POST请求。
  6. 服务器:授权代码交换访问令牌。
  7. 服务器:使用步骤6中的访问令牌重试用户信息。
  8. 服务器:按用户唯一的提供商ID查找用户。如果用户已存在,请抓取现有用户,否则创建新用户帐户。
  9. 服务器:在步骤8的两种情况下,创建一个JSON Web令牌并将其发送回客户端。
  10. 客户端:解析令牌并将其保存到本地存储,以便在页面重新加载后继续使用。

    退出

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

答案 3 :(得分:-1)

当您从任何护照身份验证网站获取令牌时,您必须将令牌保存在浏览器的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);