web前端 | 博客(二)登录功能

扶醉桌前 提交于 2020-01-06 20:29:47

实现登录功能

  1. 创建用户集合,初始化用户
    1. 连接数据库
    2. 创建用户集合
    3. 初始化用户
  2. 为登录表单项设置请求地址,请求方式(GET方法会将参数放到地址栏中,不隐蔽,要用POST方法,它将参数放到消息体中,比较隐蔽)以及表单name属性
  3. 当用户点击登录按钮时,客户端验证用户是否填写了登录表单
  4. 如果其中一项没有输入,则阻止表单提交
  5. 服务器端请求接收参数,验证用户是否填写了登录表单(有时候客户端的js代码会被禁用,无法正确识别表单的准确性,故服务端的表单验证必不可少)
  6. 如果有一项没有输入,为客户端做出响应,阻止程序向下执行(例如,如果没有填写邮箱,则找不到该用户)(无论邮箱地址错误还是密码错误,一律提示两者都错,防止用户恶意猜出其他用户的账号密码)
  7. 根据邮箱地址查询用户信息
  8. 如果用户不存在,为客户端做出响应
  9. 如果用户存在,将用户名和密码进行比对
  10. 比对成功,则用户登录成功
  11. 比对失败,则则用户登录失败

数据库

数据库连接

在model中新建connect.js和user.js,分别用于数据库连接和创建用户集合。

connect.js

//连接数据库
//引入mongoose第三方模块,这个对象下面有个connect方法用户连接数据库
const mongoose = require('mongoose');

//连接数据库
mongoose.connect('mongodb://localhost/blog');
        .then(() => console.log('数据库连接成功'))
        .catch(() => console.log('数据库连接失败'))

在connect方法后,如果连接成功,会执行then中的函数;如果失败,会执行catch中的函数。

但是此时,connect.js只是一个独立的模块,要想真正连接数据库,要将这个模块引入到入口文件中去。

故在app.js文件中,使用require的方式,引入connect.js。require在引入文件时,同时会执行文件。

require('./model/connect');

由于它并不返回任何模块成员,故不需要使用变量去接受。

再使用mongodb --dbpath E:\mongodb\data,启动数据库
保存修改的代码,再命令行可以看到数据库连接成功。

设定用户集合

用户集合中要存的字段:
用户名
邮箱(登录)
密码
角色(普通用户,管理员)
状态(启用,禁用)

再user.js中要使用到mongoose的Schema,故也要引入mongoose模块。
Schema是一个构造函数,由于要创建一个实例,故要使用new,并且用一个常量去接受这个实例。
而创建这个实例的时候,可以把集合规则当作参数传入。


//创建用户集合规则
const userSchema = new mongoose.Schema({
  username:{
    type: String,
    //只能保证注册的时候用户一定要提供username的字段,如果字段中存了undefined 或 none 则也能够通过验证
    require: true,
    minlength: 2,
    maxlength: 20
  },
  email: {
    type: String,
    //保证邮箱地址在插入数据库时不重复
    unique: true,
    require:true
  },
  password: {
    type: String,
    require:true
  },
  role: {
    type: String,
    require: true
  },
  // 0 启用状态
  // 1 禁用状态
  state: {
    type: Number,
    default: 0
  }
});

使用mongoose的model方法,创建一个User集合,使用上方变量中的规则。model方法会返回集合的构造函数,可以用集合的构造函数对集合进行各种各样的操作。故使用一个常量接受这个构造函数

const User = mongoose.model('User', userSchema);

最后,将用户集合作为模块成员进行导出。

module.exports = {
  Users : User
}

由于在es6中,如果对象的键和值名称一样,可以省略掉值。即仅仅写成User也可以。

module.exports = {
  Users
}

此时,用户集合创建完成。

初始化用户

在user.js中调用User的create方法,在参数中传入信息。

User.create({
  username: 'username1',
  email: 'username1@qq.com',
  password: '1',
  role: 'admin',
  state: 0
}).then(() => {
  console.log('用户创建成功')
})
  .catch(() => {
    console.log('用户创建失败')
})

