Passport
基于Passport.js 的权限认证
参考了
认证又称 “ 验证 ”
登陆认证,是用户在访问应用或者网站时,通过是先注册的用户名和密码,告诉应用使用者的身份,从而获得访问权限的一种操作。
几乎所有的应用都需要登陆认证!
策略
策略配置
本地认证
const LocalStrategy = require("passport-local").Strategy;
passport.use(
"local",
new LocalStrategy(
{
usernameField: "email",
passwordField: "password",
},
(uEmail, uPassword, done) => {
db.User.findOne({ email: uEmail, provider: "local" })
.then((user) => {
if (user) {
// validatePassword 是 User 模型自带的数据校验辅助函数
return user
.validatePassword(uPassword)
.then(() => {
return done(null, user) || true;
})
.catch((err) => {
return done(err, null);
});
} else {
return done(new Error("INVALID_LOGIN"), null);
}
})
.catch((err) => {
done(err, null);
});
}
)
);
如果使用
// 绑定对于用户密码进行加密的操作
userSchema.statics.hashPassword = (rawPwd) => {
return bcrypt.hash(rawPwd);
};
// 绑定对于密码的验证操作
userSchema.methods.validatePassword = function (rawPwd) {
return bcrypt.compare(rawPwd, this.password).then((isValid) => {
return isValid
? true
: Promise.reject(new Error(lang.t("auth:errors:invalidlogin")));
});
};
注意,这里的字段名称应该是页面表单提交的名称,即 req.body.xxx
,而不是
将
在
当发生系统级异常时,返回
混合策略
const passport = require('passport')
, LocalStrategy = require('passport-local').Strategy
, AnonymousStrategy = require('passport-anonymous').Strategy;
...
// 匿名登录认证作为本地认证的 fallback
passport.use(new AnonymousStrategy());
...
app.get('/',
passport.authenticate(['local', 'anonymous'], { session: false }),
function(req, res){
if (req.user) {
res.json({ msg: "用户已登录"});
} else {
res.json({ msg: "用户以匿名方式登录"});
}
});
框架集成
登录认证
const express = require('express');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const flash = require('express-flash');
const passport = require('passport');
...
// 在使用 app.use 之前需要进行 passport 的配置
app.use(cookieParser());
app.use(session({...}));
app.use(flash())
app.use(passport.initialize());
app.use(passport.session());
...
const ExpressBrute = require('express-brute')
const ExpressBruteMongooseStore = require('express-brute-mongoose')
app.post(
"/login",
passport.authenticate("local", {
successRedirect: "/",
failureRedirect: "/login",
failureFlash: true,
}),
function (req, res) {
// 验证成功则调用此回调函数
res.redirect("/users/" + req.user.username);
}
);
// controllers/auth.js
...
// 使用 ExpressBruteMongooseStore 来存放爆破信息,也可以使用 MemoryStore 将信息存放于内存
const EBstore = new ExpressBruteMongooseStore(db.Bruteforce)
const bruteforce = new ExpressBrute(EBstore, {
freeRetries: 5,
minWait: 60 * 1000,
maxWait: 5 * 60 * 1000,
refreshTimeoutOnRequest: false,
failCallback (req, res, next, nextValidRequestDate) {
req.flash('alert', {
class: 'error',
title: lang.t('auth:errors.toomanyattempts'),
message: lang.t('auth:errors.toomanyattemptsmsg', { time: moment(nextValidRequestDate).fromNow() }),
iconClass: 'fa-times'
})
res.redirect('/login')
}
})
// 处理来自表单提交中包含的登录信息
router.post('/login', bruteforce.prevent, function (req, res, next) {
new Promise((resolve, reject) => {
// [1] LOCAL AUTHENTICATION
passport.authenticate('local', function (err, user, info) {
if (err) { return reject(err) }
if (!user) { return reject(new Error('INVALID_LOGIN')) }
resolve(user)
})(req, res, next)
}).then((user) => {
// LOGIN SUCCESS
// 执行用户登录操作,将用户 ID 写入到 Session 中
return req.logIn(user, function (err) {
if (err) { return next(err) }
req.brute.reset(function () {
return res.redirect('/')
})
}) || true
}).catch(err => {
// LOGIN FAIL
if (err.message === 'INVALID_LOGIN') {
req.flash('alert', {
title: lang.t('auth:errors.invalidlogin'),
message: lang.t('auth:errors.invalidloginmsg')
})
return res.redirect('/login')
} else {
req.flash('alert', {
title: lang.t('auth:errors.loginerror'),
message: err.message
})
return res.redirect('/login')
}
})
})
...
const router = express.Router();
// body parser
const bodyParser = require("koa-bodyparser");
app.use(bodyParser());
// Sessions
const session = require("koa-session");
app.keys = ["secret"];
app.use(session({}, app));
const passport = require("koa-passport");
app.use(passport.initialize());
app.use(passport.session());
访问校验
注意上面的代码里有个
logIn(user, options, callback):用
验证用户提交的凭证是否正确,是与
// 获取用户编号,用于在 logIn 方法执行时向 Session 中写入用户编号,ID 或者 Token 皆可
passport.serializeUser(function (user, done) {
done(null, user._id);
});
// 根据 ID 查找用户,也是为了判断用户是否存在
passport.deserializeUser(function (id, done) {
db.User.findById(id)
.then((user) => {
if (user) {
done(null, user);
} else {
done(new Error(lang.t("auth:errors:usernotfound")), null);
}
return true;
})
.catch((err) => {
done(err, null);
});
});
这里第一段代码是将环境中的
第二段代码是从
//这里getUser方法需要自定义
app.get("/user", isAuthenticated, getUser);
// 将req.isAuthenticated()封装成中间件
module.exports = (req, res, next) => {
// 判断用户是否经过认证
if (!req.isAuthenticated()) {
if (req.app.locals.appconfig.public !== true) {
return res.redirect("/login");
} else {
req.user = rights.guest;
res.locals.isGuest = true;
}
} else {
res.locals.isGuest = false;
}
// 进行角色的权限校验
res.locals.rights = rights.check(req);
if (!res.locals.rights.read) {
return res.render("error-forbidden");
}
// Expose user data
res.locals.user = req.user;
return next();
};
app.get("/logout", function (req, res) {
req.logout();
res.redirect("/");
});
OAuth
* OAuth 验证策略概述
*
* 当用户点击 “ 使用 XX 登录 ” 链接
* * 若用户已登录
* * 检查该用户是否已绑定 XX 服务
* - 如果已绑定,返回错误(不允许账户合并)
* - 否则开始验证流程,为该用户绑定XX服务
* * 用户未登录
* * 检查是否老用户
* - 如果是老用户,则登录
* - 否则检查OAuth返回profile中的email,是否在用户数据库中存在
* - 如果存在,返回错误信息
* - 否则创建一个新账号
const OAuth2Strategy = require('passport-oauth').OAuth2Strategy;
passport.use('provider', new OAuth2Strategy({
authorizationURL: 'https://www.provider.com/oauth2/authorize',
tokenURL: 'https://www.provider.com/oauth2/token',
clientID: '123-456-789',
clientSecret: 'shhh-its-a-secret'
callbackURL: 'https://www.example.com/auth/provider/callback'
},
function(accessToken, refreshToken, profile, done) {
User.findOrCreate(..., function(err, user) {
done(err, user);
});
}
));
const GitHubStrategy = require("passport-github2").Strategy;
passport.use(
"github",
new GitHubStrategy(
{
clientID: appconfig.auth.github.clientId,
clientSecret: appconfig.auth.github.clientSecret,
callbackURL: appconfig.host + "/login/github/callback",
scope: ["user:email"],
},
(accessToken, refreshToken, profile, cb) => {
db.User.processProfile(profile)
.then((user) => {
return cb(null, user) || true;
})
.catch((err) => {
return cb(err, null) || true;
});
}
)
);
router.get(
"/login/ms",
passport.authenticate("windowslive", {
scope: ["wl.signin", "wl.basic", "wl.emails"],
})
);
router.get(
"/login/google",
passport.authenticate("google", { scope: ["profile", "email"] })
);
router.get(
"/login/facebook",
passport.authenticate("facebook", { scope: ["public_profile", "email"] })
);
router.get(
"/login/github",
passport.authenticate("github", { scope: ["user:email"] })
);
router.get(
"/login/slack",
passport.authenticate("slack", {
scope: ["identity.basic", "identity.email"],
})
);
router.get("/login/azure", passport.authenticate("azure_ad_oauth2"));
router.get(
"/login/ms/callback",
passport.authenticate("windowslive", {
failureRedirect: "/login",
successRedirect: "/",
})
);
router.get(
"/login/google/callback",
passport.authenticate("google", {
failureRedirect: "/login",
successRedirect: "/",
})
);
router.get(
"/login/facebook/callback",
passport.authenticate("facebook", {
failureRedirect: "/login",
successRedirect: "/",
})
);
router.get(
"/login/github/callback",
passport.authenticate("github", {
failureRedirect: "/login",
successRedirect: "/",
})
);
router.get(
"/login/slack/callback",
passport.authenticate("slack", {
failureRedirect: "/login",
successRedirect: "/",
})
);
router.get(
"/login/azure/callback",
passport.authenticate("azure_ad_oauth2", {
failureRedirect: "/login",
successRedirect: "/",
})
);