上一讲创建了用户模型。这一讲,在创建对应用户注册和登录等等操作的端点之前,我们先要创建负责用户身份验证的(也就是与JWT相关的)Express中间件。
什么是Express中间件?如果我们把Express应用比作一座乐高层叠而成的高塔,这座塔接受HTTP请求的对象,经过每一层逻辑的处理之后返回一个HTTP响应的对象,那么一个中间件就是其中的一层,代表了后端应用的一小段逻辑。感兴趣的读者可以阅读我的这篇博文,详细理解下Express的中间件。
后端涉及到令牌的逻辑可以分为两类:发放令牌的逻辑和验证令牌的逻辑;对应的中间件也分为这么两类。本文先讲如何创建及配置发放令牌的中间件。验证的部分,留到下一讲。
创建发放JWT令牌的中间件
我们使用Passport这个库来帮助我们检查用户的身份。这个库是专为Node开发的验证系统,提供了很多验证策略,我们将会使用所谓的“本地验证”(Local Authentication)策略。“本地”是相对于QQ、微信、微博等等第三方验证来说的,说白了也就是考察用户名(或者等同于用户名的邮箱等)和密码两条信息。
配置Passport
新建config/passport.js
文件,写入:
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var mongoose = require('mongoose');
var User = mongoose.model('User');
passport.use(new LocalStrategy({
usernameField: 'user[email]',
passwordField: 'user[password]'
}, function(email, password, done) {
User.findOne({email: email}).then(function(user){
if (user && user.validPassword(password))
return done(null, user);
return done(null, false, {errors: {'email or password': 'is invalid'}});
}).catch(done);
}));
上面的配置代码里,passport
对象使用一个LocalStrategy
对象(来自passport-local
库)。这个对象代表了本地验证的逻辑。不过,我们的用户在登录时不是提供用户名,而是邮箱和密码,类似于:
{
"user": {
"email": "jake@example.com".
"password": "mypasswordisjake"
}
}
所以我们用下面的配置来告诉这个策略对象怎么去从这样一个JSON对象中抽取对应的字段信息:
{
usernameField: 'user[email]',
passwordField: 'user[password]'
}
抽取出来的信息会传给后面的回调函数。这个回调函数首先检查数据库中是否有与传入的邮箱对应的用户,然后检查密码是否正确。注意done
这个参数,它实际上也是一个回调函数,其签名为(err, user, info)=>void
。在后面使用这个passport
对象时,我们需要传入这么个回调来处理验证用户名/密码的结果(成功则发放令牌,不成功则返回错误信息;具体实现见后文)。
在应用里初始化上述的passport
对象
在app.js
里加入如下一行:
require('./models/User');
// +++
require('./config/passport');
// +++
app.use(require('./routes'));
使用passport
的中间件
在应用中创建了passport
对象,现在我们要创建使用这个对象的中间件。
新建routes/api/users.js
文件,写入下面的代码:
const passport = require('passport');
const localAuthentication = (req, res, next) => {
passport.authenticate('local', {session: false}, (err, user, info) => {
if (err) return next(err);
if (!user)
return res.status(422).json(info);
user.token = user.generateJWT();
return res.json({user: user.toAuthJSON()});
} (res, req, next);
}
从签名可以看出localAuthentication
就是一个典型的Express中间件。(再强调一遍,一般的不处理错误的Express中间件,简单来说,就是签名为(req, res, next)=>void
的函数,其三个参数分别代表请求、响应和逻辑栈中的下一个中间件。)事实上,passport.authenticate
生成的也是这么一个函数,只不过为了从外部获得res
参数,所以我们在外面包了一层。
passport.authenticate
方法接受三个参数。
第一个local
表明所用的是本地验证。
Passport默认的验证方式是基于session的,第二个参数则是关闭这种行为。
第三个参数(err, user, info) => {...}
就对应上一节提到的done
回调,是处理验证结果的逻辑的具体实现:如果验证过程中出现错误,则调用后面的一个处理错误的中间件next(err)
;如果验证失败,则回复422;如果验证成功,则返回带有令牌的用户JSON对象。
从上面的代码我们可以看到使用Passport的好处:它为各种验证方式提供了一致而好用的接口,我们只用关心passport
对象的配置和怎么处理验证结果,而不用去处理验证时的各种烦琐的细节。以后如果我们想要使用第三方登录,可以很方便的实现。
现在唯一美中不足的地方是我们没有遵循之前“真实世界”全栈开发-2.2-错误处理与注册登录API中的规定:将错误发生的字域和错误信息配为键值对。万一前端传来的请求体中不包含邮箱或者密码,我们应该向前端准确地指出缺少的到底是哪个字段,而且我们最好在Passport做验证之前就处理这种信息缺失的错误。
在routes/api/users.js
里,把上面的代码改写为:
const localAuthentication = (req, res, next) => {
if(!req.body.user.email)
return res.status(422).json({errors: {email: "can't be blank"}});
if(!req.body.user.password)
return res.status(422).json({errors: {password: "can't be blank"}});
passport.authenticate('local', {session: false}, (err, user, info) => {
if (err) return next(err);
if (!user)
return res.status(422).json(info);
user.token = user.generateJWT();
return res.json({user: user.toAuthJSON()});
}) (req, res, next);
}
上面增加了确保请求体中含有邮箱和密码数据的逻辑,返回的错误信息符合之前的规定。
中间件定义好之后,还需要把它插入到Express应用的中间件栈中,这一步我们留给后面的章节。下一讲我们实现负责验证令牌的中间件。
网友评论