(黑马程序员)MongoDB + Express + art-template 项目实例-博客管理系统 第四页

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: '[email protected]',
    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 
  }
}));

现在我们设置的过期时间是:从登录的时间开始,一天后登录状态就自动失效了。

猜你喜欢

转载自blog.csdn.net/weixin_39202130/article/details/119042148