Notes

JWT Authentication Demo

Demo 地址

大致内容如下

将 Express 的纯 JavaScript 项目改造成 TypeScript 项目

  1. 安装 TypeScript,这里采取的是非全局安装的方式

     npm i -D typescript
    
  2. 安装依赖包的 @types 文件

     npm i -D @types/express @types/body-parser @types/debug @types/morgan
    

    在使用 TypeScript 时,安装第三方依赖时,需要尽量地安装对应的 @types 文件,提供给 tsc 进行类型检查,否则,编译可能会不通过

  3. 将脚手架的 JavaScript 文件用 TypeScript 重写一次

    这里可以使用第三方提供的脚手架,不过手动操作一次,可以了解一下流程

    在根目录下新建一个 src 文件夹,用于存放项目的所有源代码 *.ts

    对其中的 app.js, www 文件进行重写时,可以参照微软官方的 TypeScript Node Starter

  4. src 的根目录下,需要添加 TypeScript 的编译配置文件 tsconfig.json

    这个文件描述了 TypeScript 编译为 JavaScript 的一些配置,如目标 JavaScript 的标准版本,输出目录等,更多的配置项可以参考文档 tsconfig.json

    例如,在这个 Demo 中,配置文件如下

     {
         "compilerOptions": {
             "module": "commonjs",
             "outDir": "../built",
             "allowJs": true,
             "target": "es6",
             "sourceMap": true,
         },
         "exclude": [
     		"node_modules"
     	]
     }
    

    需要注意的是,tsconfig.json 需要放置在 TypeScript 文件夹的根目录下,否则,编译时会报错,找不到配置文件

  5. 为了方便,在 package.json 中添加一个编译的脚本命令

     "scripts": {
         "start": "node ./built/www.js",
         "compile": "node ./node_modules/typescript/bin/tsc -p ./src",
         "run": "npm run compile && npm start"
     },   
    
     npm run compile # 编译 `*ts` 文件
     npm start # 运行编译后的 `*.js` 文件,也就是启动服务
     npm run run # 编译后启动服务(想不到什么好名字了,有点啰嗦😂)
    

由于 Demo 中不进行视图的渲染,因此,我们可以对项目文件进行删减,首先是对视图文件的删除,接着是中间件使用的删减,最后的结果是

app.use(logger('dev'));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, '../public')));

编写验证模块

Passport 与验证策略的关系 Passport 对于 Express 这种 Web 框架而言,只是一个简单的中间件;对验证这个功能来说,它是一个框架,负责调度,但不负责真正的验证工作 验证策略,负责真正的验证工作,如 passport-jwt, 则是使用 JWT 这种机制来进行验证,而 Passport 对此并不知情,Passport 只是负责调用相应的策略来验证 因此,Passport 可以调度多种验证策略

创建验证策略

var strategy = new Strategy(StrategyOptions, function(payload, done) {
    const user = users[payload.id] || null;
    if (user) {
      return done(null, {
        id: user.id,
        email: user.email,
      });
    } else {
      return done(new Error('User Not Found'), null);
    }
});

注册验证策略

passport.use(strategy);

在这里,还有另外一种写法

passport.use('strategy name', strategy);

完成验证模块

由于在一个 App 中,我们可能会用到多种验证策略,而将这些策略写在堆放在路由或入口文件会非常难看,因此,将这些上面的步骤都写在一个文件里面,形成一个验证模块

function auth() {
  var strategy = new Strategy(StrategyOptions, function(payload, done) {
    const user = users[payload.id] || null;
    if (user) {
      return done(null, {
        id: user.id,
        email: user.email,
      });
    } else {
      return done(new Error('User Not Found'), null);
    }
  });

  passport.use(strategy);
  return {
    initialize: function() {
      return passport.initialize();
    },

    authenticate: function() {
      return passport.authenticate('jwt', JWTConfig.jwtSession);
    }
  }
}

使用验证模块

基于上面对验证模块的封装,我们可以在路由出简单地调用验证功能

  1. 创建验证对象

     // 引入自己写的验证模块
     import auth from './auth/auth';
        
     // 创建验证器
     const auther = auth();
    
  2. 注册中间件

     app.use(auther.initialize());
    
  3. 对路由的访问进行验证

     app.post('/user',  auther.authenticate(), (req, res) => {
       // 验证成功的回调
     });
    

生成 JWT

关于 JWT 的详细解释,可以参考 Introduction to JSON Web Tokens

简单地说,JWT 是 JSON Web Token

特点

使用场景

结构

一个 JWT 有三个部分组成,每个部分使用 . 进行连接,最后成为一个字符串 xxxx.yyyy.zzzz

典型地,包括

{
  "alg": "HS256",
  "typ": "JWT"
}

最后,将形如上面的 JSON 转换成 Base64 字符串,构成了 Header

Payload

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

最后,将形如上面的 JSON 转换成 Base64 字符串,构成了 Payload

Signature

Signature 用来验证发送者是否为它所声称的用户,即验证你是不是你本人,并确保附带的信息没有经过篡改

生成一个 Signature, 需要有

如采用的是 HMAC SHA256 算法,则 Signature 的生成方法为

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

工作原理

由于使用 JWT 时,我们并没有使用到 cookies, 因此,不存在 CORS 攻击的危险

生成 JWT

在 Demo 中,生成 JWT, 我们


要生成 JWT, 我们需要调用的 API 是 jwt.sign(payload, secretOrPrivateKey, [options, callback]), 为此,我们需要提供两个参数

在上述对 JWT 结构中,我们知道,Payload 中包含了用户的身份信息以及其他对 JWT 的设置信息(如,有效时间),而在 sign 方法中,我们可以将用户的身份信息,对 JWT 的设置信息分开,使得业务代码与技术代码分离,当然,代码运行的时候,它们还是会在一起的

// 用户身份信息
const tokenPayload = {
    id: user.id,
};

// JWT 的设置
const signOptions: jwter.SignOptions = {
    expiresIn: 60, // 有效时间
};

// 生成 JWT
const token = jwter.sign(tokenPayload, configs.JWTConfig.jwtSecret, signOptions);

在 JWTConfig 中,我们定义了 secret

const JWTConfig = {
  jwtSecret: 'secret', // 用于 encode 和 decode token
  jwtSession: {
    session: false, // 禁用 session
  },
};

在调用 sign 方法时,我们可以在最后传入一个回调函数

通过验证保护路由

新建一个路由 /user, 模拟获取用户信息,访问这个路由是,需要客户端带上 JWT, 否则,Passport 会自动返回 401

app.post('/user',  auther.authenticate(), (req, res) => {
  if (req.user) {
    res.json({
      id: req.user.id,
      email: req.user.email,
    });
  } else {
    res.json({

    });
  }
});

用户身份验证的数据流动

在 auth.ts 中,创建策略时

var strategy = new Strategy(StrategyOptions, function(payload, done) {
    const user = users[payload.id] || null;
    if (user) {
      return done(null, {
        id: user.id,
        email: user.email,
      });
    } else {
      return done(new Error('User Not Found'), null);
    }
});

References