Lesson-1 Session
Session是什么
其实就是用户的认证与授权。认证与授权又是什么?认证,就是让服务器知道你是谁,授信,就是让服务器知道你的权限是什么,什么能干,什么不能干
工作原理
以登陆为例,当客户端通过用户名与密码请求服务端,服务端就会生成身份认证相关的Seccion数据。生成Session数据之后,可能保存在内存里,也可能保存在内存数据库里(比如redis),并将Session ID返回给客户端(比如请求头里添加Set-Cookie:session=***)。客户端将Session ID存放到cookie里。接下来客户端的所有请求,都将附带该Session ID,服务端通过该Session ID来查找该用户相关的数据
Session 的优势
- 相比 JWT,最大的优势就在于可以主动清楚session了
- session 保存在服务器端,相对较为安全
- 结合 cookie 使用,较为灵活,兼容性较好(客户端服务端都可以清除,也可以加密)
Session 的劣势
- cookie+session 在跨域场景表现并不好(不可跨域,domain变量,需要复杂处理跨域)
- 如果是分布式部署,需要做多机共享 Session 机制(成本增加)
- 基于 cookie 的机制很容易被 CSRF
- 查询 Session 信息可能会有数据库查询操作
Session 相关的概念介绍
- session::主要存放在服务器,相对安全
- cookie:主要存放在客户端,并且不是很安全
- sessionStorage:仅在当前会话下有效,关闭页面或浏览器后被清除
- localstorage:除非被清除,否则永久保存
Lesson-2 JWT 简介
什么是 JWT?
- JSON Web Token 是一个开放标准(RFC[请求意见稿] 7519)
- 定义了一种紧凑且独立的方式,可以将各方面之间的信息作为 JSON 对象进行安全传输
- 该信息可以验证和信任,因为是经过数字签名的
JWT 的构成
- 头部(Header)
- 有效载荷(Payload)
- 签名(Signature)
JWT 的例子
不同颜色,. 号结束,红色代表Header,紫色代表Payload,蓝色代表Signature
Header
Header,本质是JSON,使用 Base64 编码,因此更加紧凑。Header 包含下面两个字段:
- typ:token的类型,这里固定为 JWT
- alg:使用 hash 算法,例如 HMAC SHA256 或者 RSA
Header 编码前后
编码前:{"alg": "HS256", "typ": "JWT"}
编码后:'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9'
Payload
- 存储需要传递的信息,如用户ID、用户名等
- 还包含元数据,如过期时间、发布人等
- 与 Header 不同,Payload 可以加密
Payload 编码前后
编码前:{"user_id": "zhangsan"}
编码后: 'eyJ12VylkIjoiemhhbmdzYW4ifQ=='
由于base64会忽略最后的等号,所以结果为: 'eyJ12VylkIjoiemhhbmdzYW4ifQ'
Signature
- 对 Header 和 Payload 部分进行签名
- 保证 Token 在传输的过程中没有被篡改或者损坏
Signature 算法
Signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret),生成完之后依然需要进行一次 base64 编码
JWT 原理
以登录为例,浏览器端通过 post 请求将用户名和密码发送给服务端,服务端接受完进行核对,核对成功后将用户 ID 和其他信息作为有效载荷(Payload),将其与头部进行 base64 编码之后,形成一个 JWT。服务端将该 JWT 作为登录成功的返回结果,返回给浏览器端,浏览器端将其保存在 localstorage 或 sessionStorage 中。接下来的每次请求,都将带上该 JWT (请求头中,Authorization: Bearer*** JWT ***),服务端接收后都将核对(身份,令牌是否过期等),并返回相关的用户信息
Lesson-3 JWT vs Session
- 可拓展性(水平拓展:加服务器,垂直拓展:增加硬盘容量,内存等,JWT优胜)
- 安全性(两者均有缺陷)
- XSS攻击(跨站脚本攻击):JS均能篡改;防范:签名/加密,敏感信息不要放其中;
- CSRF(跨站请求伪造):两者都能被篡改
- 重犯攻击:过期时间尽量短
- 中间人攻击:HTTPS来解决
- RESTful API,JWT优胜,因为RESTful API提倡无状态,JWT符合要求
- 性能(各有利弊,因为JWT信息较强,所以体积也较大。不过Session每次都需要服务器查找,JWT信息都保存好了,不需要再去查询数据库)
- 时效性,Session能直接从服务端销毁,JWT只能等到时效性到了才会销毁(修改密码也无法阻止篡夺者的使用)
Lesson-4 在 Nodejs 中使用 JWT
操作步骤
- 安装 jsonwebtoken
- 签名
- 验证
这一节是直接在 git 中使用命令行来操作代码片段的
安装 jsonwebtoken
执行 npm i jsonwebtoken
进行安装插件
签名
执行 node
,进入 node 环境
执行 jwt = require('jsonwebtoken');
引入jwt
执行 token = jwt.sign({name: 'yose'}, 'secret');
生成token,secret 则代表密钥,后面用于验证使用的
执行 jwt.decode(token);
直接解码,但是并不会验证,所以并不会用
执行 jwt.verify(token, 'secret');
验证密钥并解码,可以看到返回 { name: 'yose', iat: 1565110602 }
iat代表的是签名时的时间,单位毫秒
验证
执行 jwt.verify(token, 'secret1');
密钥被篡改,返回 JsonWebTokenError: invalid signature
,密钥校验失败
执行 jwt.verify(token.replace('e', 'a'), 'secret');
签名篡改,返回 JsonWebTokenError: invalid token
,令牌校验失败
Lesson-5 实现用户注册
操作步骤
- 设计用户 Schema
- 编写保证唯一性的逻辑
设计用户 Schema
userSchema 新增 password 字段,来表示用户注册的密码
// model/users.js
const userSchema = new Schema({
name: { type: String, required: true },
password: { type: String, required: true },
});
调用postman,但是这个时候会把密码也给返回出来,这是不合理的,所以我们需要把密码设置为不返回
这里有两种做法
一种是通过 mongoose 中 Query.prototype.select() 方法,由于是 Query对象,所以不是所有方法都能直接链式调用该方法来屏蔽关键字段,也就是说前面类似 create 中 save() 方法是不能用的。
这里简化学习直接用find()方法演示
// controllers/users.js
async find (ctx) {
ctx.body = await User.find().select("-password");
}
第二种方法,通过 Schma 属性 属性来过滤(提倡,因为你不用去关注所使用的方法返回的究竟是不是 Query对象)
// models/users.js
const userSchema = new Schema({
__v: { type: Number, select: false },
name: { type: String, required: true },
password: { type: String, required: true, select: false },
});
结果依然如上面 postman 截图所示,拿到的用户列表并不会返回 password 字段
编写保证唯一性的逻辑
真实场景中,我们经常会先对用户名、手机、邮箱等等进行唯一性的验证,如果已经被人注册过了,那么就不能继续使用这些数据进行注册,否则用户可以正常注册
对到修改用户,因为真实场景中用户可能只是更改用户名,但其他信息不更换,那么根据 RESTful API 规范,我们需要将 put(全部更改) 方法改成 patch(局部更改)
// routes/users.js
// 修改特定用户
router.patch('/:id', update); // 将put 改为 patch
create方法新增去重,并对密码进行校验(实际是为了报错信息友好),而更新则需要将校验全部改为非必传(因为可以进行局部更改)
async create (ctx) {
ctx.verifyParams({
name: { type: 'string', required: true },
password: { type: 'string', required: true }
});
// 查重
const { name } = ctx.request.body;
const requesteUser = await user.findOne({ name });
if(requesteUser) ctx.throw(409, '用户已经存在');
// save方法,保存到数据库。并根据 RESTful API最佳实践,返回增加的内容
const user = await new User(ctx.request.body).save();
ctx.body = user;
}
async update (ctx) {
ctx.verifyParams({
name: { type: 'string', required: false },
password: { type: 'string', required: false }
});
// findByIdAndUpdate,第一个参数为要修改的数据id,第二个参数为修改的内容
const user = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body);
if(!user) ctx.throw(404, '用户不存在');
ctx.body = user;
}
Lesson-6 实现登陆并获取Token
操作步骤
- 登陆接口设计
- 用 jsonwebtoken 生成 token
登陆接口设计
根据 github 接口设计,由于登陆并不属于增删改查,所以我们按照github上这种教科书级别的接口设计规范来设计,采用 post+动词形式来定义,路由新增login,控制器里添加login方法
// router/users.js
const { find, findById, create, update, delete: del, login } = require('../controllers/users');
// 登陆
router.post('/login', login);
用 jsonwebtoken 生成 token
设计思路:login 方法先对数据进行参数校验,如果必传字段都存在,再去数据库中查询是否有符合请求体中的用户名及密码。全部验证通过了,再生成token。这里的密钥是直接写在 config.js 中的,但正常情况下其实密钥是必须通过环境变量来获取的,否则这个密钥被人家一扒就拿到了
// config.js
module.exports = {
secret: 'zhihu-jwt-secret', // 正常需要通过环境变量获取
}
// controllers/users.js
const jsonwebtoken = require('jsonwebtoken');
const { secret } = require('../config');
async login (ctx) {
ctx.verifyParams({
name: { type: 'string', required: true },
password: { type: 'string', required: true }
});
const user = await User.findOne(ctx.request.body);
if(!user) ctx.throw(401, '用户名或密码不正确');
const { _id, name } = user;
const token = jsonwebtoken.sign({ _id, name }, secret, { expiresIn: '1d' });
ctx.body = { token };
}
加个小插曲,新建请求,全都编写好后,发现一直不通过,总提示两个字段都为空,检查后才发现这里默认新生成的是text类型,注意改回JSON格式,由于习惯复制粘贴,这里一直没设置,容易忘记设置这个地方
Lesson-7 自己编写 Koa 中间件实现用户认证与授权
操作步骤
- 认证:验证 token,并获取用户信息
- 授权:使用中间件保护接口
认证:验证 token,并获取用户信息
前面已经实现了用户登录并返回token,接下来便是在 postman 中使用自动化脚本来获取token,否则每次都去填,这是不可行的
postman中是可以在请求头中自动加入验证信息的,这里我们用的是Bearer Token,将登陆后的 token 填入即可。但也如上面所说,每次都去填是不可行的(更换密钥/过期/用户更改登陆信息等)。截图中写了 {{token}},其实代表的就是自动化脚本中的全局变量
var jsonData = pm.response.json();
pm.globals.set("token", jsonData.token);
以上便完成了自动化脚本获取 token 的操作,现在我们不用再去复制 token 来手动加到请求头里了。
授权:使用中间件保护接口
根据一开始的 洋葱模型,其实我们所要做的验证token,实际就是编写一个中间件,加进需要验证的控制器中
// router/users.js
const jsonwebtoken = require('jsonwebtoken');
const { secret} = require('../config');
// 认证中间件
const auth = async (ctx, next) => {
const { authorization = '' } = ctx.request.header; // 容错,没token得用户默认为空字符串,否则报语法错误
const token = authorization.replace('Bearer ', ''); // 根据上面截图,可以看到需要对value进行处理
try {
const user = jsonwebtoken.verify(token, secret); // 不记得的往前看第四节
ctx.state.user = user; // 约定俗成,一般就是放这里,也是没有为什么
} catch (err) {
ctx.throw(401, err.message);
}
await next();
}
用户操作中,关于修改与删除(实际并不存在删除,所谓删除其实只会把数据库中的这一条数值设置为非激活状态,现实当中是不会去删除数据库数据的),是必须要验证用户权限的,否则当前用户能修改别人的信息是不合理的,所以需要对用户的权限进行校验,思路也很简单,检查一下用户的id跟要修改的用户数据id是否一致即可
// controllers/user.js
async checkOwner (ctx, next) {
if (ctx.params.id !== ctx.state.user._id) ctx.throw('403', '没有权限');
await next();
}
// routes/users.js
// 修改特定用户
router.patch('/:id', auth, checkOwner, update);
// 删除用户
router.delete('/:id', auth, checkOwner, del);
注意一点,自动化脚本只能写在登陆上,其他接口不能写,否则其他接口也会去获取写入全局token,这样会导致token为空~
Lesson-8 用 koa-jwt 中间件实现用户认证与授权
上一节自己编写的只是用来了解原理,实际操作肯定还是用人家造好的轮子的。就像一开始字段的验证一样,尽量使用社区中优秀的中间件
操作步骤
- 安装 koa-jwt
- 使用中间件保护接口
- 使用中间件获取用户信息
安装 koa-jwt
执行命令 npm i koa-jwt --save
使用中间件保护接口
// routes/users.js
const jwt = require('koa-jwt');
// 认证中间件
const auth = jwt({ secret }); // 原来的auth整段代码变成一句,简洁
使用中间件获取用户信息
由于 koa-jwt 自带 jsonwebtoken ,所以并不需要我们额外再去引入 jsonwebtoken 来解析 token 获取用户资料,所以没代码。