Passport & Passport-jwt 实现安全验证
目录
[TOC]
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.....