json web token 前后端总结
一. 关于 Token 简介
1. 基于 Token 的 WEB 后端认证机制
几种常见的认证机制 >- HTTP Basic Auth
HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。
- OAuth(开放授权)
OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用;
- Cookie Auth
Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效
- Token Auth
2. Token Auth的优点
Token机制相对于Cookie机制又有什么好处呢?
支持跨域访问:
- Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
- Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
无状态(也称:服务端可扩展行):
- Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
更适用CDN:
- 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
去耦:
- 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
更适用于移动应用:
- 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
CSRF:
- 因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
性能:
- 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
不需要为登录页面做特殊处理:
- 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.
基于标准化:
- 你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).
二. 关于 JWT 三段原理理解
1. JWT的组成
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
- #### 头部(Header) JWT需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{
"typ": "JWT",
"alg": "HS256"
}
在这里,我们说明了这是一个JWT,并且我们所用的签名算法(后面会提到)是HS256算法。
对它也要进行Base64编码,之后的字符串就成了JWT的Header(头部)。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
- #### 载荷(Payload) 我们可以把一些业务需求操作描述成一个JSON对象。其中添加了一些其他的信息,帮助今后收到这个JWT的服务器理解这个JWT。
{
"iss": "John Wu JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "jrocket@example.com",
"from_user": "B",
"target_user": "A"
}
这五个字段都是由JWT的标准所定义的: - iss: 该JWT的签发者 - sub: 该JWT所面向的用户 - aud: 接收该JWT的一方 - exp(expires): 什么时候过期,这里是一个Unix时间戳 iat(issued at): 在什么时候签发的
将上面的JSON对象进行[base64编码]可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。
eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
- #### 签名(签名)
将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0
这一部分的过程在node-jws的源码中有体现
最后,我们将上面拼接完的字符串用HS256算法进行加密。
在加密的时候,我们还需要提供一个密钥(secret)。如果我们用mystar作为密钥的话,那么就可以得到我们加密后的内容
rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
这一部分又叫做签名。
最后将这一部分签名也拼接在被签名的字符串后面,我们就得到了完整的JWT
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
三. 前后端传递出来 Token 流程原理
用户认证八步走
所谓用户认证(Authentication),就是让用户登录,并且在接下来的一段时间内让用户访问网站时可以使用其账户,而不需要再次登录的机制。
小知识: 可别把用户认证和用户授权(Authorization)搞混了。用户授权指的是规定并允许用户使用自己的权限,例如发布帖子、管理站点等。
首先,服务器应用(下面简称“应用”)让用户通过Web表单将自己的用户名和密码发送到服务器的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
接下来,应用和数据库核对用户名和密码。
核对用户名和密码成功后,应用将用户的id(图中的user_id)作为JWT Payload的一个属性,将其与头部分别进行Base64编码拼接后签名,形成一个JWT。这里的JWT就是一个形同lll.zzz.xxx的字符串。
应用将JWT字符串作为该请求Cookie的一部分返回给用户。注意,在这里必须使用HttpOnly属性来防止Cookie被JavaScript读取,从而避免跨站脚本攻击(XSS攻击)。
在Cookie失效或者被删除前,用户每次访问应用,应用都会接受到含有jwt的Cookie。从而应用就可以将JWT从请求中提取出来。
应用通过一系列任务检查JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
应用在确认JWT有效之后,JWT进行Base64解码(可能在上一步中已经完成),然后在Payload中读取用户的id值,也就是user_id属性。这里用户的id为1025。
应用根据用户请求进行响应。
四. 代码 demo 实现
首先我先建立一个 jwt.js 文件来专门生产token。
/**
* @function jwtProducer
* @description jwt的统一生产方法
* @returns {{username: *, token: *}}
*/
const jwt = require('jsonwebtoken');
module.exports = {
/*这里的 username 是 用户的账号+密码*/
jwtProducer: function (username, dbName, userId) {
// JWT Id
const jti = (Math.random() * 100000000000000000).toString();
// JWT 的签发者
const iss = 'localhost';
// 签发时间
const iat = Date.now() / 1000;
// 失效时间
const exp = iat + 7200;
// 用户 ID
const sub = userId;
// 数据库名称
const location = dbName;
const payload = {
jti: jti,
iss: iss,
iat: iat,
exp: exp,
sub: sub,
location: location
};
var token = jwt.sign(payload, 'Nodezhang'); // 'Nodezhang'是自定义的校验码。
return {
operatorUsername: username,
token: token
};
}
};
然后你可以 在你的 login.js 中开始业务代码处理了
const jwtP = require('../model/jwt');
const jwt = require('jsonwebtoken');
/**
* @function lookToken
* @description 检查 jwt token 密码
* @param token
* @param callback
*/
function lookToken(token, callback) {
jwt.verify(token, 'Nodezhang', function (error, payload) { // 检验Token的合法性 用到 检验码
if (error) {
console.log(error);
return callback;
}
callback(null, payload);
});
}
......//省略代码处
var token = req.body.token;
// console.log('This is :'+token);
if (token === undefined || !token) {
return res.status(401).end('invalid token');
}
// 检查 jwt token 密码
lookToken(token, function (error, payload) {
if (error) {
return res.status(402)
.end('invalid token');
}
if (payload.exp < Date.now() / 1000) {
return res.status(405)
.end('expired token');
}
return payload;
});
/*后端验证登录密码正确后调用生产token*/
var tokenData = jwtP.jwtProducer(loginData, 'nodezhang', result[0].id);
result = {
code: 200,
msg: '密码正确',
tokenData: tokenData,
};
前端部分
$.ajax({
url: "/login/userLogin",
data: {
username: $("#username").val(),
password: $("#password").val(),
token: tokenCook
},
type: "POST",
timeout: 36000,
dataType: "text",
success: function (data, textStatus) {
//alert(data);
var dataJson = eval("(" + data + ")");
if (dataJson.code === 200) {
alert("登录成功");
var toke = dataJson.tokenData.token;
console.log(toke);
document.cookie = toke;
alert(document.cookie);
debugger;
window.location.href = "/";
} else if (dataJson.code === 300) {
popup.show();
popupContent.html("账号不存在,请重新输入!");
} else if (dataJson.code === 400) {
popup.show();
popupContent.html("密码有误,请重新输入!");
} else {
popup.show();
popupContent.html("登录出错!");
}
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert("error:" + textStatus);
}
}
);