代码改变世界

理解JSON Web Token (一)

2018-12-09 00:20  龙恩0707  阅读(13518)  评论(0编辑  收藏  举报

一:理解单系统登录的原理及实现?

web应用采用的 browser/server 架构的,http是无状态协议的,也就是说用户从A页面跳转到B页面会发起http请求,当服务器返回响应后,当用户A继续访问其他页面的时候,服务器端无法获知该状态,因此会使用cookie/session来记录用户状态的。

session认证状态的基本原理:当客户端向服务器端请求时,会创建一个session标识存在客户端的cookie当中,每次请求的时候会将该标识随cookie一起发送到服务器端,服务器端会首先检查这个客户端的请求里面是否包含了一个session的标识,如果已经包含了,那么服务器端就会根据该session标识来判断用户的状态,否则的话,服务器端会创建一个新的session标识传给客户端的cookie当中,以后每次客户端请求的时候,会从cookie中获取该标识传递过去。

二:理解单点登录的原理?

上面使用session/cookie 可以实现单个系统的登录,那如果是多个系统的话,怎么办?难道需要用户一个个去登录? 一个个去注销?我们需要做的是,无论系统有多少个,我们只需要登录一次就够了,其他的相关的系统都可以登录/注销一次即可。

单系统登录解决方案的核心是cookie,cookie会携带服务器端返回的sessionId, 在浏览器与服务器端维护会话状态。但是我们知道cookie是有限制的,cookie有域的概念,浏览器发送http请求时会自动匹配该本站点的cookie域。而不是所有的cookie。

那么既然这样,我们很容易想到的是,我们可以把所有子系统的域名都放在一个顶级域名下不就可以了?比如 "*.taobao.com",然后将他们的cookie域设置为 "taobao.com", 但是这种并不好:

第一:因为所有系统的域名需要统一,比如淘宝和天猫的域名就不相同;
第二:应用群各个系统所使用的技术需要相同,比如tomcat服务器叫JESSIONID, 其他的服务器可能不叫这个标识。
第三:cookie的安全性不高的。

因此我们需要一种全新的方式来实现多系统应用群的登录,这就是单点登录。

什么是单点登录?单点登录的全称是 Single Sign On (可以简称为SSO), 在多个系统中只要登录一次,便可以在其他所有系统中得到授权而无需再次登录。

SSO有一个独立的认证中心,认证中心它可以接受用户的用户名密码等安全信息,其他的地方不接受登录入口,只接受认证中心的间接授权,间接授权它是通过令牌实现的。授权令牌作为参数会发送到各个子系统,子系统拿到令牌,因此会得到了授权,因此就可以创建了局部的会话。局部会话和单系统登录的原理很类似的。

下面我们来打个比方理解单点登录的基本原理:

第一步:我想登录A系统,A系统发现用户未登录,因此我们需要他们跳转到SSO认证中心(且将自己请求的地址作为参数传递过去)。SSO认证中心发现未登录,会将用户引导到登录页面。

第二步:用户输入用户名和密码提交申请登录,SSO认证中心会检测用户名和密码信息,如果用户名和密码正确的话,那么用户和SSO认证中心之间会创建一个局部会话,并且创建一个授权令牌。sso认证中心会带着该令牌跳转到A系统那个请求的地址去。

第三步:A系统会检测该令牌,如果有效的话,就会跳到用户输入的地址页面去,否则,还是返回登录页面,提示错误信息。

第四步:用户访问系统B,系统B发现用户未登录,会跳转到SSO认证中心(将自己请求的地址作为参数传递过去),sso认证中心发现用户已经登录了,会跳转回系统B的那个地址去,并带上令牌,系统B拿到令牌,就会去sso认证中心去校验该令牌是否有效。
如果有效的话,说明认证成功了,就会跳转到系统B访问地址的页面上去。

用户现在已经登录成功了,sso认证中心会与各个子系统建立会话,用户与sso认证中心建立的会话被称为全局会话,用户与各个
子系统建立的会话被称为局部会话,局部会话建立之后,用户访问子系统资源后就不会再通过sso认证中心了。

三:什么是JSON Web Token?

JSON Web Token 是一个开放标准协议,它定义了一种紧凑和自包含的方式,它用于各方之间作为JSON对象安全地传输信息。

它有如下优点:

1. 可以适用于分布式的单点登录场景。
2. 可以使用跨域认证解决方案。
3. jwt实现自动刷新token的方案(待认证)。

JSON Web Token,它定义了一种紧凑和自包含的方式,如何理解紧凑和自包含呢?

紧凑:就是说这个数据量比较少,并且能通过url参数,http请求提交的数据以及http header的方式来传递。
自包含:这个串可以包含很多信息,比如用户id,订单号id等,如果其他人拿到该信息,就可以拿到关键业务信息。

3.1)JWT的基本原理,基本流程如下:

1. 客户端使用账号和密码请求登录接口。
2. 登录成功后服务器使用签名密钥生成JWT,然后返回JWT给客户端。
3. 客户端再次向服务端请求其他接口时会带上JWT。
4. 服务器接收到JWT后验证签名的有效性,对客户端做出相应的响应。

