JSON Web Token (JWT)

前后端分离通过Restful API进行数据交互时,如何验证用户的登录信息及权限。在原来的项目中,使用的是最传统也是最简单的方式,前端登录,后端根据用户信息生成一个token,并保存这个 token 和对应的用户id到数据库或Session中,接着把 token 传给用户,存入浏览器 cookie,之后浏览器请求带上这个cookie,后端根据这个cookie值来查询用户,验证是否过期。

但这样做问题就很多,如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户 token 泄露,而作为后端识别用户的标识,cookie 的泄露意味着用户信息不再安全。尽管我们通过转义输出内容,使用 CDN 等可以尽量避免 XSS 注入,但谁也不能保证在大型的项目中不会出现这个问题。

在设置 cookie 的时候,其实你还可以设置 httpOnly 以及 secure 项。设置 httpOnly 后 cookie 将不能被 JS 读取,浏览器会自动的把它加在请求的 header 当中,设置 secure 的话,cookie 就只允许通过 HTTPS 传输。secure 选项可以过滤掉一些使用 HTTP 协议的 XSS 注入,但并不能完全阻止。

httpOnly 选项使得 JS 不能读取到 cookie,那么 XSS 注入的问题也基本不用担心了。但设置 httpOnly 就带来了另一个问题,就是很容易的被 XSRF,即跨站请求伪造。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容。因为 cookie 默认被发了出去。

另外,如果将验证信息保存在数据库中,后端每次都需要根据token查出用户id,这就增加了数据库的查询和存储开销。若把验证信息保存在session中,有加大了服务器端的存储压力。那我们可不可以不要服务器去查询呢?如果我们生成token遵循一定的规律,比如我们使用对称加密算法来加密用户id形成token,那么服务端以后其实只要解密该token就可以知道用户的id是什么了。不过呢,我只是举个例子而已,要是真这么做,只要你的对称加密算法泄露了,其他人可以通过这种加密方式进行伪造token,那么所有用户信息都不再安全了。恩,那用非对称加密算法来做呢,其实现在有个规范就是这样做的,就是我们接下来要介绍的 JWT。

Json Web Token(JWT)
JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:

简洁(Compact)

可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快

自包含(Self-contained)

负载中包含了所有用户所需要的信息,避免了多次查询数据库
Header 头部
头部包含了两部分,token 类型和采用的加密算法
Payload 负载
这部分就是我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。
Signature 签名
前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。

三个部分通过.连接在一起就是我们的 JWT 了,它可能长这个样子,长度貌似和你的加密算法和私钥有关系。

首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形同lll.zzz.xxx的字符串。后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

参考 https://docs.aws.amazon.com/zh_cn/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html

图片上加一划而过的 闪光

[cc lang=”css”].img {
display:block;
position: relative;
width:800px;
height:450px;
margin:0 auto;
}
.img:before{
content: “”;
position: absolute;
width:200px;
eight: 100%; top: 0;
left: -150px;
overflow: hidden;
//background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(50%, rgba(255,255,255,.2)), color-stop(100%, rgba(255,255,255,0)));

background:linear-gradient(left, rgba(255,255,255,0)0, rgba(255,255,255,.2)50%, rgba(255,255,255,0)100%);
transform: skewX(-25deg);

}

.img:hover:before {
left: 150%;
transition: left 1s ease 0s;
}[/cc]

[cc lang=”html”]

[/cc]

async/await/promise/ setTimeout/ event loop

promise:
[cc lang=”javascript”]
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error(‘fail’)), 3000)
})

const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})

p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
[/cc]
上面代码中,p1是一个 Promise,3 秒之后变为rejected。p2的状态在 1 秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。

!!! 注意,调用resolve或reject并不会终结 Promise 的参数函数的执行。如下
[cc lang=”js”]
new Promise((resolve, reject) => {
resolve(1); //这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
如果加上return语句就不会打印2
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
[/cc]
promise then 方法
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
then里的 回调函数完成以后,会将返回结果作为参数,传入下一次回调函数。如果回调函数返回promise,这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用
promise catch 方法
Promise.prototype.catch方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数.
建议用catch 不要用then 第二个方法
!!! catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法

promise.all([ p1, p2, p3 ]).then().catch

promise.race([ p1, p2, p3]).then().catch
那个率先改变的 Promise 实例的返回值,就传递给then或catch的回调函数
[cc lang=”js”]
const p = Promise.race([
fetch(‘/resource-that-may-take-a-while’),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error(‘request timeout’)), 5000)
})
]);

p
.then(console.log)
.catch(console.error);
//上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数

[/cc]

1.async 函数返回的是一个 Promise 对象,async 函数没有返回值,它会返回 Promise.resolve(undefined)

2 await 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
如果它等到的是一个 Promise 对象,await就忙起来了,它会阻塞asycn函数内后面的代码,等着Promise对象resolve,然后得到 resolve 的值,作为 await 表达式的运算结果
3。1 js 执行机制 event loop
首先判断JS是同步还是异步,同步就进入主进程,异步就进入event table
异步任务在event table中注册函数,当满足触发条件后,被推入event queue
同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主进程中
3.2event loop (2)
macro-task(宏任务):包括整体代码script,setTimeout,setInterval
micro-task(微任务):Promise,process.nextTick

!! 执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里
当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完
[cc lang=”javascript”]
async function async1() {
console.log( ‘async1 start’ )
await async2() // return promise 此时返回的Promise会被放入到回调队列中等待,await会让出线程
//等外面执行完成后再回到这生成promise.resolve(undefined) 又被放到micro-task, 再次让出线程跳出async
console.log( ‘async1 end’ )
}

async function async2() {
console.log( ‘async2’)
}

console.log( ‘script start’ )

setTimeout( function () {
console.log( ‘setTimeout’ )
}, 0 )

async1();

new Promise( function ( resolve ) {
console.log( ‘promise1’ )
resolve(); // 回调then 会放入micro-task 让出线程
console.log(34)
} ).then( function () {
console.log( ‘promise2’ )
return Promise.resolve();
} ).then(function(){console.log(11)
return Promise.resolve();
}).then(function(){console.log(22)})

console.log( ‘script end’ )

//script start
//async1 start
//async2
//promise1
// 34
//script end
//promise2
//async1 end
//11
//22
// setTimeout
[/cc]