Passportjs: nodejs 身份验证中间件
本文资料来源
一、Passport介绍
Passport 是 基于 Express框架的Node.js 的身份验证中间件。 Passport 极其灵活和模块化,可以不显眼地放入任何基于 Express 的 Web 应用程序中。
认证请求流程(以配置localStrategy为例):
当我们通过 接口 /login
进行身份验证时候,发生以下情况:
- 发起
/login
请求时,执行我们设置好的passport.authenticate
;
- 发起
- 开始进行认证,因为我们配置的是
localStrategy
, 所以passport
将调用localStrategy
进行逻辑认证;
- 开始进行认证,因为我们配置的是
- Passport 获取到 请求中的
username
和password
(一般是放在body中,在这里我放在了路径里方便查看),将其传递给策略中的verification function
- Passport 获取到 请求中的
- 加载用户传过来的数据并检查是否符合我们的判定条件
-
- 判定条件结果:
- 判定条件未执行完成:比如在验证的逻辑中出现数据库查询失败,调用其他服务报错等等,调用
done(err)
,将错误信息抛出; - 数据不符合判定条件:当用户输入的参数不符合判定条件,调用
done(null, false)
; - 符合判定逻辑:验证通过,调用
done(null, user)
- 如果 5 验证成功,
passport.authenticate()
自动调用req.login()
函数,该函数主要在用户注册时使用,在此期间可以调用 req.login() 来自动记录新注册的用户。
- 如果 5 验证成功,
- 之后(验证通过)继续调用
passport.serializeUser
方法(之前定义好的),将用户信息序列化,目的放到req.session.passport.user
- 之后(验证通过)继续调用
- 用户信息最终也会放到
req.user
中
- 用户信息最终也会放到
- 流程结束
后续经过身份验证的请求流程
在用户登录之后,用户访问被保护的路由时,会经过如下的流程:
- express 框架加载 session 数据,由于之前登录的用户信息已经被passport 保存在了session中,所以可以从
req.session.passport.user
中找到
- express 框架加载 session 数据,由于之前登录的用户信息已经被passport 保存在了session中,所以可以从
- 这个时候 之前定义好的
passport.initialize
函数就会被调用,如果找到了之前认证过的session信息,就会将passport.user
也放到req中;如果请求没有经过认证,就将passport.user = {}
放到req中
- 这个时候 之前定义好的
- 接下来,
passport.session
就会被触发(每一个请求都会触发这个中间件),逻辑: 如果该中间件在请求中找到了序列化的用户信息,那么这个请求就是已经经过认证了的;
- 接下来,
-
passport.session
会回调之前定义好的passport.deserializeUser
,将反序列化的用户对象放到req.user 中
-
Passport 的方法介绍
-
passport.initialize
: 每个请求进来都会被调用,可以确保session 中有passport.user(用户信息 || {} )
-
passport.session
:passport.session 中间件是一个 Passport 策略,如果服务端找到序列化的用户信息,它将序列化的用户信息加载到 req.user 上 -
passport.deserializeUser
: 每个请求都会通过passport.session
调用该函数,用户信息或者其他的信息可以通过这个函数加载到req.user 上 - 策略什么时候调用: 只有在路由中使用 了
passport.authenticate
中间件才会调用 -
passport.serializeUser
只会在验证期间才会调用,以此来存储对应的用户信息
其他的一些方法
req.login()
req.logout()
req.isAuthenticated()
req.isUnAuthenticated()
策略示例
- passport-wechat
const passport = require('passport');
const WechatStrategy = require('passport-wechat');
passport.use(new WechatStrategy({
appID:'', // {APPID},
name:'wechat-test-demo', // {默认为wechat,可以设置组件的名字}
appSecret:'', // {APPSECRET},
client:'', // {wechat|web},
callbackURL:'/emma/wechat/callback', // {CALLBACKURL},
scope:'', // {snsapi_userinfo|snsapi_base},
state:'', // {STATE},
getToken:'', // {getToken},
saveToken:'', // {saveToken}
}, function(accessToken, refreshToken, profile, done) {
return done(err,profile);
}
));
router.get('/emma/wechat', passport.authenticate('wechat-test-demo', passportOption));
router.get('/emma/wechat/callback', passport.authenticate('wechat-test-demo', {
failureRedirect: '/auth/fail',
successReturnToOrRedirect: '/',
}));
passportOption
state —— 重定向后会带上 state 参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节
callbackURL —— 授权后重定向的回调链接地址, 需要使用 urlEncode 对链接进行处理
scope —— snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过 openid 拿到昵称、性别、所在地。并且, 即使在未关注的情况下,只要用户授权,也能获取其信息 )
- passport-oauth2
const OAuth2Strategy = require('passport-oauth2');
passport.use(new OAuth2Strategy({
authorizationURL: 'https://divineswordvilla.xyz/oauth2/authorize',
tokenURL: 'https://divineswordvilla.xyz/oauth2/token',
clientID: EXAMPLE_CLIENT_ID,
clientSecret: EXAMPLE_CLIENT_SECRET,
callbackURL: "http://divineswordvilla.xyz/auth/emma/callback"
},function(accessToken, refreshToken, profile, cb) {
// 业务逻辑处理
if (err) return cb(err);
return cb(null, profile);
}));
router.get('/emma/login', passport.authenticate('oauth2'));
router.get('/auth/emma/callback', passport.authenticate('oauth2', { failureRedirect: '/login' }), function (req, res) {
// 认证成功之后,跳转到业务需求的界面
res.redirect('/');
});
- passport-jwt
使用JSON web token (jwt) 对端进行认证,可用于一些没有用session的restful 端点
const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'secret',
name: '', // {}
issuer: '',
audience: '',
}, 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);
}
});
}));
app.post('/profile', passport.authenticate('jwt', { session: false }),
function(req, res) {
res.send(req.user.profile);
}
);
- passport-local
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
passport.use(new LocalStrategy(
function(username, password, done) {
User.findOne({ username: username }, function (err, user) {
if (err) { return done(err); }
if (!user) { return done(null, false); }
if (!user.verifyPassword(password)) { return done(null, false); }
return done(null, user);
});
}
));
app.post('/login', passport.authenticate('local', { failureRedirect: '/login' }),
function(req, res) {
// 业务逻辑
res.redirect('/');
});
遗留的问题
- initialize 和 策略 的先后顺序是否会对 passport的使用有影响?
- 当不符合验证(符合验证) 会出现一些什么信息
- 登录完之后,其他请求进入为完整的200;
- 受保护的路由不传用户名密码也能返回200
- 受保护的路由,在验证失败或者过期之后返回 401
- 序列化反序列化目的?
- 序列化反序列化使用场景?
- 什么时候会执行序列化和反序列化函数?
- name 用法
demo