3.2)JWT与session的区别?

   session是基于cookie来传输的,session信息是存储在服务器端中,客户端向服务器端发请求时,服务器端会返回一个jessionId给客户端中的cookie中,以后每次请求都会从cookie中的jessionid传递过去,服务器通过cookie中的sessionid获取到当前会话的用户,对于单系统来讲这是没有问题的,但是对于多个系统的话就涉及到session如何共享的问题了,并且随着认证用户增多的话,session会占用大量服务器内存。

JWT是存储在客户端的,服务器端不需要存储JWT,JWT含有用户id,服务器拿到jwt验证后就可以拿到用户信息了,jwt是无状态的,它不与任何机器绑定的,只要签名密钥足够的安全就能保证jwt的可靠性。

3.3)JWT中的token与session中的token安全性比较

session 中安全性问题:

服务器端执行session机制的时候会生成session的口令,在Tomcat服务器中,默认会采用 jsessionid 这个值,但是在其他服务器上会有所不同,比如Connect默认会叫 connect_uid, 我们一般把一些敏感的信息放在cookie中是不可取的,但是将口令放在cookie中还是可以的,如果口令被篡改了的话,就丢失了映射关系,也无法修改服务器端存在的数据,并且session的有效期一般为20/30分钟,如果在该时间之内客户端和服务器端没有产生任何交互,服务器端会自动将session自动清空,因此session中想要维护用户一直登陆的状态的话,需要客户端每隔20分钟使用setInterval自动发一个请求给服务器端,这样的话,前后端就有交互,所以就可以一直保持登陆状态。否则的话,每次20分钟后,登陆状态就会失效,每隔20分钟用户需要重新登录,用户体验将会变得不好。session这样做的最主要的是为了安全性考虑,有效期的时间非常短,防止黑客攻击。

JWT方案中安全性问题

jwt是存储在客户端的,服务器端不需要存储jwt的,客户端每次发送请求时会携带该token,然后到服务器端会验证token是否正确,是否过期了,然后会通过解码出携带的用户的信息的,但是如果token在传输的过程中被攻击者截取了的话,那么对方就可以伪造请求,利用窃取到的token模拟正常请求,实现用户的正常操作,而服务器端完全不知道,因为JWT在服务器端是无状态的,且服务器端不存储jwt的。其实jwt解决的问题是认证和授权的问题,对于安全性的话,还是建议对外公布的接口使用https.

四:理解JWT的基本数据结构

基本的JWT的数据结构是如下这样的:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImlhdCI6MTU0Mzc1MzczNX0.h1XmQo017udxlFsH-8US9Lg8dJ0IDsSbRbjEN5Nq0l4

如上它是由三部分组成的,中间使用 . 分割成三个部分,它是有 Header(头部), PayLoad(负载),Signature(签名)组成的。
如:Header.Payload.Signature

4.1 Header

Header部分是一个JSON对象,描述JWT的元数据,一般是如下的样子:

{
  "typ": "JWT",
  "alg": "HS256"
}

如上json代码,alg属性表示签名的算法,默认是 HMAC SHA256 (缩写为:HS256); typ属性表示这个令牌(token)的类型为JWT. 最后将上面的JSON对象使用 Base64URL的算法转成字符串。
我们可以使用在线的base64编码转下(http://tool.oschina.net/encrypt?type=3),如下所示:

如上 alg 部分,默认加密的算法是 HMAC SHA256, 当然我们也可以选择下面的加密算法,加密算法有如下:

4.2 Payload

Payload部分也是一个JSON对象,用来存放实际需要传递的数据,官方提供了7个字段,如下:

iss(issuer): 签发人
exp (expiration time): 过期时间
sub (subject): 主题
aud (audience): 受众
nbf (Not Before): 生效时间
iat (Issued At): 签发时间
jti (JWT ID): 编号

payload的中文含义是载荷,它可以理解为存放有效信息的地方。这些有效信息一般包含如下三个部分:

4.2.1)标准中注册的声明:(如上就是官方提供的7个字段)。

4.2.2)公共的声明:公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但是不建议添加敏感信息,
因为该部分在客户单可解密。

4.2.3)私有的声明:私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,该部分信息
可以理解为明文信息。

那么定义一个简单的 payload 可以如下结构:

{
  "sub": '123456',
  "name": "kongzhi",
  "admin": true
}

我们还是使用如上的base64编码,会编码成如下所示:

4.3 Signature

Signature 是对前面两部分的签名,防止数据被篡改。签名是把Header和payload对应的json结构进行base64 编码之后得到的两个串用英文句点号拼接起来的,然后会根据header里面的alg指定的前面算法(默认是 HMAC SHA256)生成出来的。
如上header部分使用的是 HS256(即HMAC和SHA256),HMAC是用于生成摘要的,SHA256是用于对摘要进行数字签名的。因此使用HMACSHA256实现signature实现的算法如下:

HMACSHA256(
  base64UrlEncode(header) + '.' + 
  base64UrlEncode(payload),
  secret
)

