【问题标题】:Session Managment between NodeJS and Angular using JSONWebToken使用 JSONWebToken 在 NodeJS 和 Angular 之间进行会话管理
【发布时间】:2018-04-23 10:52:37
【问题描述】:

我正在尝试以 NodeJS 作为后端的方式构建应用程序,所有业务逻辑都公开 JSON REST 服务以供 angular 4 应用程序使用,而 Angular 4 应用程序只不过是一个愚蠢的客户端。到目前为止一切顺利,但是我很难弄清楚会话管理。

我发现基于令牌的身份验证是一种可行的方法,因为您可能有一天会为移动应用程序提供服务,但是我有一个问题:如果我在服务器端使用 JSONWebToken 策略并将令牌到期时间设置为半小时,那么我的客户需要在半小时后重新进行身份验证,这似乎不太合适,因为这样可能会迫使用户再次登录已经在客户端应用程序上工作的用户,这不是任何 Web 应用程序的工作方式.如果我的令牌在服务器上过期但它违反了愚蠢客户端的原则,我是否还需要在 Angular 级别维护会话管理并自动登录,或者我应该完全放弃它在 NodeJS 上实现会话它自己?另一件事是,如果我实现 WebTokenStrategy,我发现对于来自客户端的每个请求,如果我在 NodeJS 上进行会话管理,我将访问数据库以验证我可以在会话中缓存的用户。

我很难弄清楚的最后一件事是,我可以在 NodeJS 上保护我的资源,但是我还需要根据我的客户端应用程序中的用户权限来提供我的路由和页面,我是否也应该将此信息存储在NodeJS 数据库并由同一个 API 服务器提供服务,但我认为这再次违反了单一责任原则,或者应该有另一个数据库用于此客户端站点路由和用户管理。

有人可以提出一个好的方法吗?如果可能的话,可以举个例子吗?

谢谢。

【问题讨论】:

    标签: node.js angular session express json-web-token


    【解决方案1】:

    没有 JSON Web 令牌不需要访问数据库,因为您在有效负载上编码了所需的信息。但是,如果您希望能够撤销它们,您可以实施 redis 策略(例如,对于正确的更改)。您的服务器将使用签名部分来确保真实性(感谢您的服务器端 JWT 机密)。

    你也可以选择你想要的过期时间。但是,如果您想将其限制为 30 分钟,您也可以实施更新策略。 (在旧令牌即将过期之前请求一个新令牌:服务器只会交付一个具有相同数据编码的新令牌。对于前端更新策略,您可以使用这样的库:

    'use strict';
    
    /**
     * Helper class to decode and find JWT expiration.
     */
    class JwtHelper {
    
      urlBase64Decode(str) {
        let output = str.replace(/-/g, '+').replace(/_/g, '/');
        switch (output.length % 4) {
          case 0: { break; }
          case 2: { output += '=='; break; }
          case 3: { output += '='; break; }
          default: {
            throw 'Illegal base64url string!';
          }
        }
        return this.b64DecodeUnicode(output);
      }
    
      // credits for decoder goes to https://github.com/atk
      b64decode(str) {
        let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
        let output = '';
    
        str = String(str).replace(/=+$/, '');
    
        if (str.length % 4 == 1) {
          throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
        }
    
        for (
          // initialize result and counters
          let bc = 0, bs, buffer, idx = 0;
          // get next character
          buffer = str.charAt(idx++);
          // character found in table? initialize bit storage and add its ascii value;
          ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
            // and if not first of each 4 characters,
            // convert the first 8 bits to one ascii character
            bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
        ) {
          // try to find character in table (0-63, not found => -1)
          buffer = chars.indexOf(buffer);
        }
        return output;
      }
    
      // https://developer.mozilla.org/en/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem
      b64DecodeUnicode(str) {
        return decodeURIComponent(Array.prototype.map.call(this.b64decode(str), (c) => {
          return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
        }).join(''));
      }
    
      decodeToken(token) {
        let parts = token.split('.');
    
        if (parts.length !== 3) {
          throw new Error('JWT must have 3 parts');
        }
    
        let decoded = this.urlBase64Decode(parts[1]);
        if (!decoded) {
          throw new Error('Cannot decode the token');
        }
    
        return JSON.parse(decoded);
      }
    
      getTokenExpirationDate(token) {
        let decoded;
        decoded = this.decodeToken(token);
    
        if (!decoded.hasOwnProperty('exp')) {
          return null;
        }
    
        let date = new Date(0); // The 0 here is the key, which sets the date to the epoch
        date.setUTCSeconds(decoded.exp);
    
        return date;
      }
    
      isTokenExpired(token, offsetSeconds) {
        let date = this.getTokenExpirationDate(token);
        offsetSeconds = offsetSeconds || 0;
    
        if (date == null) {
          return false;
        }
    
        // Token expired?
        return !(date.valueOf() > (new Date().valueOf() + (offsetSeconds * 1000)));
      }
    }
    
    const jwtHelper =  new JwtHelper();
    
    const decodedData = jwtHelper.decodeToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ');
    
    console.log(decodedData)

    【讨论】:

    • 那么,本质上我确实需要一个客户端的会话管理机制来缓存用户名和密码,并在发现令牌在服务器端过期后重新调用它?或者我应该在它自己的有效负载中对​​用户名和密码进行编码,因为它会阻止在客户端站点上缓存用户名/密码?说了这么多,我是否也应该将所有会话信息(如 ACL 等)编码到它自己的有效负载中,以避免访问 DB?
    • 不,在令牌过期之前询问新令牌`isTokenExpired(token, offsetSeconds)`。不要在令牌中存储凭据:这毫无意义,每个人都可以解码令牌,因此这将是一个巨大的安全问题。您还可以在令牌中存储权限:请参阅:npmjs.com/package/express-jwt-permissions
    • 感谢您的回答,但我还是有点困惑。抱歉,添麻烦了。我知道我需要在令牌过期之前重新颁发令牌,但是出于客户端站点的目的,我在客户端维护一个会话并在令牌过期之前请求令牌。您还可以在不从用户凭据生成新令牌的情况下更新令牌吗?其次,它与令牌管理没有直接关系,但我也可以将客户端的页面或路由信息存储在令牌中而不是服务器的REST资源中还是违反原子性原则?
    猜你喜欢
    • 2017-08-14
    • 2012-12-29
    • 2016-02-07
    • 2012-12-15
    • 2013-12-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-26
    相关资源
    最近更新 更多