使用 JSONWebToken 在 NodeJS 和 Angular 之间进行会话管理

Session Managment between NodeJS and Angular using JSONWebToken

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

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

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

有人可以推荐一个好的方法吗?如果可能的话,请举例说明?

谢谢。

否 JSON 网络令牌不需要访问数据库,因为您在有效负载上编码了所需的信息。但是,如果您希望能够撤销它们,则可以实施 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)