一、项目介绍
开发环境:Nodejs平台+express框架+mysql数据库搭建服务端平台
功能:实现对英雄的增删改查,系统的注册、登录、会话状态保持等功能
效果图:
项目结构
大纲:
二、技术分析
- express搭建服务器
- 文件上传
- 验证码功能
- md5登录注册加密
- cookie用户会话保持
三、项目流程
- 1.express框架搭建服务器入口文件
- 1.导入模块
- 2.创建服务器
- 3.配置中间件
- 托管静态资源
- body-parser:解析post请求参数
- express-fileupload:文件上传
- mysql-ithm:数据库操作
- cookie-session中间件:用户会话保持
- 4.开启服务器
- 3.设计路由(接口文档)
- 4.处理
- 请求:获取请求参数
- 处理:增删改查数据库
- 响应:将数据库操作结果响应给客户端
express搭建服务器
//1.导入模块
const express = require('express');
//2.创建服务器
let app = express()
//3.配置中间件
//3.1 托管静态资源
app.use(express.static('www'));//静态网页
app.use(express.static('static'));//英雄图像
//3.2 body-parser:解析body
var bodyParser = require('body-parser');
// parse application/x-www-form-urlencoded
//以后所有的req都会有一个body属性,就是解析好的post参数对象
app.use(bodyParser.urlencoded({
extended: false }));
//3.3 express-fileupload:接收文件数据
const fileUpload = require('express-fileupload');
app.use(fileUpload());
//3.4 mysql-ithm数据库操作
//(1)导包
const hm = require('mysql-ithm');
//(2)连接数据库
hm.connect({
host: 'localhost',//数据库地址
port: '3306',
user: 'root',//用户名,没有可不填
password: 'root',//密码,没有可不填
database: 'cqmanager'//数据库名称
});
//(3)创建Model(表格模型:负责增删改查)
//英雄表格
let heroModel = hm.model('heros', {
name: String,
skill: String,
icon: String,
});
//4.设计路由(接口文档)
//(1)查询英雄列表
app.get('/hero/list', (req, res) => {
});
//(2)查询英雄详情
app.get('/hero/info', (req, res) => {
});
//(3)编辑英雄
app.post('/hero/update', (req, res) => {
});
//(4)删除英雄
app.post('/hero/delete', (req, res) => {
});
//(5)新增英雄
app.post('/hero/add', (req, res) => {
});
//(6)验证码
app.get('/captcha', (req, res) => {
});
//(7)注册
app.post('/user/register', (req, res) => {
});
//(8)登录
app.post('/user/login', (req, res) => {
});
//(9)退出登录
app.get('/logout', (req, res) => {
});
//5.开启服务器
app.listen(3000, () => {
console.log('success');
});
四、难点与注意点
- 服务端英雄查询注意点
根据英雄名字查询数据库 需要使用sql的模糊查询
- name like “%名字%” : name包含名字的
- 不能使用 name = 名字 : 查询具体名字
- str =
name like "%${search}%"
;
//(1)查询英雄列表
app.get('/hero/list', (req, res) => {
//1.请求 : 获取参数search
let search = req.query.search;
//2.处理:查询数据库
let str = '';
if (search) {
//如果客户端传了search,则使用mysql模糊查询
str = `name like "%${
search}%"`;
} else {
//如果客户端没有传search,则查询hero表格所有字段
str = ['name', 'skill', 'icon', 'id'];
};
heroModel.find(str, (err, results) => {
//3.响应
if (err) {
res.send({
code: 500,
msg: err
});
} else {
res.send({
code: 200,
heros: results,
});
}
});
});
- 客户端代码:index.html
注意点
- 编辑按钮 :使用
window.location.href
给编辑页面传参id - 删除按钮 :使用自定义属性 data-id 存储英雄id
- 服务端查询英雄详情
- 数据库操作结果results一定是一个数组,如果想要返回客户端一个英雄对象,则需要取下标0
//(2)查询英雄详情
app.get('/hero/info', (req, res) => {
//1.请求 : 获取参数id
let id = req.query.id;
//2.处理:查询数据库
heroModel.find(`id=${
id}`, (err, results) => {
//3.响应
if (err) {
res.send({
code: 500,
msg: err
});
} else {
res.send({
code: 200,
data: results[0]
});
}
});
});
- 服务端编辑功能
注意点:
- 1.接收文件需要使用express-fileupload插件
- 2.数据库只存储图片的路径字符串,具体的图片文件存储在static/imgs文件夹中
- 3.图片文件名 使用 英雄名字.png 格式存储
//5.4 编辑英雄
app.post('/hero/update', (req, res) => {
//1.请求
//1.1 文本
let {
id, name, skill } = req.body;
//1.2 文件
let icon = req.files ? req.files.icon : null;
console.log(id, name, skill, icon);
//2.处理
//2.1 处理图片
if (icon) {
icon.mv(`${
__dirname}/static/imgs/${
name}.png`, (err) => {
if (err) {
res.send({
code: 500,
msg: '修改失败'
});
};
});
};
//2.2 处理文本
let obj;
if (icon) {
obj = {
name,
id,
skill,
icon: `http://127.0.0.1:3000/imgs/${
name}.png`
};
} else {
obj = {
name,
id,
skill,
}
};
heroModel.update(`id=${
id}`, obj, (err) => {
if (err) {
res.send({
code: 500,
msg: '修改失败'
});
} else {
res.send({
code: 200,
msg: '修改成功'
});
}
})
// if( icon ){//有文件
// //2.1 文件:写入文件夹
// icon.mv(`${__dirname}/static/imgs/${name}.png`,(err)=>{
// if(err){
// res.send({
// code:500,
// msg:'修改失败'
// });
// };
// });
// //2.2 文本: 写入数据库 图片:数据库存对应的网址
// heroModel.update(`id=${id}`,{
// name,
// id,
// skill,
// icon:`http://127.0.0.1:3000/imgs/${name}.png`
// },(err)=>{
// if(err){
// res.send({
// code:500,
// msg:'修改失败'
// });
// }else{
// res.send({
// code:200,
// msg:'修改成功'
// });
// }
// })
// }else{//没有文件
// heroModel.update(`id=${id}`,{
// name,
// id,
// skill
// },(err)=>{
// if(err){
// res.send({
// code:500,
// msg:'修改失败'
// });
// }else{
// res.send({
// code:200,
// msg:'修改成功'
// });
// }
// })
// }
});
- 验证码功能
验证码功能思路
- 1.服务端生成一个二进制验证码图片文件与对应的验证码文本
- 2.服务端声明全局变量存储验证码文本(用于客户验证)
- 3.客户端img标签发起网络请求,服务端响应验证码图片
- 4.客户端提交注册数据,服务端处理
服务端代码
//(6)验证码
//声明全局变量存储验证码问题 (用于客户端验证)
let captchaTxt = '';
app.get('/captcha', (req, res) => {
//1.创建验证码对象
var captcha = svgCaptcha.create();
//2.获取验证码文本并保存
captchaTxt = captcha.text;
console.log(captcha.text);
//3.将验证码图片响应给客户端
res.type('svg');
res.status(200).send(captcha.data);
});
客户端register.html
- img标签src属性请求图片,服务器响应之后img标签会自动加载图片
<img class="code" src="http://127.0.0.1:3000/captcha" alt="">
验证码不要直接比较,应该将服务器生成的验证码与用户上传的code全部转为大写或小写
-
验证码一般不区分大小写
-
验证码图片不能用ajax的,因为ajax请求json数据
- 核心原理:重新设置一下img标签的src属性
注意点: 浏览器有一个图片缓存机制。 如果是一样的请求,服务器只会在第一次请求的时候去获取图片。之后同一张图片请求会被浏览器忽略
解决方案 : 添加一个随机参数,告诉浏览器这是不一样的请求就可以了。
- 核心原理:重新设置一下img标签的src属性
$('.code').click(function () {
console.log('1111');
//验证码图片不能用ajax的,因为ajax请求json数据
$(this).attr('src',`http://127.0.0.1:3000/captcha?id=${
Math.random()}`);
});
//(7)注册
app.post('/user/register', (req, res) => {
//1.获取post请求参数
let body = req.body;
console.log(body);
//2.处理
// code: 200 成功 401:用户已注册 402:验证码错误 500:服务器内部错误
if (body.code.toLowerCase() != captchaTxt.toLowerCase()) {
//全部转小写,不区分大小写
//验证码错误
res.send({
code: 402,
msg: '验证码错误'
});
} else {
//检查是否已经注册
userModel.find(`username="${
body.username}"`, (err, results) => {
if (err) {
res.send({
code: 500,
msg: '注册失败'
});
} else if (results.length != 0) {
res.send({
code: 401,
msg: '用户已存在'
});
} else {
//如果没有注册,则添加到数据库
userModel.insert({
username: body.username,
password: body.password
}, (err, results) => {
if (err) {
res.send({
code: 500,
msg: '注册失败'
});
} else {
res.send({
code: 200,
msg: 'success'
});
}
})
}
});
}
});
- 会话保持
// 只要在响应头中加入 Set-Cookie 字段,客户端会把这个数据放到一个文件中然后保存到客户端电脑上
//服务器响应头添加cookie发给浏览器
res.writeHead(200, {
'Content-Type': 'text/plain; charset=utf-8',
"Set-Cookie": 'userid=123456'
})
- 退出登录重定向
重定向技术实现退出登录流程
- (1)客户端发送退出登录请求 :
http://127.0.0.1:3000/logout
- (2)服务器接收请求
- a. 清空该用户cookie
- b. 重定向刷新首页
html代码
<li><button class="btn btn-danger btn-exit" id="logout" onclick="location.href='/logout'">退出</button></li>
服务端
//(9)退出登录
app.get('/logout', (req, res) => {
//1.清空session
req.session = null;
//2.重定向显示首页
res.writeHead(302, {
'Location': './index.html'
});
res.end();
});
五、总结
验证码技术
svg-captcha第三方模块(文档传送门)
安装
npm install --save svg-captcha
用法
const svgCaptcha = require('svg-captcha');
const captcha = svgCaptcha.create();
console.log(captcha);
// {data: '<svg.../svg>', text: 'abcd'}
在express使用
const svgCaptcha = require('svg-captcha');
app.get('/captcha', function (req, res) {
var captcha = svgCaptcha.create();
req.session.captcha = captcha.text;
res.type('svg');
res.status(200).send(captcha.data);
});
- 工作流程
md5 加密 加盐
数据加密思路
- 1.客户端点击提交的时候对密码进行md5加密(使用前端第三方包 md5.min.js)
- 2.服务端接收到密文保存到数据库
- 密码明文只存在于用户填写的表单input中(不要在代码里面去打印用户明文,避免泄露)
- 无论是网络传输还是服务器都只存储密文
- 防止http请求被攻击导致密码泄露
- 防止数据库被攻击导致密码泄露
- 下一次用户登录的时候,使用相同加密方式对登录密码进行加密。然后服务端只匹配两个密文是否一致
导包
<!-- 导入md5 -->
<script src="./libs/md5.min.js"></script>
使用
- 参数1:要加密的明文
- 参数2:盐 作用:提高密文复杂度 同样的数据,盐不同得到的密文不同
- 返回值:加密好的密文
let mima = md5( $('#password').val() ,'add salt');
console.log(mima);
- 注意点
前端不会直接去获取用户的密文,不安全。
1)获取输入的数据
(2)加密
(3)直接赋值给表单
$('#password').val( md5( $('#password').val() ,'add salt') );
登录时先转成密文
$('#password').val( md5( $('#password').val() ,'add salt') );
cookie会话保持
- cookie的定义:Cookie,有时也用其复数形式 Cookies,指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密)
- cookie的状态管理
- HTTP 是无状态协议,它不对之前发生过的请求和响应的状态进行管理。也就是说,无法根据之前的状态进行本次的请求处理。
- 任何浏览器都可以访问服务器,服务器根本不知道到底是哪一个用户访问自己
- 当一个用户第一次访问浏览器时,服务器会生成一个cookie,并且在响应头中添加cookie后返回给浏览器
- 浏览器会自动将cookie保存在下次,当下一次访问同一服务器时会自动将cookie放入请求头中发给服务器,这样服务器就可以识别用户
- (1)每一个服务器都有自己的cookie(cookie的域名识别)
- (2)cookie是由服务器生成的
- 虽然浏览器本身也可以添加cookie,但是保持用户状态的cookie一定是由服务器生成的(seesion)
- (3)浏览器访问同一域名的请求时会自动将cookie发给服务器,我们开发人员无需编写任何代码
- 如果有则发送,没有则不发
- cookie是在请求头中的
- cookie与token区别
cookie实现用户会话保持
文档
注意:不能使用跨域
安装
npm install cookie-session
express配置中间件
//4.4 cookie-session中间件 : 用户会话状态保持 (给req添加session属性)
const cookieSession = require('cookie-session');
app.use(cookieSession({
name: '59qi',
keys: [/* secret keys */'a','b'],//设置加密的钥匙
// Cookie Options
maxAge: 7 * 24 * 60 * 60 * 1000 // 7天 设置有效时间
}));
加入需要cookie的响应中
res.send({
code: 200,
heros: results,
users:req.session.users
});