0%

基于jwt的token认证

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。

  • JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
  • JWT 不加密的情况下,不能将秘密数据写入 JWT。
  • JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数
  • JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。
  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

1. token 认证

当用户成功登陆系统并成功验证有效之后,服务器会利用某种机制产生一个token字符串,这个token中可以包含很多信息,例如来源IP,过期时间,用户信息等, 把这个字符串下发给客户端,客户端在之后的每次请求中都携带着这个token,携带方式其实很自由,无论是cookie方式还是其他方式都可以。当服务端收到请求,取出token进行验证(可以验证来源ip,过期时间等信息),如果合法则允许进行操作。

1.1 优点

  1. 支持跨域访问:Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输。
  2. 无状态: Token机制在服务端不需要存储session信息,因为Token自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息。
  3. 解耦:不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
  4. 适用性更广:只要是支持http协议的客户端,就可以使用token认证。
  5. 服务端只需要验证token的安全,不必再去获取登录用户信息,因为用户的登录信息已经在token信息中。
  6. 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT).

1.2 缺点

  1. 网络传输的数据量增大:由于token中存储了大量的用户和安全相关的信息,所以比单纯的cookie信息(例如session_id)要大很多,传输过程中需要消耗更多流量,占用更多带宽,
  2. 和所有的客户端认证方式一样,如果想要在服务端控制token的注销有难度,而且也很难解决客户端的劫持问题。
  3. 由于token信息在服务端增加了一次验证数据完整性的操作,所以比session的认证方式增加了cpu的开销。

但是整体来看,基于token的认证方式还是比session和cookie方式要有很大优势。在所知的token认证中,jwt是一种优秀的解决方案。

2. jwt 结构

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。中间用点(.)分隔成三个部分。注意JWT 内部是没有换行的。

img

2.1 头部

header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

将上面的内容进行base64编码,可以得到我们JWT的头部,编码后如下:

1
ewogICJhbGciOiAiSFMyNTYiLAogICJ0eXAiOiAiSldUIgp9ICA=

2.2 Payload

Payload 部分也是一个JSON对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

1
2
3
4
5
6
7
iss (issuer):该JWT的签发者
exp (expiration time):什么时候过期,这里是一个Unix时间戳
sub (subject):该JWT所面向的用户
aud (audience):接收该JWT的一方
nbf (Not Before):生效时间
iat (Issued At):在什么时候签发的
jti (JWT ID):编号

除了以上字段之外,你完全可以添加自己想要的任何字段,这里还是提醒一下,由于jwt的标准,信息是不加密的,所以一些敏感信息最好不要添加到json里面

1
2
3
4
5
6
7
8
9
{
"iss": "liuvv.com",
"iat": 1500218077,
"exp": 1500218077,
"aud": "www.liuvv.com",
"sub": "1@liuvv.com",
"user_id": "dc2c4eefe2d141490b6ca612e252f92e",
"user_token": "09f7f25cdb003699cee05759e7934fb2"
}

现在我们需要将负载这整个部分进行base64编码,编码后结果如下:

1
ewogICAgImlzcyI6ICJMZWZ0by5jb20iLAogICAgImlhdCI6IDE1MDAyMTgwNzcsCiAgICAiZXhwIjogMTUwMDIxODA3NywKICAgICJhdWQiOiAid3d3LmxlZnRzby5jb20iLAogICAgInN1YiI6ICJsZWZ0c29AcXEuY29tIiwKICAgICJ1c2VyX2lkIjogImRjMmM0ZWVmZTJkMTQxNDkwYjZjYTYxMmUyNTJmOTJlIiwKICAgICJ1c2VyX3Rva2VuIjogIjA5ZjdmMjVjZGIwMDM2OTljZWUwNTc1OWU3OTM0ZmIyIgp9

2.3 Signature

为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥(这个秘钥只有服务端知道),签名算法是header中指定的那个,然对它们签名即可。

1
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

首先需要将头部和负载通过.链接起来就像这样:header.Payload,上述的例子链接起来之后就是这样的:

