2.1.12、密码加密处理
项目包含的知识点:密码加密 bcrypt
在数据库中以明文的方式存储密码就是不安全的,所以要对密码进行加密处理。
哈希加密是单程加密方式,即只能加密,不能解密。
例如:1234 => abcd ,假如有个密码是1234,经过加密变成abcd,这个密码只能从1234变成abcd,不能从abcd解密成1234。
还可以在加密的密码中加入随机字符串可以增加密码被破解的难度。
genSalt 是异步 API,返回值是一个 promise 对象,可以在方法前面加上 await ,使用返回值的方式接收生成的随机字符串。
语法:加密密码
// 导入 bcrypt 模块
const bcrypt = require('bcrypt');
// 生成随机字符串 gen => generate 生成 salt 盐
let salt = await bcrypt.genSalt(10);
// 使用随机字符串对密码进行加密
let pass = await bcrypt.hash('明文密码', salt);
语法:密码比对
//密码比对
let isEqual = await bcrypt.compare('明文密码', '加密密码');
bcrypt 依赖的其他环境:
1、python 2.x
python 的下载地址:https://www.python.org/downloads/windows/
下载完成后进行安装,默认安装就好。
然后把python的目录放到系统环境变量中。
2、node-gyp
在命令工具中,输入:
npm install node-gyp -g
3、windows-build-tools
在命令行工具中,输入:
npm install --global --production windows-build-tools
这个安装比较慢,请耐心等待。根据网速的快慢,大概需要10分钟左右。
4、安装 bcrypt
重新开启一个命令行工具,输入:
npm install bcrypt
例子:
在项目根目录下新建 hash.js 文件:
// 导入 bcrypt 模块
const bcrypt = require('bcrypt');
async function run () {
// 生成随机字符串
// genSalt 方法接收一个数值作为参数,默认值为10
// 数值越大 生成的随机字符串复杂度越高,反之复杂度越低
// 返回生成的字符串
const salt = await bcrypt.genSalt(10);
console.log(salt);
}
run()
在命令行工具中,输入:node hash.js,可以看到结果:生成的随机字符串
进行加密处理:
// 导入 bcrypt 模块
const bcrypt = require('bcrypt');
async function run () {
// 生成随机字符串
// genSalt 方法接收一个数值作为参数,默认值为10
// 数值越大 生成的随机字符串复杂度越高,反之复杂度越低
// 返回生成的字符串
const salt = await bcrypt.genSalt(10);
// 对密码进行加密
// 第1个参数是要进行加密的明文,第2个参数是生成的随机字符串
// 返回值是加密后的密码
const result = await bcrypt.hash('123456', salt);
console.log(salt);
console.log(result);
}
run()
在命令行工具中,输入:node hash.js,可以看到结果:加密后的密码
这时就实现了密码加密的功能。
回到项目中,打开 model 目录下的 user.js 文件,把上次注释掉的创建用户代码放开,然后进行修改:
// 密码加密函数
async function createUser () {
//生成随机字符串
const salt = await bcrypt.genSalt(10);
// 进行加密
const pass = await bcrypt.hash('123456', salt);
const user = await User.create({
username: 'itlili',
email: 'lili@163.com',
password: pass,
role: 'admin',
state: 0
})
}
createUser();
在命令行工具中输入:node app.js
然后打开 Compass 软件,可以看到新创建的用户 lili :密码已经是加密后的形式了
把 user.js 文件中的 createUser 函数注释掉:
// createUser();
打开 admin.js 文件,修改用户登录时的密码比对功能的代码:
// 导入 bcrypt 模块
const bcrypt = require('bcrypt');
// 实现登录功能
admin.post('/login', async (req, res) => {
。。。
if (user != null) {
// 查询到了用户,将客户端传递过来的密码与查询出用户信息中的密码进行比对
// trie 比对成功;false 比对失败
let isValid = await bcrypt.compare(password, user.password)
if (isValid) {
//登录成功
res.send('登录成功');
}else{
// 登录失败
res.status(400).render('admin/error.art', {msg: '邮件地址或密码错误'})
}
}
。。。
});
在命令行工具中,输入:nodemon app.js ,启动服务,然后刷新登陆页,填写正确的信息提交,可以看到:登录成功
验证登录效果:
在用户登陆成功后,将用户名存储在 req 这个请求对象中,然后在浏览器中访问 user 用户列表页,在用户列表页中从请求对象中获取用户名,将用户名显示在页面中。如果页面中能显示用户名,就说明用户真的登陆成功了,如果用户名不能显示,就说明用户的登陆是失败的。
修改 admn.js 文件:
if (isValid) {
// 登录成功
// 将用户名存储在请求对象中
req.username = user.username
res.send('登录成功');
}
用户列表路由添加参数:
// 创建用户列表路由
admin.get('/user', (req, res) => {
res.render('admin/user.art', {
msg: req.username
})
});
打开 views-admin-user.art 文件,添加代码:
<h4>用户 {{msg ? msg : '用户名不存在'}}</h4>
在浏览器打开 login 页,输入正确的用户信息,登录成功后,在输入:http://localhost/admin/user 地址,可以看到:
说明登录没成功。
2.1.13、保存登陆状态
项目包含的知识点:cookie 与 session
cookie:浏览器在电脑硬盘中开辟的一块空间,主要供服务器端存储数据。
● coolie 中的数据是以域名的形式进行区分的。
● coolie 中的数据是有过期时间的,超过时间数据会被浏览器自动删除。
● coolie 中的数据会随着请求被自动发送到服务器端。
session:实际上就是一个对象,存储在服务器端的内存中,在 session 对象中也可以存储多条数据,每一条都有一个 sessionid 作为唯一标识。
在 node.js 中需要借助 express-session 实现 session 功能。
下载安装:
npm install express-session
示例代码:
const session = require('express-session');
app.use(session({ secret: 'secret key' }));
把登陆状态存储到 cookie
打开 app.js 文件,导入 express-session 模块:
// 导入 express-session 模块
const session = require('express-session');
// 配置 session
app.use(session({ secret: 'secret key'}));
打开 admin.js 文件,修改代码:
if (isValid) {
// 登录成功
// 将用户名存储在请求对象中
req.session.username = user.username
res.send('登录成功');
}
在命令行工具中启动服务:nodemon app.js
在浏览器中刷新页面,重新输入用户信息,登录成功后,查看 Application 可以看到:
connect.sid 是 express-session 设置的默认名字,它对应的值是加密字符串,在这个加密的字符串里保存的是服务器端给客户端生成唯一的 sessionid。
接下来我们再往服务器端发送请求的时候,这个 cookie 就会被自动携带。服务器端接收到这个 cookie,并且从 cookie 中提取出对应的 sessionid,然后在 session 对象当中,根据这个 sessionid 去查找用户信息,如果查找到了,就说明用户登陆成功。
修改 admin.js 文件中的用户列表路由:
// 创建用户列表路由
admin.get('/user', (req, res) => {
res.render('admin/user.art', {
msg: req.session.username
})
});
在浏览器刷新页面,重新登陆,登陆成功后在浏览器输入:http://localhost/admin/user ,可以看到:用户名显示出来了
下面要实现登录成功后跳转到用户列表页,同时在页面右上角把用户的信息显示出来。
在 admin.js 文件中添加重定向:
if (user != null) {
// 查询到了用户,将客户端传递过来的密码与查询出用户信息中的密码进行比对
// trie 比对成功;false 比对失败
let isValid = await bcrypt.compare(password, user.password)
if (isValid) {
// 登录成功
// 将用户名存储在请求对象中
req.session.username = user.username
// 重定向到用户列表页
res.redirect('/admin/user');
// res.send('登录成功');
}
刷新浏览器重新登陆,登陆成功后自动跳转到用户列了。
通过 app.locals 把用户名显示在页面的右上角:
修改 admin.js 文件,把用户列表路由的 msg 去掉,并在登录成功后添加 app.locas :
// 创建用户列表路由
admin.get('/user', (req, res) => {
res.render('admin/user.art')
});
if (user != null) {
// 查询到了用户,将客户端传递过来的密码与查询出用户信息中的密码进行比对
// trie 比对成功;false 比对失败
let isValid = await bcrypt.compare(password, user.password)
if (isValid) {
// 登录成功
// 将用户名存储在请求对象中
req.session.username = user.username
// res.send('登录成功');
// 在 req.app 里拿到的就是 app.js 里的app
req.app.locals.userInfo = user;
// 重定向到用户列表页
res.redirect('/admin/user');
}
删除掉 user.art 中的代码 : {{msg ? msg : '用户名不存在'}}
并在 header.art 文件中添加:
<span class="btn dropdown-toggle" data-toggle="dropdown">
{{userInfo.username}}
<span class="caret"></span>
</span>
打开浏览器回到登陆页面,重新登陆用户信息,登陆成功后可以在用户列表的右上角看到用户的用户名:
这是因为在没登陆的情况下,是没有 userInfo 这个属性的,也就没有 userInfo 这个属性下的 username。
打开 header.art 文件,添加个判断:
{{ userInfo && userInfo.username }}
重新刷新页面,可以看到用户列表页:没有登陆,所以右上角的用户名为空
而我们想要的是在用户没有登陆的情况下,是不能访问到用户列表页的。
这是我们需要使用中间件进行拦截,注意:中间是有顺序的,从上到下顺序执行。所以中间件的代码要写带路由之间。
打开 app.js 文件:
// 拦截请求,判断用户登录状态
app.use('/admin', (req, res, next) => {
// 判断用户访问的是否是登录页面
// 判断用户的登录状态
// 如果用户是登录的,将请求放行,向下执行;如果用户不是登录的,则将请求重定向到登录页
if (req.url != '/login' && !req.session.username) {
// 重定向到登录页
res.redirect('/admin/login');
} else {
// 用户是登录的,将请求放行,向下执行
next()
}
})
回到浏览器刷新:http://localhost/admin/user ,发现跳转到了 http://localhost/admin/login
功能实现:只有登陆成功了,才能访问到用户列表页。
下面要对当前代码进行优化
app.js 文件中我们只想引入一些模块,做一些基础的配置,不想把具体的功能代码写带里面。所以我们要把登陆拦截的代码分离出去。
在项目根目录下新建 middleware 文件夹,存放中间件,并创建 loginGuard.js 文件:把拦截中的函数代码剪切过来
const guard = (req, res, next) => {
// 判断用户访问的是否是登录页面
// 判断用户的登录状态
// 如果用户是登录的,将请求放行,向下执行;如果用户不是登录的,则将请求重定向到登录页
if (req.url != '/login' && !req.session.username) {
// 重定向到登录页
res.redirect('/admin/login');
} else {
// 用户是登录的,将请求放行,向下执行
next();
}
}
module.exports = guard;
在 app.js 文件中引入:
// 拦截请求,判断用户登录状态
app.use('/admin', require('./middleware/loginGuard'));
下面我们在浏览器中重新测试验证下,发现功能还是一样的。
再来优化 admin.js 这个路由文件
在 route 目录下新建 admin 文件夹,新建 login.js 文件,把实现登录功能的代码,剪切过来,并把用到的模块引入过来:
// 导入用户集合构造函数
const { User } = require('../../model/user');
// 导入 bcrypt 模块
const bcrypt = require('bcrypt');
const login = async (req, res) => {
// 接收请求参数
const {email, password} = req.body;
// 如果用户没有输入邮件地址或密码
if (email.trim().length == 0 || password.trim().length == 0) {
// return res.status(400).send('<h4>邮件地址或密码错误</h4>')
return res.status(400).render('admin/error.art', {msg: '邮件地址或密码错误'})
}
// 根据邮箱地址查询用户信息
let user = await User.findOne({email: email.trim()})
// 如果查询到了用户,user 变量的值是对象类型
// 如果没有查询到用户,user 变量为空
if (user != null) {
// 查询到了用户,将客户端传递过来的密码与查询出用户信息中的密码进行比对
// trie 比对成功;false 比对失败
let isValid = await bcrypt.compare(password, user.password)
if (isValid) {
// 登录成功
// 将用户名存储在请求对象中
req.session.username = user.username
// res.send('登录成功');
// 在 req.app 里拿到的就是 app.js 里的app
req.app.locals.userInfo = user;
// 重定向到用户列表页
res.redirect('/admin/user');
}else{
// 登录失败
res.status(400).render('admin/error.art', {msg: '邮件地址或密码错误'})
}
} else {
// 没有查询到用户
res.status(400).render('admin/error.art', {msg: '邮件地址或密码错误'})
}
}
module.exports = login;
再在 admin.js 文件中引入:
// 实现登录功能
admin.post('/login', require('./admin/login'));
回到浏览器在测试 验证下,功能都正常。
login.js 文件还可以简化下:
// 导入用户集合构造函数
const { User } = require('../../model/user');
// 导入 bcrypt 模块
const bcrypt = require('bcrypt');
module.exports = async (req, res) => {
// 接收请求参数
const {email, password} = req.body;
// 如果用户没有输入邮件地址或密码
if (email.trim().length == 0 || password.trim().length == 0) {
// return res.status(400).send('<h4>邮件地址或密码错误</h4>')
return res.status(400).render('admin/error.art', {msg: '邮件地址或密码错误'})
}
// 根据邮箱地址查询用户信息
let user = await User.findOne({email: email.trim()})
// 如果查询到了用户,user 变量的值是对象类型
// 如果没有查询到用户,user 变量为空
if (user != null) {
// 查询到了用户,将客户端传递过来的密码与查询出用户信息中的密码进行比对
// trie 比对成功;false 比对失败
let isValid = await bcrypt.compare(password, user.password)
if (isValid) {
// 登录成功
// 将用户名存储在请求对象中
req.session.username = user.username
// res.send('登录成功');
// 在 req.app 里拿到的就是 app.js 里的app
req.app.locals.userInfo = user;
// 重定向到用户列表页
res.redirect('/admin/user');
}else{
// 登录失败
res.status(400).render('admin/error.art', {msg: '邮件地址或密码错误'})
}
} else {
// 没有查询到用户
res.status(400).render('admin/error.art', {msg: '邮件地址或密码错误'})
}
}
下面我们把其他的路由也都分离出来:
新建 loginPage.js 文件:
module.exports = (req, res) => {
res.render('admin/login.art')
}
新建 userPage.js 文件:
module.exports = (req, res) => {
res.render('admin/user.art')
}
在 admin.js 文件中引入:
// 渲染登录页面
admin.get('/login', require('./admin/loginPage'));
// 实现登录功能
admin.post('/login', require('./admin/login'));
// 创建用户列表路由
admin.get('/user', require('./admin/userPage'));
2.1.14、实现功能
在服务器端删除这个用户对应的 session,还要删除客户端的 cookie,这样客户端和服务器端就断开了联系,也就实现了退出。
打开 views-admin-common 目录下 header.art 文件:
<li><a href="/admin/logout">退出登录</a></li>
打开 route 目录下 admin.js 文件:创建退出功能路由
// 实现退出功能
admin.get('/logout', require('./admin/logout'));
在route-admin 目录下新建 logout.js 文件:
module.exports = (req, res) => {
// 删除 session
req.session.destroy(function () {
// 删除 cookie
res.clearCookie('connect.sid');
// 重定向到登陆页面
res.redirect('/admin/login');
})
}
回到浏览器刷新页面,登陆成功后,点击退出登陆。可以看到跳回了登录页。
但是还有个问题,当我们退出登录以后,还能看到Cookie:
这是因为看到的这个 Cookie 已经不是你登录的那个 Cookie 了。因为在 session 方法里有个配置 ,这个配置 saveUninitialized: true 保存未初始化的 Cookie,意思是:只要客户端访问服务器端,不管登没登录,都存储一个 Cookie。所以我们要把这个参数修改下。
打开 app.js 文件:修改 session 配置
// 配置 session
app.use(session({ secret: 'secret key' , saveUninitialized: false}));
刷新浏览器重新登录,再退出,可以看到已经没有 Cookie 了。
还有个问题,我们在配置 session 时,没有指定 Cookie 的过期时间,如果在存储 session 的时候没去指定 Cookie 的过期时间,那么这个 Cookie 在浏览器关闭的时候,这个 Cookie 就会自动被删除掉。而我们希望的是在一天后如果不登录,那么就自动失效。
继续修改 session 配置
// 配置 session
app.use(session({
secret: 'secret key' ,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
}));
现在我们设置的过期时间是:从登录的时间开始,一天后登录状态就自动失效了。