在app.js中引入,进行测试

require('./model/user');

在这里插入图片描述
提示成功。

在compass中输入mongodb://localhost/blog
连接数据库成功,查看User集合,插入数据成功
在这里插入图片描述

此时可以把app.js中的require('./model/user');去掉了,因为其实并不能在app.js中引用,而应该在路由文件中引入User,因为我们在路由文件中对数据库进行操作,而不是在app.js中对路由进行操作,并且要把创建用户的测试代码注释掉,否则多次引入时会出现错误。(快捷键ctrl+/)

登录表单

在login.art中找到登录表单(<form>标签)
把表单的请求地址设置为/login

action="/login"

请求方式设置为post

method="post"

设置id

id="loginForm"

还要为每一个表单项设置name属性,如果不设置,当表单提交到服务器端时,服务器端是接收不到表单的请求参数的。用户输入的name属性的值,最好与数据库中字段的值保持一致。
所以在两个input标签里,分别加上name="email"name="password"

客户端表单验证

阻止表单自动提交

在表单提交时,要先阻止表单自动提交的行为,先对表单内容进行验证。
阻止表单提交,先添加一段js代码

    <script type="text/javascript">
      //为表单添加事件
      $( '#loginForm' ).on('submit', function(){
        //阻止表单默认提交的行为
        return false;
      })
    </script>

获取表单信息

在获取用户信息时,可以选择为控件设置id属性,但是表单项较多时,代码将会比较啰嗦。
jquery提供了一个serializeArray()方法,可以获取表单信息,它的返回值是一个数组。

      //为表单添加事件
      $( '#loginForm' ).on('submit', function(){
        //阻止表单默认提交的行为
        //[{name:'email', value:'用户输入的内容'}]
        var f = $(this).serializeArray();
        console.log(f)
        return false;
      })

http://localhost/admin/login输入一些值后,控制台内容如下:
在这里插入图片描述
然而,包含对象的数组始终不是特别方便,我们希望它就是一个对象,对象中

{email: 'aaa@aa.com', password: 'asdf'}

然而并没有方法可以实现这样的功能,所以要自己来实现。
要把serializeArray的返回值转为需要的格式,即把一个数组转换为对象。
这个方法,应该是建立一个空对象,对数组进行循环,把name属性的值作为对象的属性,把value属性的值作为对象属性的值。

      function serializeToJson(form){
        var result = {};
        //[{name:'email', value:'用户输入的内容'}]
        var f = form.serializeArray();
        //item的形式就和上面那个注释一样
        f.forEach(function(item) {
          //result.email 即 result[item.name]
          result[item.name] = item.value;
        });
        return result;
      }

其中var f = form.serializeArray();之前的$(this)改为了form
再在提交时使用这个函数

  //为表单添加事件
      $( '#loginForm' ).on('submit', function(){
        //$(this)就是表单对象
        var result = serializeToJson($(this));
        console.log(result)
        //阻止表单默认提交的行为
        return false;
      })

重新进入localhost/admin/login并提交表单,控制台显示内容如下:
在这里插入图片描述
由于对表单进行验证的方法是一个常用的公共方法,所以在public/admin/js下新建common.js,把方法剪切进去,再用script标签引入。

<script src="/admin/js/common.js"></script>

而要想让所有的文件都可以使用到这个js文件,可以在骨架文件layout.art中引入这个js文件。

表单验证

要对用户输入的内容进行验证,使用result.就行了。
result.email获得email字符串,.trim()去除字符串两边的空格,.length判断字符串长度。如果email长度>0则说明输入不为空,也不全是空格,==0则用户没有输入空格

        if(result.email.trim().length == 0){
          alert('需要邮箱地址');
          //阻止程序向下执行,不只写return,否则下面的return不执行,无法阻止表单提交
          return false;
        }
        //如果用户没有输入密码,阻止程序向下执行
        if(result.password.trim().length == 0){
          alert('需要密码');
          //阻止程序向下执行
          return false;
        }

