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

Posted

技术标签:

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

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

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

举例说明:

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

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

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

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

    ???

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

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

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

【问题讨论】:

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

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

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

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

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

希望有帮助!

【讨论】:

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

    客户端:解析令牌并将其保存到本地存储以供页面重新加载后使用。

    退出

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

【讨论】:

第 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 请求,他们的问题是在用户被重定向到回调路由后如何将令牌传递回前端

以上是关于服务器身份验证后如何为前端提供 JSON Web 令牌?的主要内容,如果未能解决你的问题,请参考以下文章

如何为 Spring Boot RESTful Web 服务配置多级身份验证?

我应该如何为 Firebase 身份验证创建新的联合身份提供程序

在Postgres中加入两个表后如何为具有相同名称的列提供别名

如何为 .NET Core Web API 进行安全身份验证?

如何使用 OIDC (vuejs + nodejs) 对前端和后端进行身份验证?

如何为自托管的Web API添加身份验证?