Session简介
Session是一种非常重要非常流行的用户认证
与授权
的方式。
认证
:让服务器知道你是是谁
授权
:让服务器知道你什么能干什么不能干
Session的优势
- 相比
JWT
,最大的优势就在于可以主动清除session
了(因为session
是保存在服务端的,服务端可以主动清除;JWT
是以Token
形式保存在客户端,只要没过期,客户端就可以一直拿着Token
来进行用户认证与授权) session
保存在服务器端,相对较为安全- 结合
cookie
使用,较为灵活,兼容性较好
session的劣势
cookie+session
在跨域
场景表现并不好(cookie具有不可跨域性)- 如果是分布式部署,需要做
多机共享
session机制 - 基于
cookie
的机制很容易被CSRF
(CSRF
是跨站请求伪造
,一种攻击,它可以用你的cookie
进行攻击) - 查询
session
信息可能会有数据库查询操作(想要拿到完整的session
信息还需要拿session_id去查询数据库,查询就需要时间和计算能力,这就会带来一定的性能问题。)
Session相关的概念介绍
- session:主要存放在
服务器端
,相对安全 - cookie:主要存放在
客户端
,并且不是很安全 - sessionStorage:仅在当前会话下有效,关闭页面或浏览器后被清除
- localstorage:除非被清除,否则永久保存
JWT简介
什么是JWT?
- JSON Web Token 是一个开放标准(RFC 7519)
- 定义了一种紧凑且独立的方式,可以将各方之间的信息作为
JSON
对象进行安全传输 - 该信息可以验证和信任,因为是经过数字签名的
JWT的构成
- 头部(Header)
- 有效载荷(Payload)
- 签名(Signature)
JWT分成了三个部分,每个部分都有黑点隔开
Header
Header本质是个JSON,这个JSON里面有2个字段
- typ:token的类型,这里固定有JWT
- alg:使用的hash算法,例如:HMAC SHA256或者RSA
Header编码前后
- {“alg”:“HS256”,"typ":"JWT"}
- 编码后就是一段Base64字符串
Payload
- 存储需要传递的信息,如用户ID、用户名等
- 还包含元数据,如过期时间、发布人等
- 与Header不同,Playload可以加密
Playload编码前后
- {“user_id”:"xiaofengche"}
- 编码后就是一段Base64字符串
Signature
- 对Header和Payload部分进行签名
- 保证Token在传输的过程中没有被篡改或者损坏
Signature算法
Signature
= HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
生成完签名之后依然需要进行Base64编码
JWT工作原理
客户端(浏览器)通过POST请求
将用户名和密码传给服务器,服务端对用户名和密码进行核对,核对成功后将用户ID等其他信息作为JWT
的有效载荷
,将其与头部进行base64编码
后形成一个JWT
,然后后端将那一段字符串作为登录成功这个请求的返回结果返回给前端,然后前端将其保存在localStorage
或者sessionStorage
中。
之后前端每次请求都会把JWT字符串作为Http
头里面的Authorization(鉴权)
,然后发送给后端,后端检查其是否存在,如果存在则验证JWT
字符串的有效性(例如签名是否正确,令牌是否过期等)。
验证通过后,后端则使用JWT中包含的用户信息进行其他业务逻辑并返回相应的结果。
JWT vs. Session
- 可扩展性
JWT可以无缝接入水平拓展
,因为基于Token
(令牌)的身份验证是无状态
的,所以不需要在session
中存储用户信息,应用程序可以轻松拓展,可以使用Token从不同的服务器中访问资源,而不用担心用户是否真的登录在某台服务器上。
- 安全性
这两种都是会受到攻击的。
- RESTful API
RESTful
要求程序是无状态
的,像session
这种是有状态
的认证方式,显然是不能做RESTful API的。
- 性能
客户端向服务端发出请求的时候,可能会有大量的用户信息在JWT
中,每个Http请求
都会产生大量的开销;如果用session的话只要少量的开销就可以了, 因为session_id
非常小,JWT的大小可能是它的好几倍。
但是session_id
也有缺点,查询完整信息需要session_id
,这也是要消耗性能的;JWT字符串包含了完整信息,JWT就不需要数据库查询,性能消耗就少一点,JWT相当于用空间交换时间
- 时效性
JWT
的时效性要比session
差一点。因为JWT
只有等到过期时间才可以销毁,无法实时更新,session
可以在服务端主动手动销毁。
在Node.js中使用JWT
- 安装jsonwebtoken
npm i jsonwebtoken
- 签名
在终端进入 node
命令行,引入jwt
使用sign
方法进行签名,它的第一个参数是JSON对象,第二个参数可以写密钥
> token = jwt.sign({name:"xiaofengche"},'secret');
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoieGlhb2ZlbmdjaGUiLCJpYXQiOjE2Mjg0MDYzMzF9.zOCf0dzBRvuBjOCcZZ5nuLbUGd4q05SQuFod48ScML4'
拿到token之后就可以传给客户端,客户端每次请求都可以拿着这个token放在头部传回给服务端,服务端拿到token之后就可以判断当前用户是谁了,有什么权限。
- 验证
使用解码 decode
就可以判断用户是谁
> jwt.decode(token);
{ name: 'xiaofengche', iat: 1628406331 }
这里的iat
是指签名时的时间,单位是秒
需要验证
用户信息是否被篡改,verify
第二个参数是要加密钥的
> jwt.verify(token,'secret');
{ name: 'xiaofengche', iat: 1628406331 }
证明token是合法的,签名也是合法的
实现用户注册
- 设计用户Schema
需要重新设计Schema,添加密码这个字段。
const mongoose = require('mongoose');
//mongoose提供的Schema类生成文档Schema
const { Schema,model } = mongoose
const userSchema = new Schema({
//将没用的信息隐藏起来
__v:{type:Number,select:false},
//required表示这个属性是必选的
//default可以设置默认值
name:{type:String,required:true},
//像密码这种敏感信息不应该随便暴露,需要将其隐藏起来——select:false
password:{type:String,required:true,select:false},
});
//建立模型
//User:为文档集合名称
module.exports = model('User',userSchema);
在相关操作添加新字段的定义
//创建用户
async create(ctx){
//校验请求体的name位字符串类型并且是必选的
ctx.verifyParams({
//必选:required 删掉也是默认为true
name:{ type:'string',required:true },
password:{type:'string',required:true},
});
const user = await new User(ctx.request.body).save();
ctx.body = user;
}
//更新用户
async update(ctx){
ctx.verifyParams({
//必选:required 删掉也是默认为true
name:{ type:'string',required:false },
password:{type:'string',required:false},
});
const user = await User.findByIdAndUpdate(ctx.params.id,ctx.request.body);
if(!user){ctx.throw(404,'用户不存在');}
ctx.body = user;
}
由于修改用户属性可以部分修改,所以需要修改更改路由的请求方法
//put是整体替换,现在的用户可以更新一部分属性
router.patch('/:id',update);
- 编写保证唯一性的逻辑(用户的唯一性)
在创建用户编写保证唯一性的逻辑,保证创建时用户名不重复
//更新用户
async update(ctx){
ctx.verifyParams({
//必选:required 删掉也是默认为true
name:{ type:'string',required:false },
password:{type:'string',required:false},
});
//获取请求体中的用户名
const {name} = ctx.request.body
// findOne返回符合条件的第一个用户
const repreatedUser = await User.findOne({name});
//如果有重复用户返回409状态码代表冲突
if(repreatedUser){
ctx.throw(409,"用户名已占用");
}
const user = await User.findByIdAndUpdate(ctx.params.id,ctx.request.body);
if(!user){ctx.throw(404,'用户不存在');}
ctx.body = user;
}
实现登录并获取token
- 登录接口设计
登录这个动作不属于用户增删改查的任何一种,可以模仿github采用POST+动词
形式
- 用jsonwebtoken生成token
首先在config.js配置密钥
secret:'jwt-secret',
在users.js引入jsonwebtoken和密钥,接着实现登录接口
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,'用户名或密码不正确');}
//获取id和name
const {_id,name} = user;
//登录成功生成token,参数分别为用户不敏感的信息,签名密钥,过期时间
//1d:一天
const token = jsonwebtoken.sign({_id,name},secret,{expiresIn:'1d'});
ctx.body = {token};
}
最后别忘了在routes->users.js注册接口
//delete是关键字,取别名
const {find,findById,create,update,delete:del,login} = require('../controllers/users');
router.post('/login',login)
效果演示:
自己编写Koa中间件实现用户认证与授权
- 认证:验证token,并获取用户信息
在routes->users.js编写认证中间件。
假设客户端是通过Authorization字段 加上Bearer 空格+token这种形式把token传进来的,我们就知道怎么获取token了
const jsonwebtoken = require('jsonwebtoken');
const {secret} = require('../config');
const auth = async(ctx,next) => {
//当不设置authorization的时候把它设置为空字符串
const {authorization = ''} = ctx.request.header;
//去掉'Bearer '才是我们真正想要的token
const token = authorization.replace('Bearer ','');
//验证用户信息
try{
const user = jsonwebtoken.verify(token,secret);
ctx.state.user = user;
}catch(err){
//所有的验证失败手动抛成401错误,也就是未认证
ctx.throw(401,err.message);
}
await next();
}
最后把中间件放在需要认证的接口上
router.patch('/:id', auth,update);
router.delete('/:id',auth,del);
- 授权:使用中间件保护接口
在users.js控制器中编写鉴权中间件(也可以像上面一样在routes->users.js里面)
async checkOwner(ctx,next){
//判断当前修改或删除的用户id是不是当前登录用户的id
if(ctx.params.id !== ctx.state._id){
//操作的对象不是自己就抛出错误
ctx.throw(403,'没有权限')
}
await next();
}
最后把中间件添加到需要鉴权的接口上
const {find,findById,create,update,delete:del,login,checkOwner} = require('../controllers/users');
router.patch('/:id', auth,checkOwner,update);
router.delete('/:id',auth,checkOwner,del);
用koa-jwt中间件实现用户认证与授权
- 安装koa-jwt:
npm i koa-jwt --save
这是一个第三方中间件,功能强大。有了这个中间件,我们就不需要自己编写中间件了。
- 使用中间件保护接口
引入中间件,只需一行代码就可以替换掉自己编写的认证中间件。
const jwt = require('koa-jwt');
const auth = jwt({ secret });
- 使用中间件获取用户信息
koa-jwt
同样将用户信息存放在ctx.state.user
上,自定义授权中间件依然能正常使用。