web前端 | 博客(二)登录功能
实现登录功能
- 创建用户集合,初始化用户
- 连接数据库
- 创建用户集合
- 初始化用户
- 为登录表单项设置请求地址,请求方式(GET方法会将参数放到地址栏中,不隐蔽,要用POST方法,它将参数放到消息体中,比较隐蔽)以及表单name属性
- 当用户点击登录按钮时,客户端验证用户是否填写了登录表单
- 如果其中一项没有输入,则阻止表单提交
- 服务器端请求接收参数,验证用户是否填写了登录表单(有时候客户端的js代码会被禁用,无法正确识别表单的准确性,故服务端的表单验证必不可少)
- 如果有一项没有输入,为客户端做出响应,阻止程序向下执行(例如,如果没有填写邮箱,则找不到该用户)(无论邮箱地址错误还是密码错误,一律提示两者都错,防止用户恶意猜出其他用户的账号密码)
- 根据邮箱地址查询用户信息
- 如果用户不存在,为客户端做出响应
- 如果用户存在,将用户名和密码进行比对
- 比对成功,则用户登录成功
- 比对失败,则则用户登录失败
数据库
数据库连接
在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的关系和应用:
- 客户端首次发送邮件地址和密码时,服务器端会验证客户端的请求参数
- 如果通过验证,服务端生成一个SessionId,并把SessionId存储在客户端的cookie中
- 客户端之后向服务器端发送请求时,请求中携带着cookie。
- 服务器端接收客户端的请求,并获取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,可以看到
说明登录功能真正实现了。
web前端 | 博客(二)登录功能相关推荐
- 博客项目——登录功能实现
客户端(login.art) 登录功能实现,表单要添加name属性,这样才能向服务器提交数据 <div class="login-body"><div class ...
- 多人博客管理系统登录功能
多人博客管理系统 目标 能够知道搭建项目环境的步骤 能够理解模板优化 熟悉登录功能的思路逻辑 知道密码为什么需要加密 知道cookie跟session是什么 能够参照笔记写出登录功能 功能需求 博客内 ...
- web前端 | 博客(八)用户信息修改功能
用户信息修改功能 当点击用户后面的按钮时,要跳转到用户信息修改页面.而修改和添加实际上是同一个页面. 要区分跳转后是添加操作还是修改操作,在于携带的参数. 如果是添加操作,那就直接跳转过去:如果是修改 ...
- 瞧一瞧看一看:新手小白写Web前端博客
二序 2.1 表格标签 2.1.1 <table> : 表格的最外面容器<tr> : 定义表格行<th> : 定义表头<td> : 定义表格标题注:之前 ...
- PHP系统开发/Web文章博客
PHP前后端交互 | web文章博客 前言 环境部署 一.登录 二.注册 三.主页 四.详情 五.编辑 六.删除 七.注销 八.发表 全部文件 总结 前言 一.项目需求: 做个基础的页面,文章博客we ...
- 古文字识别助手与众包平台——项目博客二
古文字识别助手与众包平台--项目博客二 背景: 由于众包算法的系统是为了让更多的人通过描绘图像而获取更多的原始数据,所以在手机端的功能流程不能做的太复杂,否则用户会直接被过于复杂的流程劝退,于是,经过 ...
- Node.js 从零开发web server博客项目--项目初始化
本篇博客记录了<Node.js 从零开发web server博客项目>的原生开发系列内容. 开篇主要介绍原生项目的搭建,以及初步的项目结构设计. 一.项目初始化 新建项目目录,并进入到项目 ...
- 项目分享:模拟博客园登录
项目二:模拟博客园登录 声明: 项目代码纯粹本人自己编写,无任何抄袭.转载等情况,所以写的很low,仅供大家参考,有不懂的随时评论留言 项目要求: 首先程序启动,显示下面内容供用户选择: 请登录 请注 ...
- 推荐 14 个 GitHub 上优质的原创前端博客文章仓库
大家好,我是你们的 猫哥,那个不喜欢吃鱼.又不喜欢喵 的超级猫 ~ 博客 下面的顺序是随机的,不分先后. SHERlocked93/blog 公众号:前端下午茶 作者:SHERlocked93 作者微 ...
最新文章
- 设计模式详解(总纲)
- Shell中判断字符串是否为数字的6种方法分享
- 因开源协议“大打出手”,AWS 宣布创建 Elasticsearch、Kibana 分支
- 蓝桥杯2019年第十届C/C++省赛B组第二题-年号字串
- JVM——类加载机制(二)
- php根据ajax传值跳转页面_vue中动态路由的跳转(name | path) 前进后退 replace...
- 阻击 瑞星 和 雅虎助手 的 SVOHOST.exe(第2版)
- 最全支付系统设计包含:账户,对账,风控...
- 网络口碑推广主要目的全知道
- 解决外接显示屏后CPU占用率过高问题
- AI绘图之基于文本/图片制图
- html页面上不断掉星星,html 页面的星星闪烁 特效 背景 (js案例 )
- Java学习之:如何将 java 程序打包成 .jar 文件
- Rational Rose的讲解
- 自制PMW3901光流模块
- 苹果开发者账号可以创建多少测试证书_苹果开发者账号对应生成的证书都有哪些...
- 学习-格鲁夫给经理人的第一课
- 破解周鸿祎的战术精要---转自月光宝盒
- C# 表达式与运算符
- 低功耗蓝牙通讯 C# WinForm