如上是 Signature 签名算法,最后一个 secret 是加密的密钥的含义。因此通过如上的用法我们就可以拿到JWT了。

4.4 JWT实践

JWT的格式是由三个点分割的base64-URL字符串,可以在html或http环境中传递,我们可以简单的使用 https://jwt.io/ 官网来生成一个JWT了,如下是我前面定义的部分数据:

五:node中使用JWT的API

nodejs实现的jwt的github代码(https://github.com/auth0/node-jsonwebtoken)

它主要有3个方法:

5.1 jwt.sign(payload, secretOrPrivateKey, [options, callback])

payload 参数必须是一个object、Buffer、或 string.
注意:exp(过期时间) 只有当payload是object字面量时才可以设置。如果payload不是buffer或string,它会被强制转换为使用的字符串JSON.stringify()。

secretOrPrivateKey 参数 是包含HMAC算法的密钥或RSA和ECDSA的PEM编码私钥的string或buffer。

options 参数有如下值:

algorithm:加密算法(默认值:HS256)
expiresIn:以秒表示或描述时间跨度zeit / ms的字符串。如60,"2 days","10h","7d",含义是:过期时间
notBefore:以秒表示或描述时间跨度zeit / ms的字符串。如:60,"2days","10h","7d"
audience:Audience,观众
issuer:Issuer,发行者
jwtid:JWT ID
subject:Subject,主题
noTimestamp
header

该方法如果是异步方法,则会提供回调,如果是同步的话,则将会 JsonWebToken返回为字符串。
在expiresIn, notBefore, audience, subject, issuer没有默认值时,可以直接在payload中使用 exp, nbf, aud, sub 和 iss分别表示。

注意:如果在jwts中没有指定 noTimestamp的话,在jwts中会包含一个iat,它的含义是使用它来代替实际的时间戳来计算的。

下面我们在项目中使用node中jsonwebtoken来生成一个JWT的demo了,在index.js 代码如下:

// 生成一个token
const jwt = require('jsonwebtoken');

const secret = 'abcdef';

let token = jwt.sign({
  name: 'kongzhi'
}, secret, (err, token) => {
  console.log(token);
});

然后我们进入项目中的目录,执行 node index.js 执行后看到命令行中会打印中的token了,如下所示:

当然我们也可以设置token的过期时间,比如设置token的有效期为1个小时,如下代码:

// 生成一个token
const jwt = require('jsonwebtoken');

const secret = 'abcdef';
// 设置token为一个小时有效期
let token = jwt.sign({
  name: 'kongzhi',
  exp: Math.floor(Date.now() / 1000) + (60 * 60)
}, secret, (err, token) => {
  console.log(token);
});

5.2 jwt.verify(token, secretOrPrivateKey, [options, callback])

该方法是验证token的合法性

比如上面生成的token设置为1个小时,生成的token为:

'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImlhdCI6MTU0Mzc2MjYzOSwiZXhwIjoxNTQzNzY2MjM5fQ.6idR7HPpjZIfZ_7j3B3eOnGzbvWouifvvJfeW46zuCw'

下面我们使用 jwt.verify来验证一下:

const jwt = require('jsonwebtoken');
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImlhdCI6MTU0Mzc2MjYzOSwiZXhwIjoxNTQzNzY2MjM5fQ.6idR7HPpjZIfZ_7j3B3eOnGzbvWouifvvJfeW46zuCw';
const secret = 'abcdef';
jwt.verify(token, secret, (error, decoded) => {
  if (error) {
    console.log(error.message);
  }
  console.log(decoded);
});

执行node index.js 代码后,生成如下信息:

现在我们再来生成一个token,假如该token的有效期为30秒,30秒后,我再使用刚刚生成的token,再去使用 verify去验证下,看是否能验证通过吗?(理论上token失效了,是不能验证通过的,但是我们还是来实践下)。如下代码:

// 生成一个token
const jwt = require('jsonwebtoken');

const secret = 'abcdef';
// 设置token为30秒的有效期
let token = jwt.sign({
  name: 'kongzhi',
  exp: Math.floor(Date.now() / 1000) + 30
}, secret, (err, token) => {
  console.log(token);
});

在命令行中生成 jwt为: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImV4cCI6MTU0Mzc2MzU1MywiaWF0IjoxNTQzNzYzNTIzfQ.79rH3h_ezayYBeNQ2Wj8fGK_wqsEqEPgRTG9uGmvD64';

然后我们现在使用该token去验证下,如下代码:

// 生成一个token
const jwt = require('jsonwebtoken');

const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImV4cCI6MTU0Mzc2MzU1MywiaWF0IjoxNTQzNzYzNTIzfQ.79rH3h_ezayYBeNQ2Wj8fGK_wqsEqEPgRTG9uGmvD64';
const secret = 'abcdef';
jwt.verify(token, secret, (error, decoded) => {
  if (error) {
    console.log(error.message);
  }
  console.log(decoded);
});

执行命令,如下所示:

如上可以看到token的有效期为30秒,30秒后再执行的话,就会提示jwt过期了。

5.3 jwt.decode(token, [, options])
该方法是 返回解码没有验证签名是否有效的payload。