大致内容如下
安装 TypeScript,这里采取的是非全局安装的方式
npm i -D typescript
安装依赖包的 @types
文件
npm i -D @types/express @types/body-parser @types/debug @types/morgan
在使用 TypeScript 时,安装第三方依赖时,需要尽量地安装对应的 @types
文件,提供给 tsc 进行类型检查,否则,编译可能会不通过
将脚手架的 JavaScript 文件用 TypeScript 重写一次
这里可以使用第三方提供的脚手架,不过手动操作一次,可以了解一下流程
在根目录下新建一个 src
文件夹,用于存放项目的所有源代码 *.ts
对其中的 app.js
, www
文件进行重写时,可以参照微软官方的 TypeScript Node Starter
在 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 文件夹的根目录下,否则,编译时会报错,找不到配置文件
为了方便,在 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-jwt
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);
}
});
Strategy
就是 passport-jwt
, 需要引入 import * as passportJWT from 'passport-jwt';
StrategryOptions
是验证的配置,更多的配置选项参见 passport-jwt, 在这里,配置项有
const StrategyOptions: passportJWT.StrategyOptions = {
secretOrKey: JWTConfig.jwtSecret, // 用于对 JWT 进行加密解密用的 secret
jwtFromRequest: ExtractJWT.fromAuthHeaderWithScheme('jwt'), // 表示从 HTTP 报文的头部获取 JWT
// jwtFromRequest: ExtractJWT.fromBodyField('token'), // 表示从 POST 报文的 body 中获取 JWT
};
Authenticate
中获取 JWT 的话,客户端拼接头部的时候,需要将 Scheme 也加上,即 Scheme + ` ` + JWT, 如 jwt xxx.xxx.xxx
payload
是在成功将 JWT 解码后获取到的,这个 payload
是我们自己赋值进去的,下面会讲到passport.use(strategy);
在这里,还有另外一种写法
passport.use('strategy name', strategy);
name
. 像 Demo 中用到的 passport-jwt, 它的默认名字是 jwt
, 这可以通过查看源代码来获知 👉 代码位置passport.authenticate()
作为中间件,插入到需要验证的路由中由于在一个 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);
}
}
}
auth
函数中,这个函数最后返回一个对象initialize()
初始化 Passportauthenticate()
返回一个验证的中间件passport.authenticate
时,我们指定了第二个参数,这个参数的值是 false
, 表明了我们不需要 Passport 生成 session, 因为在 JWT 的原理中并不需要 session基于上面对验证模块的封装,我们可以在路由出简单地调用验证功能
创建验证对象
// 引入自己写的验证模块
import auth from './auth/auth';
// 创建验证器
const auther = auth();
注册中间件
app.use(auther.initialize());
对路由的访问进行验证
app.post('/user', auther.authenticate(), (req, res) => {
// 验证成功的回调
});
passport.use(strategy)
时,最好是显式添加上 name
, 为使用多种策略验证时提供便利而对 auth.ts 中的 authenticate
方法,更是应该命名为更加表意明确的名字,如 JWTAuthenticate
, 这样,在路由添加中间件时,可以写为
app.post('/user', auther.JWTAuthenticate(), (req, res) => {
// 验证成功的回调
});
关于 JWT 的详细解释,可以参考 Introduction to JSON Web Tokens
简单地说,JWT 是 JSON Web Token
一个 JWT 有三个部分组成,每个部分使用 .
进行连接,最后成为一个字符串 xxxx.yyyy.zzzz
xxxx
yyyy
zzzz
典型地,包括
JWT
{
"alg": "HS256",
"typ": "JWT"
}
最后,将形如上面的 JSON 转换成 Base64 字符串,构成了 Header
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
最后,将形如上面的 JSON 转换成 Base64 字符串,构成了 Payload
Signature 用来验证发送者是否为它所声称的用户,即验证你是不是你本人,并确保附带的信息没有经过篡改
生成一个 Signature, 需要有
如采用的是 HMAC SHA256 算法,则 Signature 的生成方法为
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
由于使用 JWT 时,我们并没有使用到 cookies, 因此,不存在 CORS 攻击的危险
在 Demo 中,生成 JWT, 我们
为了让客户端获取到 JWT, 我们需要新建一个路由 /token
, 提供给客户端访问
app.post('/token', (req, res, next) => {
// 校验用户的账号密码输入
// 校验成功,查询用户
// 为用户生成 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({
});
}
});
auther.authenticate()
, 这个正是验证的中间件。路由的处理函数只会在验证成功的时候才会进行调用,否则 Passport 自动返回 401
, 当然,验证失败的处理也是可以自定义的auther.authenticate()
改写成 auther.JWTAuthenticate()
的话,会使得代码的可读性更强req
参数,会自动带上 user
属性,其中包含了用户信息,而这里设计到了一个问题,req.user
中的信息是怎么来的???在 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);
}
});
done()
回调函数的调用中,第二个参数,这就是路由处理函数中 req.user
数据的来源done(null, arg2)
的第二个参数传入req.user
中可以获取到我们传入的信息