Session是一种非常重要非常流行的用户认证
与授权
的方式。
认证
:让服务器知道你是是谁
授权
:让服务器知道你什么能干什么不能干
JWT
,最大的优势就在于可以主动清除session
了(因为session
是保存在服务端的,服务端可以主动清除;JWT
是以Token
形式保存在客户端,只要没过期,客户端就可以一直拿着Token
来进行用户认证与授权)session
保存在服务器端,相对较为安全cookie
使用,较为灵活,兼容性较好cookie+session
在跨域
场景表现并不好(cookie具有不可跨域性)多机共享
session机制cookie
的机制很容易被CSRF
(CSRF
是跨站请求伪造
,一种攻击,它可以用你的cookie
进行攻击)session
信息可能会有数据库查询操作(想要拿到完整的session
信息还需要拿session_id去查询数据库,查询就需要时间和计算能力,这就会带来一定的性能问题。)服务器端
,相对安全客户端
,并且不是很安全JSON
对象进行安全传输JWT分成了三个部分,每个部分都有黑点隔开
Header本质是个JSON,这个JSON里面有2个字段
Header编码前后
Playload编码前后
Signature算法
Signature
= HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
生成完签名之后依然需要进行Base64编码
客户端(浏览器)通过POST请求
将用户名和密码传给服务器,服务端对用户名和密码进行核对,核对成功后将用户ID等其他信息作为JWT
的有效载荷
,将其与头部进行base64编码
后形成一个JWT
,然后后端将那一段字符串作为登录成功这个请求的返回结果返回给前端,然后前端将其保存在localStorage
或者sessionStorage
中。
之后前端每次请求都会把JWT字符串作为Http
头里面的Authorization(鉴权)
,然后发送给后端,后端检查其是否存在,如果存在则验证JWT
字符串的有效性(例如签名是否正确,令牌是否过期等)。
验证通过后,后端则使用JWT中包含的用户信息进行其他业务逻辑并返回相应的结果。
JWT可以无缝接入水平拓展
,因为基于Token
(令牌)的身份验证是无状态
的,所以不需要在session
中存储用户信息,应用程序可以轻松拓展,可以使用Token从不同的服务器中访问资源,而不用担心用户是否真的登录在某台服务器上。
这两种都是会受到攻击的。
RESTful
要求程序是无状态
的,像session
这种是有状态
的认证方式,显然是不能做RESTful API的。
客户端向服务端发出请求的时候,可能会有大量的用户信息在JWT
中,每个Http请求
都会产生大量的开销;如果用session的话只要少量的开销就可以了, 因为session_id
非常小,JWT的大小可能是它的好几倍。
但是session_id
也有缺点,查询完整信息需要session_id
,这也是要消耗性能的;JWT字符串包含了完整信息,JWT就不需要数据库查询,性能消耗就少一点,JWT相当于用空间交换时间
JWT
的时效性要比session
差一点。因为JWT
只有等到过期时间才可以销毁,无法实时更新,session
可以在服务端主动手动销毁。
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,添加密码这个字段。
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; }
登录这个动作不属于用户增删改查的任何一种,可以模仿github采用POST+动词
形式
首先在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)
效果演示:
在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.status._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);
npm i koa-jwt --save
这是一个第三方中间件,功能强大。有了这个中间件,我们就不需要自己编写中间件了。
引入中间件,只需一行代码就可以替换掉自己编写的认证中间件。
const jwt = require('koa-jwt'); const auth = jwt({ secret });
koa-jwt
同样将用户信息存放在ctx.state.user
上,自定义授权中间件依然能正常使用。