目录
Passport
Passport 是 NodeJS 的认证中间件。他的唯一设计目的是:验证请求。书写模块化的、封装代码是一种美德,所以 Passport 将除了验证请求之外的功能都分发给应用程序来实现。关注点分离使代码能够更加整洁、可维护,同时也使 Passport 能够极易集成到应用中。
现在 Web 程序,认证有多种形式。传统的,用户通过用户名、密码登录。随着社交网络使用上升,使用 OAuth 的单点登录,例如 Facebook 或者 Twitter 已经成为了一种流行的认证方式。暴露一个 API 的服务通常需要基于 token 的证书来保护访问。
Passport 认识到每个应用有自己独特的认证需求。认证机制,也被成为策略,被打包成单独的模块。应用能够选择采用的策略,无需创建不需要的依赖。
不管认证的复杂性,代码能够不变的复杂。
安装
$ npm install passport
认证请求
Passport 提供了一个 authenticate()
函数,它作为路由中间件用于认证请求。
认证请求就像调用 passport.authenticate()
和指定采用哪种策略一样简单。authenticate()
的函数签名是标准的 Connect 中间件,所以能够方便的作为路由中间件在 Express 应用中使用。
app.post("/login", passport.authenticate("jwt"), function (req, res) {
// 如果这个函数被调用,说明认证成功。
// `req.user` 包含认证的 user
res.redirect("/user", +req.user.username);
});
默认情况下,如果认证失败,Passport 将返回 401 Unauthorized
,后续其他的路由处理器将不会执行。如果认证成功,下个处理器将调用,req.user
属性将设置为认证 user。
注意:在路由使用策略时,一定要预先配置。继续阅读详细配置 todo 章节。
策略
Passport 使用被称为策略的东西来认证请求。策略从验证用户名、密码,使用 OAuth 委托认证或者使用 OpenID 联合认证。
在让 Passport 认证请求前,应用使用的某个策略(或者某些策略)必须要先配置。
策略和他们的配置通过 use()
函数提供。例如,下面的例子使用passport-jwt 来进行tokne认证。
// 引入JWT策略和请求提取工具以及用户模型
const JwtStrategy = require("passport-jwt").Strategy,
ExtractJwt = require("passport-jwt").ExtractJwt,
UserService = require("../services/UserService");
// 初始化配置对象
opts = {};
// 配置从请求头中提取JWT的方法
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
// 配置JWT的密钥
opts.secretOrKey = "贾公子";
// 配置passport使用JWT策略
module.exports = (passport) => {
passport.use(
new JwtStrategy(opts, async (jwt_payload, done) => {
// 尝试根据JWT载荷中的用户ID查找用户
try {
let user = await UserService.findById(jwt_payload._id);
if (!user) return done(null, false);
return done(null, user);
} catch (error) {
console.log(error);
}
})
);
};
有480多个策略。你可以在 passportjs.org 找到你想要的策略。
验证回调
这个例子引入了一个重要的概念。策略需要一个称为回调的东西。验证回调的目的是找到拥有一套凭证的用户。
当 Passport 认证请求时,它解析请求中的凭证。然后将凭证作为调用回调函数的参数,这个例子中就是 token。如果凭证有效,回调函数将调用 done
将已认证的用户的信息传入 Passport
return done(null, user);
如果验证失败(本例中,比如token错误),done
函数应该传入 false
而不是用户信息来表明认证失败
return done(null, false);
可以提供额外的消息来表明失败原因。这对于展示即时消息,来提示用户再次尝试很有用。
return done(null, false, { message: '密码错误' });
最后,当验证凭证时发生异常(例如,数据库服务不可用),在常规的 Node 操作中 done
应该被调用来传入错误信息。
return done(err);
注意,对于区分两种能够发生失败的情况是很重要的。后者是服务端异常,这种情况下 err
被设置为非 null
的值。在服务器正常运行时,认证失败也是很自然的情况。确认 err
包含 null
,使用最后一个参数传递详细信息。
验证回调通过委派的方式使 Passport 可以无需数据库支持。应用可以自己决定如何存储用户信息,没有验证层强加的任何假设。
会话
Passport 将维护持久的登录会话。为了使持久会话工作,认证用户必须序列化到会话,并且在后续请求时反序列化。
Passport 不会对你如何存储用户记录施加任何限制。相反,你提供给 Passport 的函数实现了必要的序列化和反序列化逻辑。在典型的应用中,这就像序列化用户 ID,以及在反序列化时通过 ID 查找用户一样简单。
在常规的 web 应用中,用于认证用户的凭证仅在登录请求时被发送。如果认证成功,一个session 将被建立和保持,通过设置在浏览器中的 cookie。
每个随后的请求将不再包含凭证,但是会有唯一的 cookie 来确认 session。为了支持登录 session,Passport 将序列化和反序列化
user
实例经由 session。
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function (err, user) {
done(err, user);
});
});
这个例子中,仅 user ID 被序列化到 session 中,从而保持存储在 session 中的数据量较小。当后续请求到达时,这个 ID 将用来找到存储在 req.user
中的 user。
序列化和反序列化逻辑由应用来提供,允许应用选择一个合适的数据库和(或者)对象mapper,无需认证层强加。
中间件
要在 Express 或 Connect 基础的应用中使用 Passport,需要配置必需的 passport.initialize()
中间件。如果你的应用使用持久登录会话(推荐,但不必需),则还必须使用 passport.session()
中间件。
var app = express();
app.use(require('serve-static')(__dirname + '/../../public'));
app.use(require('cookie-parser')());
app.use(require('body-parser').urlencoded({ extended: true }));
app.use(require('express-session')({ secret: 'keyboard cat', resave: true, saveUninitialized: true }));
app.use(passport.initialize());
app.use(passport.session());
注意,开启 session 支持完全是可选的,尽管建议将其用于大多数应用中。如果开启,确认在 passport.session()
前使用 session()
,从而确保登录 session 能够按正确的顺序存储。
禁止 Sessions
成功授权后,Passport 将建立一个持久的登录 session。对于用户通过浏览器访问 web 应用的场景这是有用的。然后,其他情况下,不需要 session 支持。例如,API 服务器通常需要每个请求携带凭证。这种情况下,session 支持能够通过设置 session 选项为 false 来安全的禁用。
app.get('/api/users/me',
passport.authenticate('basic', { session: false }),
function(req, res) {
res.json({id: req.user.id, username: req.user.username});
}
)
自定义回调
如果内部选项不足够处理认证请求,自定义回调能够让应用处理成功和失败的情况。
app.get("/login", function (req, res, next) {
passport.authenticate("local", function (err, user, info) {
if (err) {
return next(err);
}
if (!user) {
return res.redirect("/login");
}
req.logIn(user, function (err) {
if (err) {
return next(err);
}
return res.redirect("/users" + user.username);
});
})(req, res, next);
});
这个例子中,注意 authenticate()
是在路由处理器中被调用,而不是作为路由中间件。这通过闭包给了 req
和 res
对象回调权限。
如果认证失败,user
将被设置为 false
。如果发生异常,err
将被设置。一个可选的 info
参数将传入,包括策略验证回调所提供的附加的详细信息。
这个回调能够使用提供的参数处理预期的认证结果。注意,当使用自定义回调时,需要应用来建立 session(通过调用 req.login()
) 和发送响应。
passport-jwt
一个用于通过JSON Web Token进行身份验证的Passport策略。
这个模块允许您使用JSON Web Token对端点进行身份验证。它旨在用于保护没有会话的RESTful端点。
安装
npm install passport-jwt
使用
配置策略
JWT身份验证策略的构建如下:
new JwtStrategy(options, verify)
options
是一个包含控制如何从请求中提取或验证令牌的选项的对象。
secretOrKey
是一个包含用于验证令牌签名的密钥(对称)或PEM编码的公钥(非对称)的字符串或缓冲区。除非提供了secretOrKeyProvider
,否则必须提供。secretOrKeyProvider
是一个回调函数,格式为function secretOrKeyProvider(request, rawJwtToken, done)
,应该调用done
并传入给定密钥和请求组合的密钥或PEM编码的公钥(非对称)。done
接受参数格式为function done(err, secret)
。注意,实现者需要解码rawJwtToken。除非提供了secretOrKey
,否则必须提供。jwtFromRequest
(必须)函数,接受请求作为唯一参数,并返回JWT字符串或_null_。有关更多详细信息,请参阅从请求中提取JWT。issuer
:如果定义了,将验证令牌的发行者(iss)是否与此值匹配。audience
:如果定义了,将验证令牌的受众(aud)是否与此值匹配。algorithms
:允许的算法名称列表。例如,"HS256","HS384""HS256","HS384"。ignoreExpiration
:如果为true,则不验证令牌的过期时间。passReqToCallback
:如果为true,则将请求传递给验证回调。即verify(request, jwt_payload, done_callback)。jsonWebTokenOptions
:passport-jwt使用jsonwebtoken验证令牌。在这里传入任何其他选项的对象。(例如maxAge)
verify
是一个函数,参数为verify(jwt_payload, done)
jwt_payload
是一个包含解码JWT有效负载的对象。done
是passport错误优先回调,接受参数done(error, user, info)
一个示例配置,它从带有'bearer'方案的http授权头读取JWT:
var JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
var opts = {}
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = 'secret';
opts.issuer = 'accounts.examplesoft.com';
opts.audience = 'yoursite.net';
passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
User.findOne({id: jwt_payload.sub}, function(err, user) {
if (err) {
return done(err, false);
}
if (user) {
return done(null, user);
} else {
return done(null, false);
// 或者您可以创建一个新账户
}
});
}));
JWT可能包含在请求中的多种方式。为了尽可能灵活,JWT通过用户提供的回调函数从请求中解析,该回调函数作为jwtFromRequest
参数传递。从现在起,这个回调函数被称为提取器,接受请求对象作为参数,并返回编码的JWT字符串或_null_。
passport-jwt.ExtractJwt提供了许多提取器工厂函数。这些工厂函数返回一个配置有给定参数的新提取器。
fromHeader(header_name)
创建一个新的提取器,它在给定的http头中查找JWT。fromBodyField(field_name)
创建一个新的提取器,它在给定的正文字段中查找JWT。您必须配置了正文解析器才能使用此方法。fromUrlQueryParameter(param_name)
创建一个新的提取器,它在给定的URL查询参数中查找JWT。fromAuthHeaderWithScheme(auth_scheme)
创建一个新的提取器,它在授权头中查找JWT,期望方案与auth_scheme匹配。fromAuthHeaderAsBearerToken()
创建一个新的提取器,它在带有'bearer'方案的授权头中查找JWT。fromExtractors([array of extractor functions])
使用提供的提取器数组创建一个新的提取器。按顺序尝试每个提取器,直到一个返回令牌。
如果提供的提取器不符合您的需求,您可以轻松地提供自己的回调。例如,如果您使用cookie-parser中间件并希望从cookie中提取JWT,可以使用以下函数作为jwtFromRequest选项的参数:
var cookieExtractor = function(req) {
var token = null;
if (req && req.cookies) {
token = req.cookies['jwt'];
}
return token;
};
// ...
opts.jwtFromRequest = cookieExtractor;
认证请求
使用passport.authenticate()
指定'JWT'
作为策略。
app.post('/profile', passport.authenticate('jwt', { session: false }),
function(req, res) {
res.send(req.user.profile);
}
);
在请求中包含JWT
在请求中包含JWT的方法完全取决于您选择的提取器函数。例如,如果您使用fromAuthHeaderAsBearerToken
提取器,则会在请求中包含一个Authorization
头,并将方案设置为bearer
。例如:
Authorization: bearer JSON_WEB_TOKEN_STRING.....