会话控制
会话控制
以下为学习过程中的极简提炼笔记,以供重温巩固学习
学习准备
准备工作
学习目的
Session control 会话控制
会话控制是什么
所谓会话控制就是
对会话进行控制
- 控制浏览器客户端与服务器会话的访问者、操作权限
HTTP 是一种无状态的协议,它没有办法区分多次的请求是否来自于同一个客户端,
无法区分用户
,而产品中又大量存在的区分用户的需求,所以我们需要通过会话控制
来解决该问题
常见的会话控制技术有三种:
- cookie
- session
- token
cookie
cookie 是什么
- cookie 是 HTTP 服务器发送到用户浏览器并保存在浏览器端本地的一小块数据
- cookie 是保存在浏览器端本地的一小块数据
- cookie 是按照域名划分保存的
- cookie 本质是 key value 结构的键名键值对
简单示例:
域名 | cookie |
---|---|
www.baidu.com | a=100; b=200 |
www.bilibili.com | xid=1020abce121; hm=112411213 |
jd.com | x=100; ocw=12414cce |
cookie 的特点
浏览器向服务器发送请求时,会自动将
当前域名下
可用的 cookie 携带,设置在请求报文当中的请求头里,然后传递给服务器,让服务端获取客户端浏览器的信息这个请求头的名字也叫
cookie
,所以将cookie 理解为一个 HTTP 的请求头也是可以的
cookie 的运行流程
cookie 的运行流程:
- 填写账号和密码校验身份,校验通过后下发 cookie
- 在客户端浏览器上,填写身份校验信息,给到服务器后,服务器返回cookie作为凭证
- 服务器通过响应报文传递cookie
- 在响应报文中返回
set-cookie:响应头
,在其中设置cookie内容
- 在响应报文中返回
- 浏览器接收并解析响应报文
- 解析到set-cookie:响应头,就会将set-cookie中的内容作本地存储,保存在当前域名的cookie下
- 有了 cookie 之后,后续向服务器发送请求时,就会自动携带对应域名的 cookie 信息
- 服务器通过解析 cookie 信息,得知请求的发送者
浏览器操作 cookie
浏览器操作 cookie 的操作,使用相对较少,大家了解即可
- 禁用所有 cookie
- 删除 cookie
- 删除后,再次访问需要cookie信息的站点,服务器收不到浏览器的cookie标识,涉及账号部分的页面,跳转登录页
- 查看 cookie
路径:
- 管理和删除 cookie 和站点数据 edge://settings/content/cookies
- 查看所有 Cookie 和站点数据 edge://settings/content/cookies/siteData
备注:
- 不同的浏览器之间,cookie数据是不共享的
- 可通过浏览器插件管理 cookie
- 可通过浏览器插件管理 浏览器设备标识、系统标识(某些恶心网站,会根据浏览器标识、设备标识、系统标识,展示不一样的内容)
- 二次访问网站时,理论上是携带当前网站服务器前次返回的set-cookie信息作二次访问,但由于网站页面有css和js是引用自其他网站的,这种cooike就是第三方cookie,访问标的网站的cookie称为第一方cookie
cookie 的代码操作
设置cookie的场景:
- 用户登录成功后
- 需要作客户端浏览器标识/设备标识/系统标识/用户身份校验的场景,在校验结束后设置
删除cookie的场景:
- 用户退出登录
在express框架中设置cookie步骤:
- 更改路由规则,如
app.get('/set-cookie',回调函数
,访问该路由规则时,触发设置cookie - 在回调函数中,通过
res.cookie(第一个参数cookie的键名,第二个参数cookie的键值)
返回cookie- 客户端浏览器,首次通过路由规则访问网站时,触发服务器返回cookie,在返回的响应报文响应头中就会有set-cookie响应头
- 客户端浏览器,二次通过路由规则访问网站时,就会在请求报文中携带上cookie了
- 此为默认设置方式,会在浏览器关闭的时候,销毁cookie(即cookie仅在对话活动状态下保存)
- 在回调函数中,通过
res.cookie(第一个参数cookie的键名,第二个参数cookie的键值,第三个对象参数{设置声明周期maxAge: 60 * 1000})
第三个参数选项,可以设置cookie的生命周期- 路由规则中设置的maxAge的值是数字,单位是毫秒,如 maxAge: 60 * 1000 为一分钟
- 报文中的maxAge显示的是秒
- 设置了cookie的生命周期时间后,cookie不受浏览器关闭/对话关闭影响
- 更改路由规则,如
在express框架中删除cookie步骤:
- 更改路由规则,如
app.get('/remove-cookie',回调函数
,访问该路由规则时,触发删除cookie - 在回调函数中,通过调用方法
res.clearCookie('传参需要删除的cookie键名字符串如name')
- cookie删除后的响应报文,对应删除的cookie键值在删除后为空,过期时间是1970年
- 更改路由规则,如
在express框架中读取cookie步骤:
- 安装工具包
npm i cookie-parser
- 导包/导入该中间件
const cookieParser = require('cookie-parser')
- 设置中间件
app.use(cookieParser());
- 更改路由规则,如
app.get('/remove-cookie',回调函数
访问该路由规则时,触发获取cookie - 在回调函数中,通过 req.cookies 获取到cookie
- 安装工具包
通过原生express,设置cookie的案例,cookie.js
//导入 express
const express = require('express');
const cookieParser = require('cookie-parser')
//创建应用对象
const app = express();
app.use(cookieParser());
//创建路由规则
app.get('/set-cookie', (req, res) => {
// 设置 cookie
// res.cookie('name', 'zhangsan'); // 会在浏览器关闭的时候, 销毁
res.cookie('name', 'lisi', { maxAge: 60 * 1000 }) // max 最大 age 年龄
res.cookie('theme', 'blue');
res.send('home');
});
//删除 cookie
app.get('/remove-cookie', (req, res) => {
//调用方法
res.clearCookie('name');
res.send('删除成功~~');
});
//获取 cookie
app.get('/get-cookie', (req, res) => {
//获取 cookie
console.log(req.cookies);
res.send(`欢迎您 ${req.cookies.name}`);
})
//启动服务
app.listen(3000);
- express 中可以使用
cookie-parser
进行处理
const express = require('express');
//1. 安装 cookie-parser npm i cookie-parser
//2. 引入 cookieParser 包
const cookieParser = require('cookie-parser');
const app = express();
//3. 设置 cookieParser 中间件
app.use(cookieParser());
//4-1 设置 cookie
app.get('/set-cookie', (request, response) => {
// 不带时效性
response.cookie('username', 'wangwu');
// 带时效性
response.cookie('email', '23123456@qq.com', { maxAge: 5 * 60 * 1000 });
//响应
response.send('Cookie的设置');
});
//4-2 读取 cookie
app.get('/get-cookie', (request, response) => {
//读取 cookie
console.log(request.cookies);
//响应体
response.send('Cookie的读取');
});
//4-3 删除cookie
app.get('/delete-cookie', (request, response) => {
//删除
response.clearCookie('username');
//响应
response.send('cookie 的清除');
});
//4. 启动服务
app.listen(3000, () => {
console.log('服务已经启动....');
});
不同浏览器中的 cookie 是相互独立的,不共享
session
session 是什么
- session 是保存在
服务器端的一块儿数据
,保存当前访问用户的相关信息- 用户名
- 邮箱
- id
- 等等...
session 的作用
- 实现会话控制,可以识别访问者的身份,快速获取当前用户的相关信息
- 验证码
- 权限
- 等等...
session 运行流程
- 用户在浏览器,填写账号和密码,发送给服务端校验身份
- 服务端获取用户请求,与后台服务器数据作比对校验,校验通过后创建
session
信息- 服务端会为当前访问者,创建一个对象{},也称作
session对象
- 会在session对象中,保存访问者的基本信息,如用户名、id、邮箱等信息
- 除以上访问者的基本信息外,服务端还会在对象中,生成独一无二的id值,也称作
session_id
,即会话id
- 服务器中会建档,存储各个用户/客户端浏览器的请求,包括
session对象
和session_id
- session对象中包含了该用户的信息,用户名、邮箱、id、权限、等等
- 服务端会为当前访问者,创建一个对象{},也称作
- 服务端最后将
session_id
的值,以响应cookie的形式
,通过响应头,返回给浏览器- 有了 cookie,后续浏览器向服务端发送请求时,响应头会自动携带 cookie 信息
- 服务器通过
cookie
响应头中的session对象中的session_id
的值,到服务器的session_id存放池匹配,确定用户的身份
会话对象(例如,使用 Express.js 中的 req.session)既不在响应头也不在响应体中。它存储在服务器内存中,并且与特定客户端请求相关联。
当客户端首次向服务器发送请求时,服务器会创建一个新的会话对象并将其存储在内存中。该会话对象包含有关客户端会话的信息,例如已登录的用户、购物车中的项目等。
在随后的请求中,服务器会使用客户端发送的会话 ID 来查找与该客户端关联的会话对象。这使得服务器可以跟踪客户端在会话期间所做的更改和偏好。
会话对象通常不直接包含在 HTTP 响应中。但是,服务器可以在响应头中设置一个名为 Set-Cookie 的特殊头,其中包含会话 ID。这允许客户端在后续请求中将会话 ID 发送回服务器,以便服务器可以找到与该客户端关联的会话对象。
因此,会话对象既不在响应头也不在响应体中。它存储在服务器内存中,并且与特定客户端请求相关联,可以通过 Set-Cookie 响应头在客户端和服务器之间传递。
express-session 中间件配置
- express中的 session中间件配置:
express-session
中间件- express 框架中,可以使用这个
express-session
中间件,对 session 进行操作(存入内存) - 还需要另一个包:
connect-mongo
,此包用于连接mongoDB数据库以及操作数据库,借助此包,可以将session信息存入数据库中
- express 框架中,可以使用这个
session 的代码操作步骤:
- 在app.js中,
app.use(中间件函数({接收对象类型参数}))
app.use用于设置中间件 - session函数接收对象类型参数,返回一个函数
- 对象类型参数及其含义:
- name设置响应cookie的name,默认值是:connect.sid
- secret设置密钥,参与加密的字符串(又称签名、加盐)
- saveUninitialized是否为每一个请求都自动创建一个session对象(false:不用session就不创建,true:不用session也创建,多用于对匿名用户作信息记录);是否为每次请求都设置一个cookie用来存储session的id
- resave是否在每次请求时重新保存session;
- 由于session也是有生命周期的,会存在过期,存在过期时间限制,超时销毁
- 当设置为true时,用户在客户端与服务端建立连接并操作的期间,服务器收到有请求,就会对会话内容(即session内容)作重新保存,更新session的保存时间;
- 用户的感知就是:登录期间一直有操作(有向服务端发请求)---不会退出登录;多少分钟用户没有操作后---服务器判断会话超时,标记为退出登录
- store设置mongodb数据库的连接配置,将数据存入路径地址下的数据表
- cookie设置响应cookie的特性
- 设置服务端在用户校验通过后所返回的cookie的特性
- httpOnly: true 设置为true开启后,浏览器/前端无法通过 JS 操作,即无法通过document.cookie对cookie作访问,限制为只能在http传输时访问
- maxAge: 1000 * 300 设置返回cookie的生命周期,同时也是session生命周期的设置,也控制后端,控制 sessionID 的过期时间;确保前端cookie和后端session同时过期,确保一致性
session 的代码操作
背景操作:
- 先跑起mongoDB服务端,提供可以存储的数据库环境
- 在navicat中,可以观察到,在跑起express-session中间件后,数据库中多了一张空的session表/集合
需求:
- 触发登录路由规则时,以get请求类型,向/login发送请求时,要求传递两个查询字符串参数:username=admin&password=admin,需要传对了才设置
步骤:
- 路由规则回调函数中,通过req.session.xxx,设置 session 信息,将信息存入
- 通过req.session.destroy方法销毁session
测试:
- 不传参的情况下访问/login,返回登陆失败
- 直接请求/login,校验不过,服务端没有写session,没有session_id返回
- url中传参+校验通过的情况下访问/login,返回登陆成功
- 返回的请求头中,带有set-cookie,其中包含session_id
- 此时在服务端数据库中,session表/集合中也有数据了,包括session_id,以及用户的session信息
- 服务端能从session表/集合中,辨别/确认请求是由谁发起
- 检测 session 是否存在用户数据,确认用户是否已登录
- 由于使用了connect-mongo中间件,数据已经从数据库取出,并存到req.session属性上
- 通过if (req.session.username) 检测session 是否存在用户数据
- 备注:拿到前端session要与后端数据库比较,不然被人随便注入之后就可以登录了
- 简单理解:等于是用户发送请求 程序已经自动去查询读取了 就看是否返回的是true 还是false 然后根据结果渲染页面就行了
- 不传参的情况下访问/login,返回登陆失败
备注:
- 浏览器的无痕模式/无痕窗口,就是没有任何的cookie
const express = require('express');
//1. 安装包 npm i express-session connect-mongo
//2. 引入 express-session connect-mongo
const session = require("express-session");
const MongoStore = require('connect-mongo');
const app = express();
//3. 设置 session 的中间件
app.use(session({
name: 'sid', //name设置响应cookie的name,默认值是:connect.sid
secret: 'atguigu', //secret设置密钥,参与加密的字符串(又称签名、加盐)
saveUninitialized: false, //saveUninitialized是否为每一个请求都自动创建一个session对象(false:不用session就不创建,true:不用session也创建,多用于对匿名用户作信息记录);是否为每次请求都设置一个cookie用来存储session的id
resave: true, //是否在每次请求时重新保存session
store: MongoStore.create({
mongoUrl: 'mongodb://127.0.0.1:27017/project' //数据库的连接配置
}),
cookie: {
httpOnly: true, // 开启后前端无法通过 JS 操作
maxAge: 1000 * 300 // 这一条 是控制 sessionID 的过期时间的!!!
},
}))
//创建 session;登录路由规则 session 的设置
app.get('/login', (req, res) => {
// username=admin&password=admin
if (req.query.username === 'admin' && req.query.password === 'admin') {
//设置 session 信息,将信息存入req.session.xxx
req.session.username = 'admin';
req.session.uid = '258aefccc';
//成功响应
res.send('登录成功');
// req.session.username = 'zhangsan';
// req.session.email = 'zhangsan@qq.com'
// res.send('登录成功');
} else {
res.send('登录失败~~');
}
})
//获取 session,session 的读取
app.get('/cart', (req, res) => {
console.log('session的信息');
console.log(req.session.username);
//检测 session 是否存在用户数据
if (req.session.username) {
res.send(`购物车页面, 欢迎您 ${req.session.username}`)
} else {
res.send('您还没有登录~~');
}
})
//销毁 session
app.get('/logout', (req, res) => {
//销毁session
// res.send('设置session');
req.session.destroy(() => {
res.send('成功退出');
});
});
app.listen(3000, () => {
console.log('服务已经启动, 端口 ' + 3000 + ' 监听中...');
});
//导入 express
const express = require('express');
//2. 引入 express-session connect-mongo
const session = require("express-session");
const MongoStore = require('connect-mongo');
//创建应用对象
const app = express();
//3. 设置 session 的中间件
app.use(session({
name: 'sid', //设置cookie的name,默认值是:connect.sid
secret: 'atguigu', //参与加密的字符串(又称签名) 加盐
saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的id
resave: true, //是否在每次请求时重新保存session 20 分钟 4:00 4:20
store: MongoStore.create({
mongoUrl: 'mongodb://127.0.0.1:27017/bilibili' //数据库的连接配置
}),
cookie: {
httpOnly: true, // 开启后前端无法通过 JS 操作
maxAge: 1000 * 60 * 5 // 这一条 是控制 sessionID 的过期时间的!!!
},
}))
//首页路由
app.get('/', (req, res) => {
res.send('home')
})
//登录 session 的设置
app.get('/login', (req, res) => {
// username=admin&password=admin
if (req.query.username === 'admin' && req.query.password === 'admin') {
//设置 session 信息
req.session.username = 'admin';
req.session.uid = '258aefccc';
//成功响应
res.send('登录成功');
} else {
res.send('登录失败~~');
}
})
//session 的读取
app.get('/cart', (req, res) => {
//检测 session 是否存在用户数据
if (req.session.username) {
res.send(`购物车页面, 欢迎您 ${req.session.username}`)
} else {
res.send('您还没有登录~~');
}
});
//session 的销毁
app.get('/logout', (req, res) => {
req.session.destroy(() => {
res.send('退出成功~~');
})
})
//启动服务
app.listen(3000);
总结 session 和 cookie 的区别
cookie 和 session 的区别主要有如下几点:
- 存在的位置
- cookie:浏览器端(由浏览器去维护cookie)
- session:服务端(数据在服务器端保存)
- 安全性
- cookie 是以明文的方式存放在客户端浏览器的,安全性相对较低
- session 存放于服务器中,所以安全性
相对
较好(没有绝对的安全,提及到安全,必然是相对性质)
- 网络传输量
- cookie 设置内容过多会增大报文体积, 会影响传输效率
- session 数据存储在服务器,只是通过 cookie 传递 id,所以不影响传输效率
- 存储限制
- 浏览器限制单个 cookie 保存的数据不能超过
4K
,且单个域名下的存储数量也有限制- chrome浏览器110版本的cookie下,单个域名下的存储数量约为165个
- session 数据存储在服务器中,所以没有这些限制
- 浏览器限制单个 cookie 保存的数据不能超过
实战案例-记账本-结合session功能
- 需求:对记账本的数据作保护,包括存储、查看、操作时的身份校验等
响应返回注册页面
需求:添加注册功能
背景:已通过bootstrap构建静态页,需要通过响应,返回注册页面
步骤:
- 先跑起mongoDB服务端,提供可以存储的数据库环境
- 再跑起后端express服务,
npm start
- 分析:
- 路由规则存储在
项目文件夹下的\routes
目录中,其中\routes\api
存放接口,\routes\web
存放与网页相关的路由规则 - 在
\routes\web
中加路由规则,在路由回调中返回html,回调函数中借助模板引擎res.render("页面路径")
以返回页面 - 在模板引擎目录下,创建模板文件写页面
项目文件夹下的\views\auth\login.ejs
- 在app.js中引入路由文件
const authRouter = require('./routes/web/auth');
即:项目文件夹下的\routes\web\auth.js,然后app.use('/', authRouter);
让路由规则生效
- 路由规则存储在
var express = require('express');
var router = express.Router();
//注册
router.get('/reg', (req, res) => {
//响应 HTML 内容
res.render('auth/reg');
});
module.exports = router;
注册用户功能实现
- 逻辑分析:
- 类似于在网站上注册用户账户的场景
- 具体逻辑流程分析:
- 客户端一侧,提供账号、密码、邮箱、手机号
- 服务器一侧,拿到用户数据,往数据库插入数据,返回登录页跳转给客户端
- 客户端在登录页填校验信息,返回给服务端
- 服务器校验客户端填写的校验信息,校验通过则写入登录状态,也就是将session写入到服务器,返回session_id给服务端
- 后续服务端访问网站时,将携带着session_id,以提供身份凭据供服务器识别
- 实现功能步骤:
- 打开注册页面
项目文件夹下的\views\auth\reg.ejs
,改造表单组件,写注册页表单的post
方法提交的信息 - 到
项目文件夹下的\routes\web\auth
中,加post路由规则- 通过
req.body
获取用户提交的信息 - 在处理用户数据时,应加上表单校验(此处略,参考此前的gpt方案)
- 在处理密码类型数据时,因为该数据类型比较敏感重要,大多数网站后台会将其加密
- 借助
单向不可逆加密算法MD5
实现加密,通过npm i md5
引入第三方包,使用md5加密 - 单向不可逆加密算法:无法从加密后的数据倒推加密前数据
- 备注1:找回密码的底层逻辑,就是重新设置密码,而不是查到老密码
- 备注2:网站后台将用户传入的密码先作加密,再通过比对加密后的密码的MD5,以确认用户身份
- 通过
- 到
项目文件夹下的\models\UserModel.js
中,建立用户模型/表,并回到路由规则中引入表- 回到项目文件夹下的\routes\web\auth.js,通过UserModel.create往表中写入数据,存入mongodb数据库
- 触发路由规则,可以看到数据已成功写入表中
- 打开注册页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>注册</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet" />
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-8 col-md-offset-2 col-lg-4 col-lg-offset-4">
<h2>注册</h2>
<hr />
<form method="post" action="/reg">
<div class="form-group">
<label for="item">用户名</label>
<input name="username" type="text" class="form-control" id="item" />
</div>
<div class="form-group">
<label for="time">密码</label>
<input name="password" type="password" class="form-control" id="time" />
</div>
<hr>
<button type="submit" class="btn btn-primary btn-block">注册</button>
</form>
</div>
</div>
</div>
</body>
</html>
//导入 mongoose
const mongoose = require('mongoose');
//创建文档的结构对象
//设置集合中文档的属性以及属性值的类型
let UserSchema = new mongoose.Schema({
//标题
username: String,
password: String
});
//创建模型对象 对文档操作的封装对象
let UserModel = mongoose.model('users', UserSchema);
//暴露模型对象
module.exports = UserModel;
var express = require('express');
var router = express.Router();
//导入 用户的模型
const UserModel = require('../../models/UserModel');
// 引入第三方包,使用md5加密
const md5 = require('md5');
//注册
router.get('/reg', (req, res) => {
//响应 HTML 内容
res.render('auth/reg');
});
//注册用户
router.post('/reg', (req, res) => {
//做表单验证;在处理用户数据时,应加上表单校验(此处略,参考此前的gpt方案)
//获取请求体的数据
UserModel.create({...req.body, password: md5(req.body.password)}, (err, data) => {
if(err){
res.status(500).send('注册失败, 请稍后再试~~');
return
}
res.render('success', {msg: '注册成功', url: '/login'});
})
});
module.exports = router;
用户登录+写入session
- 步骤分析:
- 呈现登陆页面
- 表单信息收集,传递服务器
- 后端服务器对账号密码在数据库作检索比对
- 校验通过,写入登录状态,返回session_id,后续根据请求中的session_id确认登录状态
- 校验不通过,提示账号密码错误
- 步骤实现:
- 打开登录页面
项目文件夹下的\views\auth\login.ejs
,改造表单组件,写登录页表单的post
方法提交的信息 - 到
项目文件夹下的\routes\web\auth
中,加post路由规则- 同样,通过
req.body
获取用户提交的信息,let {username, password} = req.body;
- 通过
router.get('/login',
加登录页面,回调函数中渲染登录页res.render('auth/login');
- 通过
UserModel.findOne
到表里确认用户信息 - 通过
UserModel.findOne({username: username, password: md5(password)},
传入用户的password并计算了MD5后再作比较 - 判断校验状态,错误返回500
- 输错账密,数据不对的情况下,后台可以看到数据返回为null(输入正确返回的是一个对象,包含了用户信息),(即!data为null),返回提示'账号或密码错误~~'
- 同样,通过
- 通过
npm i express-session connect-mongo
引入express-session
和connect-mongo
- 在app.js中导入第三方中间件,并设置中间件
app.use(session(
- 注意:中间件的设置需在路由规则之前
- 借用配置项占位符灵活引入mongoUrl:的配置
- 校验通过,到
项目文件夹下的\routes\web\auth
中,校验逻辑结束的地方,写入session,将用户名字写入sessionreq.session.username = data.username;
以及将用户在表中的用户id写入sessionreq.session._id = data._id;
- 返回登陆成功的响应,跳转账户页
res.render('success', {msg: '登录成功', url: '/account'});
- 在app.js中导入第三方中间件,并设置中间件
- 打开登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet" />
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-8 col-md-offset-2 col-lg-4 col-lg-offset-4">
<h2>登录</h2>
<hr />
<form method="post" action="/login">
<div class="form-group">
<label for="item">用户名</label>
<input name="username" type="text" class="form-control" id="item" />
</div>
<div class="form-group">
<label for="time">密码</label>
<input name="password" type="password" class="form-control" id="time" />
</div>
<hr>
<button type="submit" class="btn btn-primary btn-block">登录</button>
</form>
</div>
</div>
</div>
</body>
</html>
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/web/index');
const authRouter = require('./routes/web/auth');
//导入 account 接口路由文件
const accountRouter = require('./routes/api/account');
//导入 express-session
const session = require("express-session");
const MongoStore = require('connect-mongo');
//导入配置项
const {DBHOST, DBPORT, DBNAME} = require('./config/config');
var app = express();
//设置 session 的中间件
app.use(session({
name: 'sid', //设置cookie的name,默认值是:connect.sid
secret: 'atguigu', //参与加密的字符串(又称签名) 加盐
saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的id
resave: true, //是否在每次请求时重新保存session 20 分钟 4:00 4:20
store: MongoStore.create({
mongoUrl: `mongodb://${DBHOST}:${DBPORT}/${DBNAME}` //数据库的连接配置
}),
cookie: {
httpOnly: true, // 开启后前端无法通过 JS 操作
maxAge: 1000 * 60 * 60 * 24 * 7 // 这一条 是控制 sessionID 的过期时间的!!!
},
}))
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/', authRouter);
app.use('/api', accountRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
//响应 404
res.render('404');
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
var express = require('express');
var router = express.Router();
//导入 用户的模型
const UserModel = require('../../models/UserModel');
// 引入第三方包,使用md5加密
const md5 = require('md5');
//注册
router.get('/reg', (req, res) => {
//响应 HTML 内容
res.render('auth/reg');
});
//注册用户
router.post('/reg', (req, res) => {
//做表单验证;在处理用户数据时,应加上表单校验(此处略,参考此前的gpt方案)
//获取请求体的数据
UserModel.create({...req.body, password: md5(req.body.password)}, (err, data) => {
if(err){
res.status(500).send('注册失败, 请稍后再试~~');
return
}
res.render('success', {msg: '注册成功', url: '/login'});
})
});
//登录页面
router.get('/login', (req, res) => {
//响应 HTML 内容
res.render('auth/login');
});
//登录操作
router.post('/login', (req, res) => {
//获取用户名和密码
let {username, password} = req.body;
//查询数据库
UserModel.findOne({username: username, password: md5(password)}, (err, data) => {
//判断
if(err){
res.status(500).send('登录, 请稍后再试~~');
return
}
//判断 data
if(!data){
return res.send('账号或密码错误~~');
}
//写入session,返回session_id
req.session.username = data.username;
req.session._id = data._id;
//登录成功响应
res.render('success', {msg: '登录成功', url: '/account'});
})
});
module.exports = router;
用户登录检测
- 背景:
- 单纯只作session写入,无法将网络资源保护起来
- 使用无痕模式仍然访问、修改数据
- 单纯只作session写入,无法将网络资源保护起来
- 需求:
- 对用户的请求,对用户的身份作校验
- 有session返回结果
- 无session拒绝返回信息
- 对用户的请求,对用户的身份作校验
- 步骤:
- 写session检测逻辑
if(!req.session.username){return res.redirect('/login')}
- 因为不止一个页面使用到,因此将session检测逻辑,封装为一个中间件
- 在除了登录页以外的页面中(如项目文件夹\routes\web\index.js),引入使用这个中间件,作为第二个参数(放在地址之后),放到路由回调中
- 将中间件代码,移出到中间件
项目文件夹\middlewares\checkLoginMiddleware.js
文件中
- 写session检测逻辑
//检测登录的中间件
module.exports = (req, res, next) => {
//判断
if(!req.session.username){
// 不过跳转到登录页
return res.redirect('/login');
}
// 判断通过就执行后续的路由回调
next();
}
//导入 express
const express = require('express');
//导入 moment
const moment = require('moment');
const AccountModel = require('../../models/AccountModel');
//导入中间件检测登录
const checkLoginMiddleware = require('../../middlewares/checkLoginMiddleware');
//创建路由对象
const router = express.Router();
//添加首页路由规则
router.get('/', (req, res) => {
//重定向 /account
res.redirect('/account');
})
//记账本的列表
router.get('/account', checkLoginMiddleware, function(req, res, next) {
//获取所有的账单信息
// let accounts = db.get('accounts').value();
//读取集合信息
AccountModel.find().sort({time: -1}).exec((err, data) => {
if(err){
res.status(500).send('读取失败~~~');
return;
}
//响应成功的提示
res.render('list', {accounts: data, moment: moment});
})
});
//添加记录
router.get('/account/create',checkLoginMiddleware, function(req, res, next) {
res.render('create');
});
//新增记录
router.post('/account',checkLoginMiddleware, (req, res) => {
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if(err){
res.status(500).send('插入失败~~');
return
}
//成功提醒
res.render('success', {msg: '添加成功哦~~~', url: '/account'});
})
});
//删除记录
router.get('/account/:id', checkLoginMiddleware, (req, res) => {
//获取 params 的 id 参数
let id = req.params.id;
//删除
AccountModel.deleteOne({_id: id}, (err, data) => {
if(err) {
res.status(500).send('删除失败~');
return;
}
//提醒
res.render('success', {msg: '删除成功~~~', url: '/account'});
})
});
module.exports = router;
退出登录
- 在登录注册相关的路由文件,写退出登录路由规则及逻辑
- 方法:销毁session,
req.session.destroy(() => {})
形参不传参数 - 优化:网页中增加退出按钮/A链接,向服务端发送请求
- 在
项目文件夹\views\list.ejs
中增加退出登录按钮 - 不需要写协议域名和端口,与页面其他按钮一样,只需要写路由路径herf=/logout
- 防止 CSRF 跨站请求伪造,改为form表单及post请求
- 在
- 方法:销毁session,
var express = require('express');
var router = express.Router();
//导入 用户的模型
const UserModel = require('../../models/UserModel');
// 引入第三方包,使用md5加密
const md5 = require('md5');
//注册
router.get('/reg', (req, res) => {
//响应 HTML 内容
res.render('auth/reg');
});
//注册用户
router.post('/reg', (req, res) => {
//做表单验证;在处理用户数据时,应加上表单校验(此处略,参考此前的gpt方案)
//获取请求体的数据
UserModel.create({...req.body, password: md5(req.body.password)}, (err, data) => {
if(err){
res.status(500).send('注册失败, 请稍后再试~~');
return
}
res.render('success', {msg: '注册成功', url: '/login'});
})
});
//登录页面
router.get('/login', (req, res) => {
//响应 HTML 内容
res.render('auth/login');
});
//登录操作
router.post('/login', (req, res) => {
//获取用户名和密码
let {username, password} = req.body;
//查询数据库
UserModel.findOne({username: username, password: md5(password)}, (err, data) => {
//判断
if(err){
res.status(500).send('登录, 请稍后再试~~');
return
}
//判断 data
if(!data){
return res.send('账号或密码错误~~');
}
//写入session,返回session_id
req.session.username = data.username;
req.session._id = data._id;
//登录成功响应
res.render('success', {msg: '登录成功', url: '/account'});
})
});
//退出登录
router.post('/logout', (req, res) => {
//销毁 session
req.session.destroy(() => {
res.render('success', {msg: '退出成功', url: '/login'});
})
});
module.exports = router;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet" />
<style>
label {
font-weight: normal;
}
.panel-body .glyphicon-remove {
display: none;
}
.panel-body:hover .glyphicon-remove {
display: inline-block
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12 col-lg-8 col-lg-offset-2">
<div class="row text-right">
<div class="col-xs-12" style="padding-top: 20px">
<form method="post" action="/logout">
<button class="btn btn-danger">退出</button>
</form>
</div>
</div>
<hr>
<div class="row">
<h2 class="col-xs-6">记账本</h2>
<h2 class="col-xs-6 text-right"><a href="/account/create" class="btn btn-primary">添加账单</a></h2>
</div>
<hr />
<div class="accounts">
<% accounts.forEach(item=> { %>
<div class="panel <%= item.type=== -1 ? 'panel-danger' : 'panel-success' %>">
<div class="panel-heading">
<%= moment(item.time).format('YYYY-MM-DD') %>
</div>
<div class="panel-body">
<div class="col-xs-6">
<%= item.title %>
</div>
<div class="col-xs-2 text-center">
<span class="label <%= item.type=== -1 ? 'label-warning' : 'label-success' %>">
<%= item.type===-1 ? '支出' : '收入' %>
</span>
</div>
<div class="col-xs-2 text-right">
<%= item.account %> 元
</div>
<div class="col-xs-2 text-right">
<a class="delBtn" href="/account/<%= item._id %>">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</a>
</div>
</div>
</div>
<% }) %>
</div>
</div>
</div>
</div>
</body>
<script>
//获取所有的 delBtn
let delBtns = document.querySelectorAll('.delBtn');
//绑定事件
delBtns.forEach(item => {
item.addEventListener('click', function (e) {
if (confirm('您确定要删除该文档么??')) {
return true;
} else {
//阻止默认行为
e.preventDefault();
}
});
})
</script>
</html>
CSRF 跨站请求伪造
CSRF 跨站请求伪造,也算是网站攻击的一种形式
方式:
- 伪站的协议、路径等都与正站一致,但端口不一样
- 可以理解为:来自于不同的服务
- 也可以理解为:来自于不同的网站
- 伪站中有正站同端口的链接服务
- 伪站中有服务直接给正站发请求,干扰正站,即跨站行为
- 跨站行为:B网站向A网站服务发送请求,且在请求中,还携带上了A站的cookie(即B网站的跨站请求携带了A网站的cookie)
- 伪站的协议、路径等都与正站一致,但端口不一样
触发步骤:
- 在正站上正常访问使用
- 此时打开伪站,触发伪站中的跨站行为
- 下方的伪站代码,会在访问伪站时,在伪站加载的过程中,会往A网站发送请求
- 回到正站,观察到登录状态发生了变化
解决办法:
- 将正站退出的路由规则,改为post
防范注意:
- HTML页面中的link标签、script标签、image标签,在HTML页面加载过程中,也会向外发送请求
- 这些请求使用的都是get请求,目的是自动加载资源,包括第三方图片视频等静态资源,也包括js代码,如cdn的组件等
- 退出的路由规则改为post后,就无法跨站发退出请求了
- 对资源的操作,尽量不要使用get请求,如果是获取数据、返回数据,可以选择get请求方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSRF 演示</title>
<link rel="stylesheet" href="http://127.0.0.1:3000/logout">
</head>
<body>
</body>
</html>
完善首页和404页面功能
访问没有写路由规则的页面,返回404
添加首页路由规则
router.get('/'
//导入 express
const express = require('express');
//导入 moment
const moment = require('moment');
const AccountModel = require('../../models/AccountModel');
//导入中间件检测登录
const checkLoginMiddleware = require('../../middlewares/checkLoginMiddleware');
//创建路由对象
const router = express.Router();
//添加首页路由规则
router.get('/', (req, res) => {
//重定向 /account
res.redirect('/account');
})
//记账本的列表
router.get('/account', checkLoginMiddleware, function(req, res, next) {
//获取所有的账单信息
// let accounts = db.get('accounts').value();
//读取集合信息
AccountModel.find().sort({time: -1}).exec((err, data) => {
if(err){
res.status(500).send('读取失败~~~');
return;
}
//响应成功的提示
res.render('list', {accounts: data, moment: moment});
})
});
//添加记录
router.get('/account/create',checkLoginMiddleware, function(req, res, next) {
res.render('create');
});
//新增记录
router.post('/account',checkLoginMiddleware, (req, res) => {
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if(err){
res.status(500).send('插入失败~~');
return
}
//成功提醒
res.render('success', {msg: '添加成功哦~~~', url: '/account'});
})
});
//删除记录
router.get('/account/:id', checkLoginMiddleware, (req, res) => {
//获取 params 的 id 参数
let id = req.params.id;
//删除
AccountModel.deleteOne({_id: id}, (err, data) => {
if(err) {
res.status(500).send('删除失败~');
return;
}
//提醒
res.render('success', {msg: '删除成功~~~', url: '/account'});
})
});
module.exports = router;
到app.js中配置404页面
- 通过app.use(function(req, res, next) {回调函数响应404模板页})
配置404页面
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/web/index');
const authRouter = require('./routes/web/auth');
//导入 account 接口路由文件
const accountRouter = require('./routes/api/account');
//导入 express-session
const session = require("express-session");
const MongoStore = require('connect-mongo');
//导入配置项
const {DBHOST, DBPORT, DBNAME} = require('./config/config');
var app = express();
//设置 session 的中间件
app.use(session({
name: 'sid', //设置cookie的name,默认值是:connect.sid
secret: 'atguigu', //参与加密的字符串(又称签名) 加盐
saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的id
resave: true, //是否在每次请求时重新保存session 20 分钟 4:00 4:20
store: MongoStore.create({
mongoUrl: `mongodb://${DBHOST}:${DBPORT}/${DBNAME}` //数据库的连接配置
}),
cookie: {
httpOnly: true, // 开启后前端无法通过 JS 操作
maxAge: 1000 * 60 * 60 * 24 * 7 // 这一条 是控制 sessionID 的过期时间的!!!
},
}))
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/', authRouter);
app.use('/api', accountRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
//响应 404
res.render('404');
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
- 配置404页模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404</title>
</head>
<body>
<script src="//volunteer.cdn-go.cn/404/latest/404.js"></script>
</body>
</html>
token
token 是什么
token
是服务端生成并返回给 HTTP 客户端的一串 加密字符串token
中保存着用户信息
token 的作用
- 实现会话控制,服务端通过token可以识别用户的身份
- token 主要用于移动端 APP (安卓、iOS、小程序)
- 网页中使用session和cookie比较多
token 的工作流程
token的生成:
- 填写账号和密码,发送到服务器校验身份,在校验通过后,服务端生成 token 响应返回给客户端
- token 一般是在响应体中返回给客户端的
token的使用:
- 后续发送请求时,需要手动将 token 添加在请求报文中,一般是放在请求头中
- 二次请求时,需要带着token传递给服务器
- 服务器会对token作校验,并提取出用户的信息,进而识别用户身份
token与cookie的工作流程是相当类似的
token与cookie的不同点:
- cookie是自动携带的,而token是手动携带
- cookie信息明文保存,而token数据是完全加密的
token与session的差异:
- session的用户数据保存在服务端,而token的用户数据保存在客户端,不占用服务器端资源
token 的特点
- 使用token,服务端压力更小
- 数据存储在客户端(信息保存在手机本地)
- token相对更安全
- 数据加密
- 可以避免 CSRF(跨站请求伪造)
- token不自动携带,因此服务器不自动识别身份,不影响操作资源
- token扩展性更强
- 服务间可以共享token,一个token可以多个服务共享
- 增加服务节点更简单
- 如果使用session,用户信息在服务器端,增加服务节点时需要将用户信息作服务器同步,如果不同步,用户的登录状态丢失服务异常
JWT 介绍与演示
JWT(JSON Web Token)是目前最流行的跨域认证解决方案,可用于基于
token
的身份验证,实现会话控制- JWT 使 token 的生成与校验更规范
- 我们可以使用
jsonwebtoken 包
来操作 token
使用步骤:
- 执行
npm i jsonwebtoken
装包,const jwt = require('jsonwebtoken');
导包 - 创建(生成) token
- 当服务端接收到用户信息,校验通过后创建,并返回客户端
- 通过
jwt.sign(用户数据, 加密字符串, 配置对象);
sign方法生成token - 第一个参数是token要存储的数据,一般是用户数据
- 第二个参数与session类似,需要传递一个加密字符串,相当于session.secret设置密钥;
- 设置签名/加密的字符串,以提高安全等级(又称签名、加盐)
- 在第三个参数配置对象中,设置token的生命周期
- 返回给客户端的是:信息.加密算法信息.签名(加密结果),后续用秘钥再加密对比下签名就行
- 校验 token
- 当后续客户端二次发起请求时,将token携带进报文中
- 服务端接收后,对token作校验,提取出用户信息
- 通过
jwt.verify(传入token, 参与加密的字符串, 回调函数);
verify方法校验token- 若token校验通过,在回调函数中的第二个参数 data ,就能拿到提取出的用户信息,包括token的创建时间和过期时间
- token过期后无法再通过校验(生命周期过后,需要重新生成token)
- 执行
创建(生成) token (运行该 *.js 文件后生成)
//导入 jwt
const jwt = require('jsonwebtoken');
// 创建(生成) token
let token = jwt.sign(用户数据, 加密字符串, 配置对象);
let token = jwt.sign({
username: 'zhangsan'
}, 'atguigu', {
expiresIn: 60, //单位是秒
})
console.log(token);
校验 token
//导入 jwt
const jwt = require('jsonwebtoken');
let t = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InpoYW5nc2FuIiwiaWF0IjoxNjc3MzEzNTc1LCJleHAiOjE2NzczMTM2MzV9.fziAyCdYfhMYeM2a-XPMNZYdhIVYpluBoNR1c5oUm70';
//校验 token
jwt.verify(t, 'atguigu', (err, data) => {
if(err){
console.log('校验失败~~');
return
}
console.log(data);
})
语法案例
//导入 jsonwebtokan
const jwt = require('jsonwebtoken');
//创建 token
// jwt.sign(数据, 加密字符串, 配置对象)
let token = jwt.sign({
username: 'zhangsan'
}, 'atguigu', {
expiresIn: 60 //单位是 秒
});
//解析 token
jwt.verify(token, 'atguigu', (err, data) => {
if (err) {
console.log('校验失败~~');
return
}
console.log(data);
});
扩展阅读:https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
实战案例-记账本-结合token功能
- 需求:结合token,对记账本的数据作保护,包括存储、查看、操作时的身份校验等
登录响应token返回客户端
背景:
- 只在项目的网页端(页面端)有约束
- 例如:已无法直接通过匹配路由路径(在地址栏直接填页面路由路径)访问需要权限的页面,路由规则会拦截,并强制跳转到登录页
- 项目的接口端,没有作约束
- 如在网页端,直接在地址栏中,写api路径,直接请求接口,此时是能拿到资源的,可以看到返回了JSON格式的字符串,依然是没有受到约束和限制(即APP端没有作约束,直接发请求,就返回结果,不管是否处于已登录的状态)
- 只在项目的网页端(页面端)有约束
步骤:
- 启动项目
npm start
- 打开路由文件目录,接口相关路由在
项目文件夹\routes\api
,网页相关路由在项目文件夹\routes\web
,可以看到,网页相关的路由增加了登录检测项目文件夹\routes\web\auth.js
,接口相关还没有 - 新增
项目文件夹\routes\api\auth.js
文件,只保留操作相关的交互逻辑- 查询数据库,校验通过后,在回调函数中,返回JSON格式数据
- 引入JWT,先
npm i jsonwebtoken
装包,const jwt = require('jsonwebtoken');
导包 - 通过使用JWT,给用户响应token,给用户返回token,返回
res.json({code: '0000',msg: '登录成功',data: token})
- 创建token,让token作为JSON数据返回,通过
jwt.sign({用户的信息})
方法创建当前用户的token,如let token = jwt.sign({username,_id,等用户信息},参与加密的字符串,{生命周期配置对象})
- 回到app.js中,导入
const authApiRouter = require('./routes/api/auth');
API权限校验,app.use('/api', authApiRouter);
app.use设置触发路由规则时调用- 及后通过接口调试工具,通过JSON方式传参接口,确认接口正常
- 完成用户登录,下放token,下一节加中间件,校验token,保护接口数据
- 启动项目
var express = require('express')
var router = express.Router()
//导入 jwt
const jwt = require('jsonwebtoken')
//导入配置文件
const { secret } = require('../../config/config')
//导入 用户的模型
const UserModel = require('../../models/UserModel')
const md5 = require('md5')
//登录操作
router.post('/login', (req, res) => {
//获取用户名和密码
let { username, password } = req.body
//查询数据库
UserModel.findOne({ username: username, password: md5(password) }, (err, data) => {
//判断
if (err) {
res.json({
code: '2001',
msg: '数据库读取失败~~~',
data: null
})
return
}
//判断 data
if (!data) {
return res.json({
code: '2002',
msg: '用户名或密码错误~~~',
data: null
})
}
//创建当前用户的 token
let token = jwt.sign({
username: data.username,
_id: data._id
}, secret, {
// 一周生命周期
expiresIn: 60 * 60 * 24 * 7
})
//响应 token
res.json({
code: '0000',
msg: '登录成功',
data: token
})
})
})
//退出登录
router.post('/logout', (req, res) => {
//销毁 session
req.session.destroy(() => {
res.render('success', { msg: '退出成功', url: '/login' })
})
})
module.exports = router
const authApiRouter = require('./routes/api/auth');
app.use('/api', authApiRouter);
token校验
背景:
- 通过上节,已经给用户app客户端,下放token返回,需要校验用户的token,完成二次请求的用户权限校验
- 以帐单列表功能,为token校验逻辑展示
分析:
- 定位用户传入的token的位置:token的传递是受服务端约束的,即
服务端告诉客户端二次请求携带token传递时,在哪个位置传递token,客户端会照办
,一般都是放在请求头
中传递的- 请求头中的token键名字不是固定的,可以是
token
,也可以是tk
,或者user key
- 案例中将token定为头名传递给服务器,服务端通过获取名为token的请求头内容,来获取token;
- 请求头中的token键名字不是固定的,可以是
- 定位用户传入的token的位置:token的传递是受服务端约束的,即
步骤:
- 到
项目文件夹\routes\api\account.js
中,找到 记账本的列表 部分逻辑,写校验token- 在获取账单列表的路由规则中,先通过
let token = req.get('token')
拿到请求头名为token的数据- 判断有没有token,没有则返回错误代码和相关msg信息
- 通过接口调试工具,确认错误情况
- 在判断完token后,接着写token校验的逻辑
- 导入jwt
const jwt = require('jsonwebtoken');
,通过jwt.verify(传入token, 参与加密的字符串, 回调函数);
verify方法校验token - 在校验token的回调函数中,检测token是否正确,不正确返回JSON对象和报错码提示,正确则接上数据库读取的逻辑
- 导入jwt
- 在获取账单列表的路由规则中,先通过
- 将校验token的逻辑,封装为中间件
checkTokenMiddleware
,以供其他路由规则使用(除了记账本列表,还有新增、删除、等等)- 到
项目文件夹\routes\api\account.js
中,抽出获取token + 判断token有无 + 校验token是否正确 + 保存用户的信息 + 校验成功时继续执行后续的路由回调
的逻辑,作为中间件函数 - 将中间件函数,放在每条路由规则中,在路由传参方式中的第二个参数,引入中间件使用
- 封装中间件到
项目文件夹\middlewares\checkTokenMiddleware.js
目录下文件checkTokenMiddleware.js,通过module.exports = 回调函数
,然后原路由规则里导入中间件let checkTokenMiddleware = require('../../middlewares/checkTokenMiddleware')
- 注意,导入语句在上/前,声明语句在后/下
- 到
- 到
//导入 express
const express = require('express')
//导入 jwt
const jwt = require('jsonwebtoken')
const router = express.Router()
//导入 moment
const moment = require('moment')
const AccountModel = require('../../models/AccountModel')
//记账本的列表
router.get('/account', function (req, res, next) {
// 获取 token
let token = req.get('token')
if (!token) {
return res.json({
code: '2003',
msg: 'token缺失~~',
data: null
})
}
// 校验 token
jwt.verify(token, atguigu, (err, data) => {
// 检测 token 是否正确
if (err) {
return res.json({
code: '2004',
msg: 'token校验失败',
data: null
})
}
// 如果token正确,放入数据库读取的逻辑
//读取集合信息
AccountModel.find().sort({ time: -1 }).exec((err, data) => {
if (err) {
res.json({
code: '1001',
msg: '读取失败~~',
data: null
})
return
}
//响应成功的提示
res.json({
//响应编号
code: '0000',
//响应的信息
msg: '读取成功',
//响应的数据
data: data
})
})
})
})
//新增记录
router.post('/account', (req, res) => {
//表单验证
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if (err) {
res.json({
code: '1002',
msg: '创建失败~~',
data: null
})
return
}
//成功提醒
res.json({
code: '0000',
msg: '创建成功',
data: data
})
})
})
//删除记录
router.delete('/account/:id', (req, res) => {
//获取 params 的 id 参数
let id = req.params.id
//删除
AccountModel.deleteOne({ _id: id }, (err, data) => {
if (err) {
res.json({
code: '1003',
msg: '删除账单失败',
data: null
})
return
}
//提醒
res.json({
code: '0000',
msg: '删除成功',
data: {}
})
})
})
//获取单个账单信息
router.get('/account/:id', (req, res) => {
//获取 id 参数
let { id } = req.params
//查询数据库
AccountModel.findById(id, (err, data) => {
if (err) {
return res.json({
code: '1004',
msg: '读取失败~~',
data: null
})
}
//成功响应
res.json({
code: '0000',
msg: '读取成功',
data: data
})
})
})
//更新单个账单信息
router.patch('/account/:id', (req, res) => {
//获取 id 参数值
let { id } = req.params
//更新数据库
AccountModel.updateOne({ _id: id }, req.body, (err, data) => {
if (err) {
return res.json({
code: '1005',
msg: '更新失败~~',
data: null
})
}
//再次查询数据库 获取单条数据
AccountModel.findById(id, (err, data) => {
if (err) {
return res.json({
code: '1004',
msg: '读取失败~~',
data: null
})
}
//成功响应
res.json({
code: '0000',
msg: '更新成功',
data: data
})
})
})
})
module.exports = router
//声明中间件
let checkTokenMiddleware = (req, res, next) => {
//获取 token
let token = req.get('token');
//判断
if (!token) {
return res.json({
code: '2003',
msg: 'token 缺失',
data: null
})
}
//校验 token
jwt.verify(token, secret, (err, data) => {
//检测 token 是否正确
if (err) {
return res.json({
code: '2004',
msg: 'token 校验失败~~',
data: null
})
}
//保存用户的信息
req.user = data; // req.session req.body
//如果 token 校验成功
next();
});
}
//导入 jwt
const jwt = require('jsonwebtoken');
//读取配置项
const {secret} = require('../config/config');
//声明中间件
module.exports = (req, res, next) => {
//获取 token
let token = req.get('token');
//判断
if (!token) {
return res.json({
code: '2003',
msg: 'token 缺失',
data: null
})
}
//校验 token
jwt.verify(token, secret, (err, data) => {
//检测 token 是否正确
if (err) {
return res.json({
code: '2004',
msg: 'token 校验失败~~',
data: null
})
}
//保存用户的信息
req.user = data; // req.session req.body
//如果 token 校验成功
next();
});
}
//导入 express
const express = require('express')
//导入 jwt
const jwt = require('jsonwebtoken')
//导入中间件
let checkTokenMiddleware = require('../../middlewares/checkTokenMiddleware')
const router = express.Router()
//导入 moment
const moment = require('moment')
const AccountModel = require('../../models/AccountModel')
//记账本的列表
router.get('/account', checkTokenMiddleware, function (req, res, next) {
// 抽出
// // 获取 token
// let token = req.get('token')
// if (!token) {
// return res.json({
// code: '2003',
// msg: 'token缺失~~',
// data: null
// })
// }
// // 校验 token
// jwt.verify(token, atguigu, (err, data) => {
// // 检测 token 是否正确
// if (err) {
// return res.json({
// code: '2004',
// msg: 'token校验失败',
// data: null
// })
// }
// 如果token正确,放入数据库读取的逻辑
//读取集合信息
AccountModel.find().sort({ time: -1 }).exec((err, data) => {
if (err) {
res.json({
code: '1001',
msg: '读取失败~~',
data: null
})
return
}
//响应成功的提示
res.json({
//响应编号
code: '0000',
//响应的信息
msg: '读取成功',
//响应的数据
data: data
})
})
})
//新增记录
router.post('/account', checkTokenMiddleware, (req, res) => {
//表单验证
//插入数据库
AccountModel.create({
...req.body,
//修改 time 属性的值
time: moment(req.body.time).toDate()
}, (err, data) => {
if (err) {
res.json({
code: '1002',
msg: '创建失败~~',
data: null
})
return
}
//成功提醒
res.json({
code: '0000',
msg: '创建成功',
data: data
})
})
})
//删除记录
router.delete('/account/:id', checkTokenMiddleware, (req, res) => {
//获取 params 的 id 参数
let id = req.params.id
//删除
AccountModel.deleteOne({ _id: id }, (err, data) => {
if (err) {
res.json({
code: '1003',
msg: '删除账单失败',
data: null
})
return
}
//提醒
res.json({
code: '0000',
msg: '删除成功',
data: {}
})
})
})
//获取单个账单信息
router.get('/account/:id', checkTokenMiddleware, (req, res) => {
//获取 id 参数
let { id } = req.params
//查询数据库
AccountModel.findById(id, (err, data) => {
if (err) {
return res.json({
code: '1004',
msg: '读取失败~~',
data: null
})
}
//成功响应
res.json({
code: '0000',
msg: '读取成功',
data: data
})
})
})
//更新单个账单信息
router.patch('/account/:id', checkTokenMiddleware, (req, res) => {
//获取 id 参数值
let { id } = req.params
//更新数据库
AccountModel.updateOne({ _id: id }, req.body, (err, data) => {
if (err) {
return res.json({
code: '1005',
msg: '更新失败~~',
data: null
})
}
//再次查询数据库 获取单条数据
AccountModel.findById(id, (err, data) => {
if (err) {
return res.json({
code: '1004',
msg: '读取失败~~',
data: null
})
}
//成功响应
res.json({
code: '0000',
msg: '更新成功',
data: data
})
})
})
})
module.exports = router
token功能完善
- 问题背景及解决:
- 报错1:JWT没有定义
- 解决1:封装的 项目文件夹\middlewares\checkTokenMiddleware.js 中,需要导包,导入jwt
- 优化点2:抽出封装的中间件函数,密钥是写死的,不方便修改
- 优化解决2:
- 将抽出封装的中间件函数中的密钥,作为配置变量,从配置文件
项目文件夹\config\config.js
中引入 - 然后在配置文件中,新增一个变量
secret: 'atguigu',
来存放密钥,替换掉原来的带单引号的固定字符串; - 回到中间件checkTokenMiddleware.js中,通过
const {secret} = require('../config/config');
读取配置变量 - 所有原来涉及密钥的地方,都可以改为导入配置项来设置,如
项目文件夹\routes\api\auth.js
- 将抽出封装的中间件函数中的密钥,作为配置变量,从配置文件
- 优化点3:校验token正确后,应该保存用户的信息
- 在通过token校验后的接口,可以进入到路由回调中,作一系列操作如获取列表、新增等
- 将来存在需求:在路由回调中,获取当前用户的信息(多用户使用时,只读取当前访问用户的信息)
- 优化解决3:
- 在中间件函数中,在完成token校验的逻辑后,加
req.user = data;
,往请求对象req中,存储用户的数据信息 - 中间件函数是可以访问请求和响应对象的,在请求对象中添加req.user属性,在后续路由回调中,访问req请求对象中的user属性的数据,得到当前的用户信息
req.session
和req.data
同理,经过中间件函数对请求报文处理(如加密字符串),将数据和方法,存到了请求和响应的对象req、res身上
- 在中间件函数中,在完成token校验的逻辑后,加
附录1 配置本地域名
所谓本地域名就是
只能在本机使用的域名
,一般在开发阶段使用- 本地电脑hosts文件,强制将电脑浏览器地址/ip,作重定向
- 将在浏览器输入的后面的地址,强制跳转重定向到前面的ip/地址
- 只是域名地址/ip对应,浏览器访问时,需要加上端口号
- 本地电脑hosts文件,强制将电脑浏览器地址/ip,作重定向
可以到项目目录下的
项目文件夹\bin\www
中,修改nodejs的项目运行端口,可以改为80process.env.PORT
为获取环境变量
操作流程
编辑文件
C:\Windows\System32\drivers\etc\hosts
添加/修改hosts记录
<!-- 将在浏览器输入的后面的地址,强制跳转重定向到前面的ip/地址 -->
<!-- 只是域名地址/ip对应,浏览器访问时,需要加上端口号 -->
127.0.0.1 www.baidu.com
- 备注:如果修改失败,
可以修改该文件的权限
原理
在地址栏输入
域名
之后,浏览器会先进行 DNS(Domain Name System,即域名系统) 查询,获取该域名对应的 IP 地址DNS 查询请求会发送到 DNS 服务器,DNS 服务器会
根据域名返回 IP 地址
通过DNS确认请求报文的发送目标对象,域名系统会根据域名,返回该域名相应的IP地址
客户端浏览器根据返回的IP地址,发请求报文到指定IP地址的主机
可以通过
ipconfig /all
查看本机的 DNS 服务器hosts
文件也可以设置域名与 IP 的映射关系,在发送请求前,可以通过该文件获取域名的 IP 地址- 浏览器在作DNS查询前,会先通过hosts文件查询,通过在hosts文件中指定域名与IP的影视关系,就能实现本地域名效果
- 屏蔽广告的方式:将广告资源的url域名,在hosts文件中,配置为127.0.0.1,广告拿不到资源,以实现消除广告
- 防止软件自动更新的方式:更新的资源来源域名,配置为127.0.0.1
附录2 项目上线运行相关操作
代码上传远端仓库
步骤:
- 通过vscode的GUI界面,初始化仓库
- 创建
.gitignore
忽略文件列表- 包括依赖文件夹node_module
- 图片资源目录,项目文件夹\public\upload
- git操作
- 本地git操作
- 远端git操作,建立空仓库,记下地址
- 在vscode中,添加本地仓库对应的远程仓库地址
- 源代码管理>点击右侧
...
>远程>添加远程存储库 - 输项目名字、账号、密码,完成远程仓库的添加
- 提交到远程仓库,点击
发布 Branch
提交上传
- 源代码管理>点击右侧
购买云服务器
- 买境外的,临时演示用/服务范围不在国内,免beian
- 可以 Windows server
连接服务器与软件安装
- 通过Windows的远程桌面连接,通过购买的Windows server云服务器的公网IP,实现远程连接
- 输入远程计算机公网IP地址
- 连接,默认显示的用户名为本地电脑的用户名,应更改为部署服务器时,自己自行设置的用户名和密码,点击
使用其他账户
设置 - 连上远端服务器,安装软件,包括:
- git
- nodejs
- MongoDB(通过win下的msi安装程序安装,会连带服务端一起安装)
代码克隆服务启动
- 步骤:
- 复制仓库地址,打开云服务器,到项目目录中,通过 git bash,通过
git clone 项目地址
拉取远端项目平台的项目,到远端服务器上 - 进入目录,通过cmd,通过
npm i
安装依赖,npm i -g nodemon
nodejs保存自重启依赖,npm start
启动项目 - Windows电脑上,通过安装包安装MongoDB,会自动创建服务,不需要额外去启动mongodb服务
- 点
开始
,输入服务
,可以在服务进程列表中,找到mongodb的服务,双击点进去,可以看到服务在运行,因此不需要再手动去运行mongodb命令
- 点
- 服务跑起后,任何人都可以通过公网IP去访问服务(需要80端口)
- 通过nodemon去启动服务更多是在开发过程中使用,实际项目生产部署还是使用node
- 打开项目配置
项目文件夹\package.json
,找到scripts配置,改为dev下用nodemon,如"scripts": {"dev": "nodemon ./bin/www"}
,正式运行为"scripts": {"server": "node ./bin/www"}
- 完成本地配置修改,vscode中通过gui,提交到远端,同步更改远端状态
- 服务器端,使用 git bash,通过
git pull
,从远端项目平台,拉取更新服务器本地的项目文件 - 服务器本地改为通过
npm run server
启动项目
- 打开项目配置
- 复制仓库地址,打开云服务器,到项目目录中,通过 git bash,通过
域名购买与解析
- 买域名,优先买.com,用户识别度更高
- 国内使用域名需要备案
- 域名解析:域名绑定ip对应关系,域名提供商会将你设置的
域名与ip的绑定对应关系
同步到DNS服务器 - 记录类型:A链接类型,直接将域名指向一个ipv4的地址
- 主机记录:填写主机的前缀(即填写域名的前缀)
- 记录值:填写服务器的公网ip地址,也就是域名需要绑定的ip地址
- 如果记录类型不一样,填的记录值不一样,A类型就是为公网ipv4地址
可设置的DNS记录类型如下:
配置HTTPS证书
- https 本意是
http + SSL(Secure Sockets Layer 安全套接层)
- https 可以
加密 HTTP 报文
,所以大家也可以理解为是 安全的 HTTP- 加密是双向的,服务端/客户端给对方的请求都是加密的,只有对方能解密,其他人看到内容都不能解密读取数据
获取+配置证书流程:
- 备注:操作需要在服务器端完成
- 工具官网:https://certbot.eff.org/
- 操作流程:
- 下载工具 https://dl.eff.org/certbot-beta-installer-win_amd64.exe服务器端访问,下载证书工具
- 安装工具
- 管理员身份,cmd运行命令
certbot certonly--standalone
安装证书- 注意在安装证书前,需要先停掉先前的服务(证书安装需要80端口,不停掉装不上)
- 但是在Linux下安装证书其实可以不需要停,Linux下安装证书会校验域名的地址
- 输入域名,必须是指到当前服务器ip的域名,才能通过证书校验
- 打开证书配置好后的证书存放目录
- 代码配置如下
- 在服务器上,修改服务器本地的
项目文件夹\bin\www
文件,导入证书 - 通过
const https =require('https');
导入https模块 - 创建服务的位置
var server = http.createServer(app)
,改为使用https创建https.createServer
- 并传入服务配置对象,对象中写入证书相关文件路径,https.createServer(证书相关文件路径)
- 并在文件中,导入证书配置对象所使用到的依赖,如fs,
const fs = require('fs')
- 配置路径修改为实际证书路径
- win下的路径需要转义更换一下
- 可以做个server2来启动http的80端口,也可以强制重定向到https
- 在服务器上,修改服务器本地的
- 证书也有生命周期,3个月有效期
- 一般更新:适用于证书到期时间小于1个月,命令为
certbot renew
,如果证书到期时间大于1个月时无法使用 - 强制更新:
certbot --force-renew
任何时候都可以使用
- 一般更新:适用于证书到期时间小于1个月,命令为
const https =require('https');
https
.createServer(
{
key:fs.readFileSync('/etc/letsencrypt/path/to/key.pem'),
cert:fs.readFileSync('/etc/letsencrypt/path/to/cert.pem'),
ca:fs.readFileSync('/etc/letsencrypt/path/to/chain.pem'),
},
app
)
listen(443,()=>{
console.log('Listening...')
})
#!/usr/bin/env node
//导入 db 函数
const db = require('../db/db')
// 导入 fs
const fs = require('fs')
//调用 db 函数
db(() => {
/**
* Module dependencies.
*/
var app = require('../app')
var debug = require('debug')('accounts:server')
var http = require('http')
// 导入https模块
const https = require('https')
/**
* Get port from environment and store in Express.
*/
// 默认端口/http端口是 80
// var port = normalizePort(process.env.PORT || '80')
// 更改为 https协议的默认端口 443
var port = normalizePort(process.env.PORT || '80')
app.set('port', port)
/**
* Create HTTP server.
*/
// 原来的http服务
// var server = http.createServer(app)
// 改为https
var server = https.createServer({
key: fs.readFileSync('C:\\Certbot\\live\\你的域名\\privkey.pem'),
cert: fs.readFileSync('C:\\Certbot\\live\\你的域名\\cert.pem'),
ca: fs.readFileSync('C:\\Certbot\\live\\你的域名\\chain.pem'),
},
app)
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port)
server.on('error', onError)
server.on('listening', onListening)
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort (val) {
var port = parseInt(val, 10)
if (isNaN(port)) {
// named pipe
return val
}
if (port >= 0) {
// port number
return port
}
return false
}
/**
* Event listener for HTTP server "error" event.
*/
function onError (error) {
if (error.syscall !== 'listen') {
throw error
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges')
process.exit(1)
case 'EADDRINUSE':
console.error(bind + ' is already in use')
process.exit(1)
default:
throw error
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening () {
var addr = server.address()
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port
debug('Listening on ' + bind)
}
})
#!/usr/bin/env node
//导入 db 函数
const db = require('../db/db')
// 导入 fs
const fs = require('fs')
//调用 db 函数
db(() => {
/**
* Module dependencies.
*/
var app = require('../app')
var debug = require('debug')('accounts:server')
var http = require('http')
// 导入https模块
const https = require('https')
/**
* Get port from environment and store in Express.
*/
// 默认端口/http端口是 80
// var port = normalizePort(process.env.PORT || '80')
// 更改为 https协议的默认端口 443
var port = normalizePort(process.env.PORT || '80')
app.set('port', port)
/**
* Create HTTP server.
*/
// 原来的http服务
// var server = http.createServer(app)
// 改为https
var server = https.createServer({
key: fs.readFileSync('C:\\Certbot\\live\\你的域名\\privkey.pem'),
cert: fs.readFileSync('C:\\Certbot\\live\\你的域名\\cert.pem'),
ca: fs.readFileSync('C:\\Certbot\\live\\你的域名\\chain.pem'),
},
app)
// 做个server2来启动http的80端口
var server2 = http.createServer(app)
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port)
server.on('error', onError)
server.on('listening', onListening)
// 做个server2来启动http的80端口
server2.listen(80)
server2.on('error', onError)
server2.on('listening', onListening)
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort (val) {
var port = parseInt(val, 10)
if (isNaN(port)) {
// named pipe
return val
}
if (port >= 0) {
// port number
return port
}
return false
}
/**
* Event listener for HTTP server "error" event.
*/
function onError (error) {
if (error.syscall !== 'listen') {
throw error
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges')
process.exit(1)
case 'EADDRINUSE':
console.error(bind + ' is already in use')
process.exit(1)
default:
throw error
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening () {
var addr = server.address()
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port
debug('Listening on ' + bind)
}
})
Linux下,获取+配置证书流程:
- 准备步骤:
sudo apt-get install certbot
安装Certbot证书工具git clone https://github.com/letsencrypt/letsencrypt
安装Let's Encryptsudo apt install python3-certbot-nginx
安装 Certbot Nginx 插件sudo systemctl restart certbot
重新启动 Certbot
- 不需要停止服务的情况下获取证书:
- Nginx,运行以下命令:
sudo certbot --nginx
- Apache,运行以下命令:
sudo certbot --apache
- Nginx,运行以下命令:
- 证书配置成功后:
sudo systemctl reload nginx
重新加载 Nginx
- 备注:
- 如果修改了服务器块(也就是修改了sites-available中的配置,需要先删除sites-enabled文件夹中的,再重新启用);
- 启用新创建的服务器块:
sudo ln -s /etc/nginx/sites-available/0.conf /etc/nginx/sites-enabled/
- 如果修改了sites-available中的配置,又已经有sites-enabled文件夹的链接,直接重启nginx即可
systemctl restart nginx
- 启用自动续订证书
sudo certbot renew --dry-run
- 站点在线验证
certbot --nginx -d yourdomain.com
- 准备步骤:
当域名已经被黑,http01验证失效,需要使用DNS验证
- 此时需要将 Let's Encrypt 证书切换为 DNS 验证,需要安装 Certbot 的 DNS 插件
- 假设我们今天的DNS服务商是cloudflare,我们就可以使用下面这个命令来安装cloudflare的DNS插件
- 安装DNS插件
sudo apt install certbot python3-certbot-dns-cloudflare
- 安装DNS插件
- 需要为您的域名创建一个 DNS 验证令牌,允许 Certbot 在您的 DNS 记录中创建和删除 TXT 记录以验证您的所有权
- 列举Cloudflare这个DNS服务商的配置案例:
- 在 Cloudflare 仪表板中,转到您的域名 > DNS 记录。
- 单击“添加记录”,然后选择“TXT”类型。
- 在“名称”字段中,输入
_acme-challenge
。 - 在“内容”字段中,输入 32 个字符的随机字符串(例如
abc123def456
,可以通过工具箱获取一个32 个字符的随机字符串). - 单击“保存”。
- 其他DNS服务商类似,到DNS记录中,添加“TXT”类型的记录,“名称”字段中,输入
_acme-challenge
,“内容”字段中,输入 32 个字符的随机字符串(例如abc123def456
)- Type TXT
- Name _acme-challenge
- Value 66abcdefgabcdefgabcdefgabcdefg66
- TTL 15 minutes
- 列举Cloudflare这个DNS服务商的配置案例:
sudo certbot certonly --dns-cloudflare -d example.com
根据DNS服务商,重新获取证书- 重启 Web 服务器以使更改生效:
- Apache:
sudo systemctl restart apache2
- Nginx:
sudo systemctl restart nginx
- Apache:
- 使用 SSL 测试工具(例如 SSL Labs)测试您的 SSL 证书的安装。确保您的网站通过所有测试,并且显示为安全
前后端开发扩展介绍
浏览器网页:HTML浏览器页面布局,CSS页面样式控制,JavaScript交互逻辑控制
Android和iOS开发,与网页类似,不同点:
- 页面布局使用的方式和交互所使用的技术不一样,如:可以通过拖拽的方式完成页面构建
- 拥有事件,可以为元素绑定事件,在事件回调中,对其他元素作动态控制,如颜色修改、位置修改等
- 但是所使用的语言不一样,Android开发使用Java或者Kotlin,iOS开发使用Object-c或者swift,开发逻辑一样,使用的语言不一样
- 对于编程语言,很多地方是相通的,如数据类型、变量声明、面向对象等;掌握其中一门,再去学另一门相对简单
- 小程序开发与网页开发极为相似,也是使用JavaScript交互
前端开发技术链:页面+元素+元素样式控制+事件绑定+动态交互
互联网产品,大多数情况下都需要后端支持
- 从服务端获取数据
- 本地用户的数据,往服务端提交/存入,并处理
后端开发技术链:
- 操作系统 Windows(上手无难度)、Linux(开源+免费+性能相对好);
- 跨地域部署远端系统,如:需要海外IP办事
- web服务:http协议,是互联网使用的最广泛的协议之一,前后端交互使用的最广泛协议之一
- 支持80端口监听的软件:nginx、apache、tomcat、iis、nodejsserver(本技术),支持接收http请求报文
- 报文接收后,根据报文分析,返回不同的结果(如路由规则,根据不同的路径返回不同的结果),此时需要编程语言,对报文作动态处理
- 支持http报文处理并响应的编程语言包括:JavaScript、Java、python、PHP、ASP、Go、Ruby
- 编程语言在操作或处理报文过程中,有对数据作持久化管理的需求,则需要使用到数据库服务
- 各种数据库以及默认监听端口:
MongoDB 27017
、MySQL 3306
、SQL server 1433
、Oracle 1521
、PostgreSQL 5432
- 缓存服务:
memcache 11211
、redis 6379
- 缓存服务也能对数据作增删改查操作,但介质不一样,介质是内存ram;而数据库在管理数据时,使用的介质是硬盘rom
- 硬盘速度慢于内存,可以将使用频繁的热数据,存到redis中,当用户发起请求,需要对数据快速获取时,先去redis中确认数据,优先返回客户端,提高效率
- 操作系统 Windows(上手无难度)、Linux(开源+免费+性能相对好);
宏观层面下,各种前后端技术,可以灵活搭配使用
- 但在实际使用过程中,某些技术的使用组合,产生的问题相对少一点,运行更为稳定,效果比较好
- 例如
tomcat配java配mysql
以及nginx配PHP配mysql
、NodejsServer配JavaScript配MongoDB
的组合 - 以上效果比较好的组合,称为
最佳实践
,但并不固定,可以灵活搭配
- 例如
- 但在实际使用过程中,某些技术的使用组合,产生的问题相对少一点,运行更为稳定,效果比较好
目的:拓宽前后端开发认识,拓宽技术栈/关系链路认识,降低学习新技术的困难
下图:前后端开发技术链,左前端,右后端
接下来学什么?
- 前后端通信必学技术:ajax
- 解决回调地狱问题,降低异步编程的难度:JavaScript中的promise
- 无论使用vue框架还是react,都需要axios发送ajax请求
- webpack(vite)提高开发效率,优化并打包前端项目
- 完成上述技术栈,就可以学习主流框架,如vue、react、angular、typescript(不是框架,是JavaScript的超集)
- 主流框架完毕,到各种小程序,再到原生应用,再到第三方工具库
- 原生应用包括 uni-app、flutter、reactNavite
- 第三方工具库包括 three.js、echarts
- 快速学习方法:Focus Feedback Fixit
- 三F原则:Focus专注 + Feedback反馈 + Fixit纠正
- 专注提高效率、及时看到所作事情的反馈、及时纠正错误积累经验扎实所学