Nodejs

Section-7 JWT 在 Koa 框架中实现用户的认证与授

2019-08-07  本文已影响0人  羽晞yose

Lesson-1 Session

Session是什么

其实就是用户的认证与授权。认证与授权又是什么?认证,就是让服务器知道你是谁,授信,就是让服务器知道你的权限是什么,什么能干,什么不能干

工作原理

以登陆为例,当客户端通过用户名与密码请求服务端,服务端就会生成身份认证相关的Seccion数据。生成Session数据之后,可能保存在内存里,也可能保存在内存数据库里(比如redis),并将Session ID返回给客户端(比如请求头里添加Set-Cookie:session=***)。客户端将Session ID存放到cookie里。接下来客户端的所有请求,都将附带该Session ID,服务端通过该Session ID来查找该用户相关的数据

Session 的优势

Session 的劣势

Session 相关的概念介绍


Lesson-2 JWT 简介

什么是 JWT?

JWT 的构成

JWT 的例子

不同颜色,. 号结束,红色代表Header,紫色代表Payload,蓝色代表Signature


JWT 例子

Header

Header,本质是JSON,使用 Base64 编码,因此更加紧凑。Header 包含下面两个字段:

Header 编码前后

编码前:{"alg": "HS256", "typ": "JWT"}
编码后:'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9'

Payload

Payload 编码前后

编码前:{"user_id": "zhangsan"}
编码后: 'eyJ12VylkIjoiemhhbmdzYW4ifQ=='
由于base64会忽略最后的等号,所以结果为: 'eyJ12VylkIjoiemhhbmdzYW4ifQ'

Signature

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


Lesson-4 在 Nodejs 中使用 JWT

操作步骤

安装 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

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() 方法是不能用的。

Query对象文档说明

这里简化学习直接用find()方法演示

// controllers/users.js
async find (ctx) {
    ctx.body = await User.find().select("-password");
}
password被屏蔽

第二种方法,通过 Schma 属性 属性来过滤(提倡,因为你不用去关注所使用的方法返回的究竟是不是 Query对象)

image.png
// 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

操作步骤

登陆接口设计

根据 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格式,由于习惯复制粘贴,这里一直没设置,容易忘记设置这个地方

image.png

Lesson-7 自己编写 Koa 中间件实现用户认证与授权

操作步骤

认证:验证 token,并获取用户信息

前面已经实现了用户登录并返回token,接下来便是在 postman 中使用自动化脚本来获取token,否则每次都去填,这是不可行的
postman中是可以在请求头中自动加入验证信息的,这里我们用的是Bearer Token,将登陆后的 token 填入即可。但也如上面所说,每次都去填是不可行的(更换密钥/过期/用户更改登陆信息等)。截图中写了 {{token}},其实代表的就是自动化脚本中的全局变量

请求头附带token 自动化脚本
var jsonData = pm.response.json();
pm.globals.set("token", jsonData.token);

以上便完成了自动化脚本获取 token 的操作,现在我们不用再去复制 token 来手动加到请求头里了。

授权:使用中间件保护接口

根据一开始的 洋葱模型,其实我们所要做的验证token,实际就是编写一个中间件,加进需要验证的控制器中

postman中自动加入的请求头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

执行命令 npm i koa-jwt --save

使用中间件保护接口

// routes/users.js
const jwt = require('koa-jwt');
// 认证中间件
const auth = jwt({ secret }); // 原来的auth整段代码变成一句,简洁

使用中间件获取用户信息

由于 koa-jwt 自带 jsonwebtoken ,所以并不需要我们额外再去引入 jsonwebtoken 来解析 token 获取用户资料,所以没代码。


不需要再次编写,默认自带
上一篇 下一篇

猜你喜欢

热点阅读