CTF Rank网站开发笔记(五)——passport.js+geetest+bcrypt整合

  • 2016-12-26
  • 1,137
  • 1
  • 2

难受了也是许多天了,今天总算是把前台都写完了,这次加上了登录注册的功能,用户名密码这次干脆激进一点密码采用了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找了圈,看起来是正在上锁开发中呀~~超级期待的~加油!

发表评论

*

浙ICP备16016405号-2
浙公网安备 33010602007544号