速通passport源码以及策略源码
前提
- 具备知识:passport、express、最好有nest的基础知识
使用passport
- 策略:以何种方式去校验用户
- 定义策略
var passport = require('passport');
var LocalStrategy = require('passport-local');
passport.use(new LocalStrategy(function verify(username, password, callback) {
db.get('SELECT * FROM users WHERE username = ?', [ username ], function(err, user) {
// ... 校验username、password的逻辑
// callback(error: any, user?: Express.User | false, info?: any)
// error:错误,user:用户信息(成功传递),info:一般是失败信息(不算错误,但是失败的原因)
callback(null, user); // 校验成功
// 校验错误:callback(new Error("DB查无此人"));
// 校验失败:callback(null, false, "账号密码不一致")
});
}));
// express全局中间件上使用
app.use()
- Express中间件的普通使用:默认没有定义认证回调函数的,都会执行next()或next(err)抛出异常
// local默认是LocalStrategy内置的
app.post('/login/password',
// 后面的options有很多,表示成功、失败后如何处理,大多都是通用的
// 如:failureRedirect表示失败后重定向地址
// failureMessage:失败后在req.session.messages上增加失败原因
passport.authenticate('local', { failureRedirect: '/login', failureMessage: true }),
function(req, res) {
res.redirect('/~' + req.user.username);
});
- Express中间件使用认证回调函数:要注意next()调用
app.get('/protected', function(req, res, next) {
passport.authenticate('local', function callback(err, user, info, status) {
if (err) { return next(err) } // 认证失败交给下一个中间件处理
if (!user) { return res.redirect('/signin') }
res.redirect('/account'); // 认证成功跳转
})(req, res, next); // 手动调用passport.authenticate()返回的中间件
}, (req, res) {
// "/protected"接口逻辑
});
- nestjs整合passport、passport-jwt策略
- 定义策略,定义jwt策略中间件
// verify.middleware.ts
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { MemoService } from "../memo/memo.service";
import { NextFunction, Request, Response } from "express";
import { JsonWebTokenError } from "jsonwebtoken";
import passport from "passport";
import { Strategy as JWTStrategy, VerifiedCallback } from "passport-jwt";
@Injectable()
export default class VerifyMiddleware {
constructor(
private readonly configService: ConfigService, // 配置服务
private readonly memoService: MemoService // redis服务
) {
passport.use(
"jwt", // 虽然passport-jwt本身就叫"jwt",这里起到自定义作用
new JWTStrategy(
{
// 1. 获取Token的方法
jwtFromRequest: (req: Request) => {
if (!req.headers["authorization"].includes("Bearer ")) {
return null;
}
// 这里可以自定义,但是返回的结果一定是JWT,否则passport-jwt进行校验,失败则抛出异常
// 抛出异常就会执行passport.fail() -> 下一个策略,没有时调用认证回调函数()
return req.headers["authorization"].split("Bearer ")[1];
},
ignoreExpiration: false,
secretOrKey: this.configService.get("JWT_SECRET"),
passReqToCallback: true, // 钩子传入Request参数
},
// 2. 此策略的校验钩子(此时代表jwt校验是成功的,可以自己进行二次其他的校验)
// TIP:
// 这里最好写同步写法,不要async/await,因为passport-jwt中并未对异步做支持,要用异步就用Promise
// done(err) -> passport.error() -> 认证回调()
// done(null, payload) -> passport.success() -> 认证回调()
async (req: Request, payload: IPayload, done: VerifiedCallback) => {
if (!payload.userID || !payload.email || !payload.exp) {
done(new JsonWebTokenError("伪造JWT"), null);
return;
}
// 校验是否与redis中的accessToken一致
const accessToken = req.headers["authorization"].split("Bearer ")[1];
this.memoService.getLoginInfo(payload.userID).then(loginInfo => {
if (!loginInfo) {
done(null, null, { message: "用户已登出" });
return;
} else if (loginInfo.accessToken !== accessToken) {
done(null, null, { message: "JWT非最新" });
return;
}
// 此处调用passport.success()的回调函数,即下面的`authCallback()`
done(null, payload);
});
}
)
);
}
// nest中间件
use(req: Request, res: Response, next: NextFunction) {
passport.authenticate(
"jwt",
{
session: false,
},
function authCallback(
err: Error,
user: IPayload,
info: { message: string }
) {
// 错误处理
const forbidden = err || info;
if (forbidden) {
let tip = "";
if (info) {
tip = info.message;
} else {
tip = err.message;
}
const errRet = ResponseUtil.error(tip);
return res.status(400).json(errRet).end();
}
// 执行下一个中间件
req.user = user;
next(); // 一定要调用,passport不会为我们自动调用的
}
)(req, res, next);
}
}
- 挂载nest中间件
// app.module.ts
import VerifyMiddleware from "./middleware/verify.middleware";
export class AppModule {
async configure(consumer: MiddlewareConsumer) {
consumer
.apply(VerifyMiddleware)
.exclude("/auth/register", "/auth/login", "/auth/refresh")
.forRoutes("*");
}
}
图解
图片内容较多,不清晰右键在新标签中浏览。
passport、passport-jwt源码分析
-
passport-jwt/strategy.js:是一个策略构造函数
-
passport/authenticator.js#use():注册Strategy构造函数
// use():设置Strategy策略
// use("custom", JwtStrategy) 自定义名称的策略
// use("custom", JwtStrategy) 用JwtStrategy.name作为策略名
// 最后挂载到_strategies策略集合上,便于后面调用这些策略
Authenticator.prototype.use = function(name, strategy) {
if (!strategy) {
strategy = name;
name = strategy.name;
}
if (!name) { throw new Error('Authentication strategies must have a name'); }
this._strategies[name] = strategy;
return this;
};
// 所以我们得以如下形式调用
passport.use(new JwtStrategy(...));
- 🔥核心:passport/middleware/authenticate.js#authenticate():执行预配置,并返回一个中间件函数
// 一开始调用并非它,而核心最后执行的是该方法
module.exports = function authenticate(passport, name, options, callback) {
if (typeof options == 'function') {
callback = options;
options = {};
}
options = options || {};
var multi = true;
// 判断策略是否是单个还是多个,便于后面决定是否执行下一个策略
if (!Array.isArray(name)) {
name = [ name ];
multi = false;
}
// 返回的中间件
return function authenticate(req, res, next) {
// 这些都是passport内置的方法(passpot/http/request.js内)
// 具备session 且 passport.authenticate("session")下,passport.serializeUser()、passport.deserializeUser()才有作用
// req.login(user)相当于req.user = user;
req.login =
req.logIn = req.logIn || IncomingMessageExt.logIn;
// req.login(user)相当于req.user = undefined;
req.logout =
req.logOut = req.logOut || IncomingMessageExt.logOut;
// 根据options.userProperty设置req[options.userProperty]作为登录依据。存在即为true,反之为false
req.isAuthenticated = req.isAuthenticated || IncomingMessageExt.isAuthenticated;
req.isUnauthenticated = req.isUnauthenticated || IncomingMessageExt.isUnauthenticated;
req._sessionManager = passport._sm;
// 每一个策略失败都会push一个错误原因
var failures = [];
// 所有策略结束即调用该方法,如果存在认证回调则传入失败(info参数而不是err参数)
function allFailed() {
if (callback) {
if (!multi) {
return callback(null, false, failures[0].challenge, failures[0].status);
} else {
var challenges = failures.map(function(f) { return f.challenge; });
var statuses = failures.map(function(f) { return f.status; });
return callback(null, false, challenges, statuses);
}
}
// 下面是根据options设置相关内容
var failure = failures[0] || {}
, challenge = failure.challenge || {}
, msg;
if (options.failureFlash) {
var flash = options.failureFlash;
if (typeof flash == 'string') {
flash = { type: 'error', message: flash };
}
flash.type = flash.type || 'error';
var type = flash.type || challenge.type || 'error';
msg = flash.message || challenge.message || challenge;
if (typeof msg == 'string') {
req.flash(type, msg);
}
}
if (options.failureMessage) {
msg = options.failureMessage;
if (typeof msg == 'boolean') {
msg = challenge.message || challenge;
}
if (typeof msg == 'string') {
req.session.messages = req.session.messages || [];
req.session.messages.push(msg);
}
}
if (options.failureRedirect) {
return res.redirect(options.failureRedirect);
}
// When failure handling is not delegated to the application, the default
// is to respond with 401 Unauthorized. Note that the WWW-Authenticate
// header will be set according to the strategies in use (see
// actions#fail). If multiple strategies failed, each of their challenges
// will be included in the response.
var rchallenge = []
, rstatus, status;
for (var j = 0, len = failures.length; j < len; j++) {
failure = failures[j];
challenge = failure.challenge;
status = failure.status;
rstatus = rstatus || status;
if (typeof challenge == 'string') {
rchallenge.push(challenge);
}
}
res.statusCode = rstatus || 401;
if (res.statusCode == 401 && rchallenge.length) {
res.setHeader('WWW-Authenticate', rchallenge);
}
if (options.failWithError) {
return next(new AuthenticationError(http.STATUS_CODES[res.statusCode], rstatus));
}
res.end(http.STATUS_CODES[res.statusCode]);
}
// allFailed()结束
// 自执行第一个策略
(function attempt(i) {
var layer = name[i];
if (!layer) { return allFailed(); } // 不存在策略了,即调用上面的失败函数
// 处理策略,strategy变量就是能运行的策略函数
var strategy, prototype;
if (typeof layer.authenticate == 'function') {
strategy = layer;
} else {
prototype = passport._strategy(layer);
if (!prototype) { return next(new Error('Unknown authentication strategy "' + layer + '"')); }
strategy = Object.create(prototype);
}
// 为策略添加实例方法
// 成功:如果有认证回调函数则执行,否则执行options配置的内容并调用next();
strategy.success = function(user, info) {
if (callback) {
return callback(null, user, info);
}
info = info || {};
var msg;
if (options.successFlash) {
var flash = options.successFlash;
if (typeof flash == 'string') {
flash = { type: 'success', message: flash };
}
flash.type = flash.type || 'success';
var type = flash.type || info.type || 'success';
msg = flash.message || info.message || info;
if (typeof msg == 'string') {
req.flash(type, msg);
}
}
// ...
};
// 失败:递归下一个策略
strategy.fail = function(challenge, status) {
if (typeof challenge == 'number') {
status = challenge;
challenge = undefined;
}
// 推送错误进failures数组
failures.push({ challenge: challenge, status: status });
attempt(i + 1); // 递归执行下一个策略
};
// 重定向(这里是兼容Express 4-版本)
strategy.redirect = function(url, status) {
// 可以看看注释
// NOTE: Do not use `res.redirect` from Express, because it can't decide
// what it wants.
//
// Express 2.x: res.redirect(url, status)
// Express 3.x: res.redirect(status, url) -OR- res.redirect(url, status)
// - as of 3.14.0, deprecated warnings are issued if res.redirect(url, status)
// is used
// Express 4.x: res.redirect(status, url)
// - all versions (as of 4.8.7) continue to accept res.redirect(url, status)
// but issue deprecated versions
res.statusCode = status || 302;
res.setHeader('Location', url);
res.setHeader('Content-Length', '0');
res.end();
};
// 跳出所有策略校验,直接去执行下一个中间件
strategy.pass = function() {
next();
};
// 错误:要么执行认证回调函数,要么next(err)交给下一个中间件
strategy.error = function(err) {
if (callback) {
return callback(err);
}
next(err);
};
strategy.authenticate(req, options); // 执行策略并传入options
})(0); // attempt
};
};
⚠️ 这就是为什么我们使用了认证回调后,必须手动next()的原因
app.get('/protected', function(req, res, next) { passport.authenticate('local', function callback(err, user, info, status) { if (err) { return next(err) } // 认证失败交给下一个中间件处理 next(); // ⚠️ })(req, res, next); }, (req, res) { // "/protected"接口逻辑 });
对于想深入Session策略部分的同学,移步到官网即可。Passport: Sessions
- 🔥 passport-jwt核心代码 passport-jwt/lib/strategy.js#JwtStrategy.prototype.authenticate()
JwtStrategy.prototype.authenticate = function(req, options) {
var self = this;
var token = self._jwtFromRequest(req); // 获取jwt
if (!token) {
return self.fail(new Error("No auth token"));
}
// 校验jwt
this._secretOrKeyProvider(req, token, function(secretOrKeyError, secretOrKey) {
if (secretOrKeyError) {
self.fail(secretOrKeyError)
} else {
// Verify the JWT
JwtStrategy.JwtVerifier(token, secretOrKey, self._verifOpts, function(jwt_err, payload) {
if (jwt_err) {
return self.fail(jwt_err); // 校验失败,执行下一个中间件
} else {
// Pass the parsed token to the user
var verified = function(err, user, info) {
if(err) {
// 有错误
return self.error(err);
} else if (!user) {
// user为false
return self.fail(info);
} else {
// 成功,交给passport给策略挂载的sucess()
return self.success(user, info);
}
};
try {
if (self._passReqToCallback) {
// 具备options.passReqToCallback时,多传递request对象
// verified是上面这个函数
self._verify(req, payload, verified);
} else {
self._verify(payload, verified);
}
} catch(ex) {
// 遇到错误调用passport给策略挂载的error()
self.error(ex);
}
}
});
}
});
};
图解源代码
图解passport-oauth2源码
推荐在新标签中打开,配合debug源码查看流程!
简单分析passport-oauth2的StateStore逻辑
- passport-oauth2/strategy.js:创建Store容器
function OAuth2Strategy(options, verify) {
if (typeof options == 'function') {
verify = options;
options = undefined;
}
options = options || {};
// ... 必填参数校验
passport.Strategy.call(this);
// ...策略参数写入
this.name = 'oauth2';
this._verify = verify;
this._oauth2 = new OAuth2(options.clientID, options.clientSecret,
'', options.authorizationURL, options.tokenURL, options.customHeaders);
// ...
// 设置stateStore容器,这很重要(🚀 自定义)
if (options.store && typeof options.store == 'object') {
// 自己实现的stateStore
this._stateStore = options.store;
} else if (options.store) {
// 这里和下面的分支一个样,都是用到了session,我们分析一个就可以了
// 源码: state/store.js
this._stateStore = options.pkce ? new PKCEStateStore({ key: this._key }) : new StateStore({ key: this._key });
} else if (options.state) {
// 这里和上面的分支一个样,都是用到了session
this._stateStore = options.pkce ? new PKCEStateStore({ key: this._key }) : new NonceStore({ key: this._key });
} else {
// 该Store的源码为不去生成/校验state随机值
if (options.pkce) { throw new TypeError('OAuth2Strategy requires `state: true` option when PKCE is enabled'); }
this._stateStore = new NullStore();
}
}
- passport-oauth2/strategy.js(283行):调用Store容器的store方法
// 容器执行完store后会被调用,并发出302重定向去进行第三方站点认证授权
function stored(err, state) {
if (err) { return self.error(err); }
if (state) { params.state = state; }
var parsed = url.parse(self._oauth2._authorizeUrl, true);
utils.merge(parsed.query, params);
parsed.query['client_id'] = self._oauth2._clientId;
delete parsed.search;
var location = url.format(parsed);
self.redirect(location);
}
try {
// 调用store方法
var arity = this._stateStore.store.length;
if (arity == 5) {
this._stateStore.store(req, verifier, state, meta, stored);
} else if (arity == 4) {
this._stateStore.store(req, state, meta, stored);
} else if (arity == 3) {
this._stateStore.store(req, meta, stored);
} else { // arity == 2
this._stateStore.store(req, stored);
}
} catch (ex) {
return this.error(ex);
}
- passport-oauth2/state/store.js:存储state、校验state
SessionStore.prototype.store = function(req, state, meta, callback) {
var key = this._key; // 一个表示passport-oauth2存储的key
var sstate = {
handle: uid(24) // 随机uid
};
if (state) { sstate.state = state; }
if (!req.session[key]) { req.session[key] = {}; }
req.session[key].state = sstate; // 存储
callback(null, sstate.handle); // 去执行上面代码的回调
};
// 校验并删除session[key]的值
SessionStore.prototype.verify = function(req, providedState, callback) {
// ...
var key = this._key;
if (!req.session[key]) {
return callback(null, false, { message: 'Unable to verify authorization request state.' });
}
var state = req.session[key].state;
if (!state) {
return callback(null, false, { message: 'Unable to verify authorization request state.' });
}
delete req.session[key].state;
if (Object.keys(req.session[key]).length === 0) {
delete req.session[key];
}
if (state.handle !== providedState) {
return callback(null, false, { message: 'Invalid authorization request state.' });
}
return callback(null, true, state.state);
};
🚀 综上所述,如果你想自定义一个Store,直接按照这个文件为模板编写即可!
引用
- Passport官方文档:Sessions
- Passport Gihub Dev 源码
- passport-jwt Gihub Dev 源码
- Github测试代码:verify.middleware.ts
这个是我项目测试的代码