添加实现登录功能的路由

使路由接收post方法

将form的请求地址改为

action="/admin/login"

在route下的admin.js文件中,新建实现登录功能的路由。

admin.post('/login', (req, res) => {
  //接受请求参数
})

表示请求方法是post,地址是login,请求参数req, res分别代表请求对象和响应对象。
由于请求方式是post,而接收post请求参数,需要用到第三方模块body-parser
npm install body-parser

安装完成后,要在app.js中引入该模块,并且对该模块进行全局的配置,即拦截请求,并把请求交给body-parser处理。

//引入body-parser模块 用来处理post请求参数
const bodyParser = require('body-parser')

//处理post请求参数,extended的值决定了用什么模块去处理post请求参数的格式
app.use(bodyParser.urlencoded({extended: false}));

接下来,就可以在route中的admin.post()方法中接受post请求参数。

admin.post('/login', (req, res) => {
 //接受请求参数
 //暂时将请求参数写入body中
 res.send(req.body);
})

可以看到,表单提交后会跳转到这样一个界面:
在这里插入图片描述

表单的二次验证

把req.body中的email, password解构出来,判断是否符合格式

const {email, password} = req.body;
  //如果用户没有输入邮件地址
if (email.trim().length == 0){
  //return 阻止了程序向下运行
  return res.status(400).send('<h4>邮件地址或者密码错误</h4>');//send中默认的状态码是200,可是此时出现了问题,状态码应该是400
}
if (password.trim().length == 0){
  //return 阻止了程序向下运行
  return res.status(400).send('<h4>邮件地址或者密码错误</h4>');//send中默认的状态码是200,可是此时出现了问题,状态码应该是400
}

在谷歌浏览器中禁用js代码,提交空的表单,即可看到错误提示。

错误提示优化

可以在views/admin下新建error.art文件,继承骨架文件,并且在main坑里填上一个提示错误的p标签,展示错误信息。
把admin.js中的.send()改为.render('admin/error', {msg: '邮件地址或密码错误'})
即可向admin/error渲染错误信息。

要错误页面出现3秒后再跳转到登录页面,即再error.art中填一下script的坑,即

  <script type="text/javascript">
    setTimeout(function (){
      location.href = '/admin/login'
    }, 3000)
  </script>

根据邮箱地址查询用户信息

通过require方法,把user.js导入到当前文件中来
由于user.js中是通过

module.exports = {
 User
}

一个对象中的成员,把对象暴露出来
故再admin.js中也要使用

const { User } = require('../model/user');

而由于目录是这样的
在这里插入图片描述
user.js在admin.js的上一级的model文件夹下,所以要引用user.js
要写成../model/user

使用User.findOne({email: email})来找到唯一的邮件地址,也可以写成User.findOne({ email })
由于要通过异步的方式来获取这个方法的返回值,所以要在函数定义前加上async关键字,在耗时方法前加上await关键字。
通过一个变量接收返回值。

  //根据邮箱地址查询用户信息
  //如果查询到了用户,user变量的值是对象类型,对象中存储的是用户信息
  //如果没有查询到用户,user变量为空
  try{
      let user = await User.findOne({email});
      //查询到了用户
      if( user ) {
          if(password == user.password) {
            //登录成功
            res.send('登陆成功')
          } else {
            //登陆失败
            res.status(400).render('admin/error', {msg: '邮箱地址或者密码错误'});
          }
      } else {
        //没有查询到用户
        res.status(400).render('admin/error', {msg: '邮箱地址或者密码错误'});
      }
  }catch(e){
        console.log(e)
  }

使用密码加密bcrypt

安装

使用Nodejs的第三方模块 bcrypt
哈希密码是单程加密方式,
在加密密码中加入随机字符串可以增加密码被破解的难度
使用之前,按顺序:
下载python2.x并配置环境变量
安装node-gyp 和 windows-build-tools
npm install -g node-gyp
npm install --global --production windows-build-tools
npm install bcrypt