1
ewogICJhbGciOiAiSFMyNTYiLAogICJ0eXAiOiAiSldUIgp9ICA=.ewogICAgImlzcyI6ICJMZWZ0by5jb20iLAogICAgImlhdCI6IDE1MDAyMTgwNzcsCiAgICAiZXhwIjogMTUwMDIxODA3NywKICAgICJhdWQiOiAid3d3LmxlZnRzby5jb20iLAogICAgInN1YiI6ICJsZWZ0c29AcXEuY29tIiwKICAgICJ1c2VyX2lkIjogImRjMmM0ZWVmZTJkMTQxNDkwYjZjYTYxMmUyNTJmOTJlIiwKICAgICJ1c2VyX3Rva2VuIjogIjA5ZjdmMjVjZGIwMDM2OTljZWUwNTc1OWU3OTM0ZmIyIgp9

由于HMacSHA256加密算法需要一个key,我们这里key暂时用liuvv吧

加密后的内容为:

1
686855c578362e762248f22e2cc1213dc7a6aff8ebda52247780eb6b5ae91877

对上面的签名内容进行base64编码得到最终的签名

1
Njg2ODU1YzU3ODM2MmU3NjIyNDhmMjJlMmNjMTIxM2RjN2E2YWZmOGViZGE1MjI0Nzc4MGViNmI1YWU5MTg3Nw==

最终的JWT:

1
ewogICJhbGciOiAiSFMyNTYiLAogICJ0eXAiOiAiSldUIgp9ICA=.ewogICAgImlzcyI6ICJMZWZ0by5jb20iLAogICAgImlhdCI6IDE1MDAyMTgwNzcsCiAgICAiZXhwIjogMTUwMDIxODA3NywKICAgICJhdWQiOiAid3d3LmxlZnRzby5jb20iLAogICAgInN1YiI6ICJsZWZ0c29AcXEuY29tIiwKICAgICJ1c2VyX2lkIjogImRjMmM0ZWVmZTJkMTQxNDkwYjZjYTYxMmUyNTJmOTJlIiwKICAgICJ1c2VyX3Rva2VuIjogIjA5ZjdmMjVjZGIwMDM2OTljZWUwNTc1OWU3OTM0ZmIyIgp9.Njg2ODU1YzU3ODM2MmU3NjIyNDhmMjJlMmNjMTIxM2RjN2E2YWZmOGViZGE1MjI0Nzc4MGViNmI1YWU5MTg3Nw==

可以在线玩玩:https://www.bejson.com/jwt/

3. jwt 使用

3.1 使用流程

image

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。此后,客户端每次与服务器通信,都要带上这个 JWT。

你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

1
2
3
4
5
6
7
Authorization: Bearer <token>

fetch('api/user/1', {
headers: {
'Authorization': 'Bearer ' + token
}
})

3.2 JWT 可以被”解码”(不是解密)

JWT 的结构是:header.payload.signature,前两部分(header 和 payload)只是 Base64 编码,任何人都可以解码查看内容。

1
2
3
4
5
6
7
8
// JWT 示例
const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NSIsIm5hbWUiOiJKb2huIERvZSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";

// 解码 payload(第二部分)
const payload = jwt.split('.')[1];
const decodedPayload = atob(payload);
console.log(JSON.parse(decodedPayload));
// 输出: { userId: "12345", name: "John Doe", role: "admin", iat: 1516239022 }

所以 JWT 不适合存储敏感信息。

3.3 JWT 默认是不加密的

JWT 的内容是 公开可见的,只是 Base64 编码(可逆的)。

你可以尝试伪造 JWT,但是服务器会通过签名验证发现伪造!这就是 JWT 安全机制的核心。

1
2
3
4
5
6
7
8
9
// 服务器端生成 JWT
const jwt = require('jsonwebtoken');
const SECRET_KEY = 'server-secret-key-12345'; // 只有服务器知道

const payload = { userId: '123', role: 'admin' };
const token = jwt.sign(payload, SECRET_KEY);

// token 结构:header.payload.signature
// signature = HMACSHA256(base64(header) + "." + base64(payload), SECRET_KEY)

你可以伪造 JWT 的 header 和 payload,但无法伪造有效的签名,服务器通过重新计算签名来验证 JWT 的真实性。

3.4 jwt 泄露了,会有问题吗

JWT 是无状态的,服务器无法主动撤销, 一旦签发,在过期前都是有效的。即使用户修改密码,旧 JWT 仍然有效。

  1. 缩短 JWT 有效期
  2. 实现 Token 黑名单机制
  3. 使用 redis 存储 jwt,修改密码后删除。

4. 参考资料

可以加首页作者微信,咨询相关问题!