Passport & Passport-jwt 实现安全验证

114 阅读10分钟

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() 是在路由处理器中被调用,而不是作为路由中间件。这通过闭包给了 reqres 对象回调权限。

如果认证失败, 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.....