Web安全:细说后端密码安全防范
上一次笔者写了一篇关于安全的文章:【Web安全:细说前端XSS攻击与防范】投石问路,效果还不错,深受鼓舞,遂决定再接再厉。
其实和web相关的安全问题不仅于此,大致有【XSS攻击】、【前端CSRF攻击】、【前端cookies问题】、【前端点击劫持问题】、【HTTP传输安全】、【DOS攻击】、【后端密码安全】等一系列问题,慢慢给大家总结出来吧。。。
(为什么这么多“前端…”?emmmmmmm,可能是怕攻击频繁,对服务器造成隐患吧。但其实这些前端/后端的防御措施一般都要和后端/前端结合起来才能够更有效的运行)
今天就和各位唠唠“后端密码安全”,简单说说(其实说是没啥说的…),本文代码主要适用于中小型项目,如有不当之处,还请之处嘞。
密码安全两三事
现在一般一提起密码安全,就会想到这些年“大火的”数据库泄露 —— 事实上,这也确实是现在密码安全防范的一个重点:
泄露渠道:
- 数据库被“偷”
- 服务器被入侵
- 通讯被窃听
- 内部人员泄露数据
- 撞库
有了早些年处于热搜的“CSDN几百万用户数据泄露”、“京东账号密码数据库泄露”、“12306数据库泄露”事件,业界终于认识到数据库的“不太安全性”,以及做出了一些基本的防御措施,比如:
- 严禁密码明文存储(防泄露)
- 单向变换(防泄露)
- 密码本身复杂度要求(防猜解)
- “加盐”(防猜解)
后来做了多少努力咱暂且不提,但是现在所说密码安全的防御措施,大多数人应该都下意识想到哈希算法,它有这样几个特点:
- 明文-密文一一对应
- 雪崩效应:只要有一点不对应,则后面所有的都不对应
- 密文无法反推到明文
哈希算法常见有:MD5、SHA1、SHA256…
说到这就会有人问了:第三个特点怎么体现?现在有好多这种MD5反推的网站耶。。。
那是对于简单一些的明文:比如123456之类的。因为他们用的是暴力**(建立了数据字典 —— 也就是我们常说的“彩虹表”)。
可想而知,对于一些非常复杂的密码,而且甚至于加密了好多次,那**几乎就无从谈起了(字典存储是需要空间的,查字典也是要时间的):
表达式 | 难易程度 |
---|---|
md5(明文)=密文 | 易 |
md5(md5(明文))=密文 | 易 |
md5(md5(md5(明文)))=密文 | 稍难 |
md5(sha1(明文))=密文 | 稍难 |
md5(md5(md5(明文+一个复杂字符串)))=密文 | 难 |
md5(sha256(sha1(明文)))=密文 | 难 |
经验:用足够复杂的密码对抗彩虹表,但尽量不要让用户“承担”这件事。
密码安全实战片段
这些天恰好笔者写了一个类似每日新闻的项目,如果做大的话,这种项目的登录注册必须要进行签名/加密,否则在评论区可能会有“意想不到的”错误。
(前端代码省略,主要来看一下后端代码中关于登录密码的片段)
这里笔者用的是node.js,前端用的node模块——ejs,签名所用md5模块:
lib文件夹下的common.js文件:
//md5的封装
const crypto=require('crypto');
module.exports={
MD5_SUFFIX:'ao8durq34fb3($&896359lhd8q是奥迪会36412#',
md5:function(str){
var obj=crypto.createHash('md5'); //以MD5格式签名
obj.update(str);
return obj.digest('hex'); //hex:16进制(输出格式)
}
};
这个文件就是对MD5模块的简单封装,MD5_SUFFIX这个变量就是上文所说的那个为了增加复杂度而在明文后加的“一个很复杂的字符串”。
crypto模块为node中的MD5模块所在模块,有不明白的请移步node官网
这里我们再简化一下场景:假如这个项目中有个【管理员】,它的账号和密码是固定的——
我们可以通过MD5将“(原定)明文密码”进行签名,然后将签名后的密码字符串添加到数据库对应字段里。在用户以【管理员】账号登录时,对用户输入字符串(明文)进行MD5加密,然后比对数据库中字符串,即可。
项目>md5_2.js:
const common=require('./lib/common');
var str='123456';
var str2=common.md5(str+'ao8durq34fb3($&896359lhd8q是奥迪会36412#'); //或者:str+common.MD5_SUFFIX;
console.log(str2);
总之,str+后面的字符串一定要跟上面common文件里的属性变量MD5_SUFFIX一致。
然后就到了最重要的部分了:验证!
这里需要注意一点的是:如果是用post提交数据的话,后端一定要用post接收数据,否则get请求不会被响应,打印出来数据就是undefined了。
route>admin.js:
const express=require('express');
const common=require('../lib/common');
const mysql=require('mysql');
//node连接mysql
var db=mysql.createPool({host:'localhost',user:'root',password:'root',database:'xxx'});
module.exports=function(){
var router=express.Router()
router.use((req,res,next)=>{
if(!req.session['admin_id'] && req.url!='/login'){
res.redirect('/admin/login');
}else{
next();
}
});
router.get('/login',(req,res)=>{
res.render('admin/login.ejs',{});
});
router.post('/login',(req,res)=>{
var username=req.body.username;
var pass=common.md5(req.body.password+common.MD5_SUFFIX);
//node操作mysql语句
db.query(`SELECT * FROM XXX WHERE USERNAME='${username}'`,(err,data)=>{
if(err){
res.status(500).send('databases error').end();
}else{
if(data.length==0){
res.status(400).send('empty databases').end();
}else{
if(data[0].password==pass){
req.session['admin_id']=data[0].ID;
res.redirect('/admin/');
}else{
res.status(400).send('密码错误').end();
}
}
}
})
});
router.get('/',(req,res)=>{
res.send('进入首页了').end();
})
return router;
}
这里有几个比较重要的点:
- req.body——这里使用的body-parser中间件,这个中间件可以让post数据以请求体的形式传到后端
- 第八行以use起头——use其实是get和post的“合体”,常用在不确定请求会以哪种形式发送时使用,这里用use而且不明确【监听路由】,意味着:无论走到哪里,都要经过这个路由监听函数
- 如果是get进来(/login)的,说明什么数据都没有携带,就渲染出登录的模板,如果有数据(第二次进这个网站或者提交数据后)就去判断
这个文件最后将router暴露了出去 —— 它当然要被引用,但这里只是登录设置,同样的,在一个项目中还有其他各种模块…所以我们采用“二级路由”的设置方式:
主文件server.js:
//...
//router
server.use('/',require('./web.js')()); //其他页面
server.use('/admin/',require('./route/admin.js')());
//...
上一篇: 前端的安全防范----点击劫持
下一篇: C语言中全局变量与局部变量