CTF Rank网站开发笔记(五)——passport.js+geetest+bcrypt整合
难受了也是许多天了,今天总算是把前台都写完了,这次加上了登录注册的功能,用户名密码这次干脆激进一点密码采用了bcrypt加密存储。这里对整合过程做个记录。
首先是bcrypt,一般会采用在用户存储时进行加密,sequelize模型中建立hooks,使用beforeCreate方法对密码进行加密,并增加一个validPassword方法来进行比较:
"use strict"; var bcrypt = require('bcrypt'); module.exports = function(sequelize, DataTypes) { return sequelize.define('users', { id: { type: DataTypes.BIGINT, allowNull: false, primaryKey: true, autoIncrement: true }, ...(太多了,省略中间部分) regtime: { type: DataTypes.DATE, allowNull: false } }, { hooks:{ beforeCreate:function (user,options,next) { bcrypt.hash(user.password,10,function(err,hash) { user.password = hash; next(null,user); }); } }, instanceMethods:{ validPassword:function(password) { return bcrypt.compareSync(password,this.password); } }, tableName: 'users' }); };
这里稍微记录一下bcrypt.hash其实有2个重载方法,以前老的写法通常是先调用bcrypt.genSalt()方法,然后再进行bcrypt.hash(password,salt,callback)进行运算,现在有了一个新方法bcrypt.hash(password,rounds,callback),直接输入运算轮数,算法会自动生成salt。
下面是bcrypt给出的参考计算时间:
rounds=8 : ~40 hashes/sec rounds=9 : ~20 hashes/sec rounds=10: ~10 hashes/sec rounds=11: ~5 hashes/sec rounds=12: 2-3 hashes/sec rounds=13: ~1 sec/hash rounds=14: ~1.5 sec/hash rounds=15: ~3 sec/hash rounds=25: ~1 hour/hash rounds=31: 2-3 days/hash
一般来说10 Rounds就已经很慢了,超过10Rounds就会影响正常用户使用,所以一般取10即可。
另外,通常情况下我们用的bcrypt是一个C++ library需要编译才能使用,现在好像还有个叫bcrypt-nodejs的库,是纯js实现的bcrypt,貌似现在比较推荐使用后一个,比较容易避免一些麻烦。
passport的整合:
passport是js实现的一个认证库,既可以本地认证,也可以进行OAuth的第三方认证。我这里直接用本地的认证就可以:
首先在app.js将passport引入:
var passport = require('passport') , LocalStrategy = require('passport-local').Strategy;
然后还需要定义serializeUser和deserializeUser方法用于认证用户信息的存取:
passport.serializeUser(function(user, done) { done(null, user.id); }); passport.deserializeUser(function(id, done) { User.findById(id).then(function(user) { done(null, user); },function (err){ done(err, false); }); });
最后我们需要定义认证的方法:
passport.use(new LocalStrategy({ usernameField: 'username' }, function(username, password, done) { User.findOne({ where: { username : username } }).then(function (user) { if (!user) { return done(null, false, { message: '用户名或密码错误' }); } if (!user.validPassword(password)) { return done(null, false, { message: '用户名或密码错误' }); } return done(null, user); }); } ));
为了实现用户登录状态的保持,我们还需要express-session进行session的操作:
app.use(cookieParser('secret')); app.use(session({ secret: 'secret', name:'sid', saveUninitialized: true, resave: false, cookie:{ maxAge:24*3600*1000 //session过期时间为1天 } }));
然后进行初始化就可以使用了:
app.use(passport.initialize()); app.use(passport.session());
在用户认证的时候,使用passport.authenticate(‘local’,callback)(req,res,next);进行认证,其中callback就是前面定义的那个LocalStrategy里面定义的done函数,根据callback函数的参数就可以知道用户认证的结果了。当用户认证通过,可以调用req.logIn()函数进行登录操作。
利用locals注入用户信息:
由于req.logIn()成功后会在req.user中保存用户信息,但这个信息是不能直接在ejs模板中取出的,需要从controller的res.render()方法传入,我们肯定不打算每个页面都去传这个值,这时候可以用res.locals来解决,在所有router之前加入:
app.use(function(req,res,next){ res.locals.user=req.user; //为每个页面注入登录信息 next(); });
这时候req.user就注入到res返回给模板的的user变量中去了。
整合geetest:
geetest是个不错的第三方验证码框架,使用还算方便,这次就打算用一下。
对于后端,只需copy&paste官方的demo即可:
myutils.pcGeetest = new Geetest({ geetest_id: 'appid', geetest_key: 'appkey' }); router.get('/register', function (req,res) { // 向极验申请每次验证所需的challenge myutils.pcGeetest.register(function (err, data) { if (err) { // 进入failback,如果一直进入此模式,请检查服务器到极验服务器是否可访问 // 可以通过修改hosts把极验服务器api.geetest.com指到不可访问的地址 // 为以防万一,你可以选择以下两种方式之一: // 1. 继续使用极验提供的failback备用方案 res.send(data); // 2. 使用自己提供的备用方案 // todo } else { // 正常模式 res.send(data); } }); }); router.post("/validate",function (req,res) { // 对ajax提供的验证凭证进行二次验证 myutils.pcGeetest.validate({ challenge: req.body.geetest_challenge, validate: req.body.geetest_validate, seccode: req.body.geetest_seccode }, function (err, success) { if (err) { // 网络错误 res.send({ status: "error", info: err }); } else if (!success) { // 二次验证失败 res.send({ status: "fail", info: '登录失败' }); } else { res.send({ status: "success", info: '登录成功' }); } }); });
然后在页面里插入geetest的js即可:
DOM中插入:
<div id="float-captcha"></div>
js脚本:
var handlerFloat = function (captchaObj) { $("#float-submit").click(function (e) { var validate = captchaObj.getValidate(); if (!validate) { $("#notice").removeClass("hide"); $("#notice").addClass("show"); setTimeout(function () { $("#notice").removeClass("show"); $("#notice").addClass("hide"); }, 2000); e.preventDefault(); } }) // 将验证码加到id为captcha的元素里,同时会有三个input的值:geetest_challenge, geetest_validate, geetest_seccode captchaObj.appendTo("#float-captcha"); captchaObj.onReady(function () { $("#wait").removeClass("show"); $("#wait").addClass("hide"); }); // 更多接口参考:http://www.geetest.com/install/sections/idx-client-sdk.html }; $.ajax({ // 获取id,challenge,success(是否启用failback) url: "/geetest/register?t=" + (new Date()).getTime(), // 加随机数防止缓存 type: "get", dataType: "json", success: function (data) { // 使用initGeetest接口 // 参数1:配置参数 // 参数2:回调,回调的第一个参数验证码对象,之后可以使用它做appendTo之类的事件 initGeetest({ gt: data.gt, challenge: data.challenge, product: "float", // 产品形式,包括:float,embed,popup。注意只对PC版验证码有效 offline: !data.success // 表示用户后台检测极验服务器是否宕机,一般不需要关注 // 更多配置参数请参见:http://www.geetest.com/install/sections/idx-client-sdk.html#config }, handlerFloat); } });
这样,在form提交后会插入geetest的几个field字段,后端收到后还需要提交给geetest服务器进行再次验证:
myutils.pcGeetest.validate({ challenge: req.body.geetest_challenge, validate: req.body.geetest_validate, seccode: req.body.geetest_seccode }, function (err, success) { if (err) { // 网络错误 res.send(err); } else if (!success) { // 二次验证失败 } else { //验证成功 } });
只有前端+后端校验都无误,才算通过。
最后补充下,以前在express 3.x的时候,获取node环境变量是用process.env.NODE_ENV来获取的,而现在express 4.x继续这样做已经不行了,而应该用req.app.get(‘env’)来获取,这一点还是坑了我有点久,而现在NODE_ENV的环境变量,如果不设置那默认就是development,所以在部署的时候注意要set NODE_ENV=production,不然有些错误可能会回显给用户,造成某些不安全因素。
以上就是这次踩坑的过程,记录下过程以方便以后开发。
西瓜
啊啊在微博上看到的,本来想找找到底是哪儿的神器,google了下没发现却发现了这篇文章,然后又跑去博主的github找了圈,看起来是正在上锁开发中呀~~超级期待的~加油!