bcrypt用法

在blog下新建hash.js

//导入bcrypt
const bcrypt = require('bcrypt');
async function run() {

  //生成随机字符串
  //gensalt(),接受一个数值作为参数
  //数值越大代表生成的随机字符串复杂程度越高
  //返回生成的随机字符串
  const salt = await bcrypt.genSalt(10);
  //对密码进行加密
  //1. 要进行加密的原文
  //2. 随机字符串
  //返回值是加密后的密码
  const result = await bcrypt.hash('1234', salt);
  console.log(result);
}

run();

使用node hash.js即可看到加密后的结果。

在项目中使用 bcrypt

将明文密码加密

释放user中初始化用户的代码,并将其改为以下形式

//导入bcrypt
const bcrypt = require('bcrypt');


async function createUser () {
  try{
    const salt = await bcrypt.genSalt(10);
    const pass = await bcrypt.hash('1', salt);
    const user = await User.create({
      username: 'username1',
      email: 'username1@qq.com',
      password: pass,
      role: 'admin',
      state: 0
    })
  } catch(e) {}
}
createUser();

此时由于user.js已经被admin.js引入,刷新compass可以发现已经插入如下数据:
在这里插入图片描述

密码比对

将createUser();注释掉
在admin.js中修改代码,将客户端传过来的代码加密后,再和数据库中的代码进行比对。
同样要引入bcrypt
再将比对代码改为

  try{
      //true 比对成功
      //flase 比对失败
      let user = await User.findOne({email});
      //查询到了用户
      if( user ) {
          let isValid = await bcrypt.compare(password, user.password);
          
          //如果密码比对成功
          if(isValid) {
            //登录成功
            res.send('登陆成功')
          } else {
            //登陆失败
            res.status(400).render('admin/error', {msg: '邮箱地址或者密码错误'});
          }
      } else {
        //没有查询到用户
        res.status(400).render('admin/error', {msg: '邮箱地址或者密码错误'});
      }
  }catch(e){
        console.log(e)
  }

输入正确的邮箱和密码,可以看到登录成功界面
在这里插入图片描述

保持登录状态

cookie和session

此时其实不能算登录成功,
可以在登录后把用户名存储在req这个请求对象当中,然后再在浏览器中访问用户列表页面,在用户列表页面中从请求对象中获取用户名,将用户名显示在页面当中。如果用户名可以显示,则成功;不能显示,则失败。

admin.post('/login', async (req, res) => {...}

中添加

req.username = user.username;

req.username 即往req中添加username属性,它的值是查询出来的用户名。

admin.get('/user', (req, res) => {...}

中修改render方法

   res.render('admin/user',{ msg: req.username });

再找到views/admin/user.art,把msg显示在里面。

此时,正确登录后,访问localhost/admin/user
显示
在这里插入图片描述
说明事实上登录没效果,服务器端还是不认识客户端,
因为网站应用基于http协议,是基于请求和响应模型的应用,这种应用在完成了一次客户端和服务端的请求和响应后,客户端和服务器端就断开了,服务器端并不在意客户端是谁。这个特点被称为http协议的无状态性。

但是事实上平常登录的网站,在客户端发送请求后,事实上服务器端是可以认出它的。这就要建立客户端和服务器端的关联关系,建立这种联系,需要cookie和session技术。

cookie: 烤鸭店给客人的卡片
session: 烤鸭店的客人记录本,存储用户的身份

cookie: 浏览器在电脑硬盘中开辟的一块存储数据的控件(客户端js可以写,服务器端也可以写),主要供服务器端存储数据。
cookie的数据以域名的形式进行区分;有过期时间,超过时间的数据会被浏览器自动删除;cookie中的数据会随着请求自动发送到服务器(可以Network中查看)。

在客户端按f12,查看Application,侧边栏有个cookies这样的选项,选项中就存放在网站在客户端存储的数据。

当第一次访问网站时,这些cookie并不存在,在服务器响应了请求以后,服务器才给了客户端存储了这些数据。

在Network里可以看到Request Headers请求头信息,里面会有Cookie选项,里面的值就是之前在侧边栏cookies里看到的值了,只是这里是按字符串的形式拼接起来,然后一起发送到了服务器端。

Session就是一个对象,存储在服务器端的内存中,在Session对象中也可以存储很多条数据,每一条数据都有一个SessionId作为唯一标记。

cookie和session的关系和应用:

  1. 客户端首次发送邮件地址和密码时,服务器端会验证客户端的请求参数
  2. 如果通过验证,服务端生成一个SessionId,并把SessionId存储在客户端的cookie中
  3. 客户端之后向服务器端发送请求时,请求中携带着cookie。
  4. 服务器端接收客户端的请求,并获取cookie中的sessionId,与存储在服务器端的sessionId进行比对,如果相同,则说明用户登陆过了,那么服务器端就可以响应只有用户登陆后才能获得的数据。

在项目中使用cookie和session

在nodejs中要借助第三方模块express-session实现session功能。这个模块也是由express官方提供的,是express的中间件函数。

const session = require('express-session');
app.use(session({ secret: 'secret key'}));

引入express-session模块,模块会返回一个方法,我们用session变量去接收。调用这个方法,就可以在服务器端创建session对象了。

接下来使用app.use(中间件)拦截所有的请求,并将请求交给session()方法去处理。所以session()放在了app.use()方法中。
session()方法中,为请求对象下面添加了一个属性,属性的名字是session,而session属性的值是一个对象,这个对象可以在用户登录成功后保存用户信息。方法会在我们在session对象存储数据时,生成sessionId,这个sessionId时当前存储的数据的唯一标识。然后将sessionId存储在客户端的cookie当中,当客户端再一次访问服务器端的时候,方法会拿到客户端传递过来的cookie,并从cookie中提取sessionId,根据sessionId从cookie对象中找到用户信息,此时服务器就知道了访问服务器端的客户端是谁,也就真正实现了客户端和服务器端的联系,从而真正实现了登录功能。

在调用session()方法时,给session()方法传入了一个参数,参数的名字叫secret,意为存储一个密钥,这个密钥的值是可以自定义的,用来加密cookie信息。当我们在客户端存储数据时,需要对数据进行加密,服务器接收到cookie时,需要使用这个密钥进行解密,而客户端是不知道这个密钥的。这样做的好处是,客户端虽然可以查看到cookie的信息,但是查看到的就是一堆加密的字符串,并不知道它究竟是什么,从而提高数据的安全性。

使用npm install express-session安装模块

在app.js中,引入这个模块,使用上方的代码。注意,要把session设置的中间件放在路由控制器之前

在admin.js中,在用户登录成功后,要把用户信息存储到session当中。
把原先的

req.username = user.username;

改为

req.session.username = user.username;

这个session之前是没有的,是express-session后来添加的。当在session对象中存储了一些数据,session方法会在内部为当前用户生成唯一的sessionId,并且把sessionId存储在客户端的cookie当中。

此时,登录成功后查看cookie,多了一个数据
在这里插入图片描述

其中connect.sid是express-session设置的默认名字,它所对应的值是一个加密的字符串,这个字符串保存了服务器端为客户端生成的唯一的sessionId。接下来再往服务器端发送请求时,cookie就会被自动携带了,服务器端接收到cookie后,用session对象中的sessionId查找用户信息,如果查找到了,说明用户的登录时成功的。

把admin.js中,把

admin.get('/user', (req, res) => {...})

中的

res.render('admin/user',{ msg: req.username } );

修改为

res.render('admin/user',{ msg: req.session.username } );

此时需要重新登录一下,因为在修改保存代码后,nodemon会重新启动网站服务器。而session有这样一个特点,当服务器重启时,服务器端的session就会失效。所以要重新登录,重新在服务器端生成session信息。

登录之后,再次进入localhost/admin/user,可以看到
在这里插入图片描述
说明登录功能真正实现了。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!