Skip to content

JWT

http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
https://jwt.io/

跨域认证的问题

互联网服务离不开用户认证。一般流程是下面这样。

1
2
3
4
5
6
7
8
9
1、用户向服务器发送用户名和密码。

2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。

3、服务器向用户返回一个 session_id,写入用户的 Cookie。

4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。

5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

存在的问题是,扩展性不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。

  • 一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。

  • 另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

JWT 简介

JWT 是 JSON Web Token 的缩写
是由 3 部分拼成的一个字符串,每个部分之间用 . 分隔
在 HTTP 通信过程中,进行身份认证的一种方式。也可以在各个服务之间进行信息传输是 Token 认证方式的一种具体实现

JWT 的认证流程

JWT 的认证方式,服务端不需要保存任何 session 会话,是无状态的,比较容易扩展。
因为不依赖 cookie,可以存在 LocalStorage 里,所以可以防御 CSRF 攻击,更安全。
JWT 默认是不加密的,任何人都可以读到,所以不要把密码信息放在这个部分

放在 cookie 里会引起跨域,如何避免?

  • 放在 HTTP 请求的头信息 Authoriztion 字段里。Authorization: Bearer <token>
  • 另一种做法是,跨域的时候,token(jwt) 就放在 POST 请求的数据体里面
1
2
3
config.headers.common['Authorization'] = `Bearer ${access_token || ''}`
// 或者
axios.default.headers.common["token"] = this.token

JWT 的数据结构

JWT 默认是不加密的,所以不要把秘密信息放在这里

  • Header(头部): JSON,使用 Base64 URL 转成字符串,描述元数据
  • Payload(负载): JSON,使用 Base64 URL 转成字符串,实际需要传递的数据
    • JWT 规定的 7 个官方字段
      • iss(issuer): 签发人
      • exp(expiration time): 过期时间
      • sub(subject): 主题
      • aud(audience): 受众
      • nbf(Not Before): 生效时间
      • iat(Issued At): 签发时间
      • jti(JWT ID): 编号s
    • 还可以定义私有字段
  • Signature(签名): 对前面两部分的签名,防止数据篡改
    • 首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄漏给用户
    • 使用 Header 里面指定的签名算法,按照下面的公式产生签名

产生的签名:

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

Header 的 JSON 对象

1
2
3
4
{
 "alg": "HS256", // 表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256)
 "typ": "JWT" // 表示令牌(token)的类型
}

大概的结果

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVkIjoid3d3Lmd1c2liaS5jb20iLCJzdWIiOiIwMTIzNDU2Nzg5Iiwibmlja25hbWUiOiJnb29kc3BlZWQiLCJ1c2VybmFtZSI6Imdvb2RzcGVlZCIsInNjb3BlcyI6WyJhZG1pbiIsInVzZXIiXX0.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWT 缺点

安全性问题:
JWT 是明文编码: JWT 的编码是明文 Base64 的一个编码,是可以反编译的。在使用 JWT 传输信息的时候,不要放置重要敏感信息,最好使用 https

JWT 泄漏问题:
解决 JWT 的泄露问题是一个平衡的问题。有三种处理方式由轻到重,看你业务重要性酌情选择:

  • 将 JWT 的过期时间设置的很短,即使泄漏也无关紧要。或者旧的直接加入黑名单
  • 在服务端设计 JWT 的黑名单机制,将泄漏的 Token 加黑名单即可
  • 保存签发的 JWT,当 JWT 泄漏时,直接设置失效

JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。
也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑

用户修改账号密码之后,无法清理登录状态

JWT 优点

  • 轻量级: JWT 是非常轻量级的,传输的方式多样化,可以通过 URL/POST 参数/HTTP 头部等方式传输 (一般放在 x-access-token 里)
  • 无状态/跨域认证: 令牌包括所有用于标识用户的信息,这消除了对会话状态的需要。如果我们使用负载平衡器,我们可以将用户请求传递给任何服务器,而不是绑定到我们登录的同一台服务器上
  • 可重用性/扩展性: 我们可以有许多独立的服务器在多个平台和域上运行,并重复使用相同的令牌来验证用户。构建与另一个应用程序共享权限的应用程序很容易
  • 安全性: 无需担心跨站请求伪造 CSRF 攻击

使用场景

  • 利用 token 可以防止表单重复提交(主要说的是后端限制)
    • 前端可以通过按钮置灰防止表单提交
    • 后端的话就是验证 request 里的 token 是否和 session 里的 token 相等,从而判断这个表单是否提交过
  • 用户身份验证(授权): 这是最常见的使用场景,解决单点登录问题。因为 JWT 使用起来轻便,开销小,服务端不用记录用户状态信息(无状态),所以使用比较广泛
  • 信息交换: JWT 是在各个服务之间安全传输信息的好方法。因为 JWT 可以签名,例如,使用公钥/私钥对 - 可以确定请求方是合法的。此外,由于使用标头和有效负载计算签名,还可以验证内容是否未被篡改

flask 中使用 jwt

1
2
pip install Flask
pip install PyJWT
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from flask import Flask, jsonify, request
import jwt
import datetime
from functools import wraps

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secretkey'

def token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.headers.get('Authorization')

        if not token:
            return jsonify({'message': 'Token is missing'}), 401

        try:
            data = jwt.decode(token, app.config['SECRET_KEY'])
        except:
            return jsonify({'message': 'Token is invalid'}), 401

        return f(*args, **kwargs)

    return decorated

@app.route('/login', methods=['POST'])
def login():
    auth = request.authorization

    if not auth or not auth.username or not auth.password:
        return jsonify({'message': 'Could not verify'}), 401

    if auth.username == 'admin' and auth.password == 'password':
        token = jwt.encode({'user': auth.username, 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)}, app.config['SECRET_KEY'])
        refresh_token = jwt.encode({'user': auth.username, 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1)}, app.config['SECRET_KEY'])
        return jsonify({'token': token.decode('UTF-8'), 'refresh_token': refresh_token.decode('UTF-8')})

    return jsonify({'message': 'Could not verify'}), 401

@app.route('/protected', methods=['GET'])
@token_required
def protected():
    return jsonify({'message': 'This is only available for people with valid tokens'})

if __name__ == '__main__':
    app.run(debug=True)

在上面的代码中,我们定义了两个装饰器:token_required 和 protected。 token_required 装饰器用于检查请求是否包含有效的 JWT Token,如果没有 Token,或 Token 无效,则返回 401 错误码。protected 装饰器用于限制只有经过身份验证的用户才能访问。

当用户成功登录后,将生成一个 Token 和一个 Refresh Token,其中 Token 在30分钟后过期,Refresh Token 在1天后过期。Token 将用于验证用户请求,Refresh Token 用于更新 Token,以便用户可以持续访问受保护的路由。

在使用 Flask JWT 进行身份验证和授权时,需要使用 PyJWT 库来创建、签名和验证 Token。在本例中,我们将使用 Flask 的内置 request 对象来获取请求头中的 Token。然后,我们将使用 PyJWT 库来验证 Token 和创建新的 Token。

最后,我们在 Flask 应用程序中定义了两个路由: /login 和 /protected。/login 路由用于登录,`/

请求登录并获取到 jwt:

1
2
3
4
curl --location --request POST 'http://127.0.0.1:5000/login' \
--header 'username: admin' \
--header 'password: password' \
--header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ='
1
2
3
4
{
    "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4iLCJleHAiOjE2NzkyOTkyNDF9.MVK6Cy5sa3SjAq5JR3weXAGsxr0YDTL4xsrkYe6sTc8",
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4iLCJleHAiOjE2NzkyMTQ2NDF9.-IO5D53pEmN9eYrGLiF9pV36yVZBs4jPxi1l8oQowZI"
}

使用jwt_token请求成功:

1
2
curl --location 'http://127.0.0.1:5000/protected' \
--header 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4iLCJleHAiOjE2NzkyMTQ2NDF9.-IO5D53pEmN9eYrGLiF9pV36yVZBs4jPxi1l8oQowZI'

用户修改密码后怎么处理 jwt token

1
2
3
4
5
6
7
8
9
When a user changes their password, the existing JWT (JSON Web Token) that was issued to them based on their previous password is no longer valid, as the token would contain the old password's hash.

To handle this situation, you can follow these steps:

1. Invalidate any existing JWT tokens associated with the user's account.
2. Require the user to log in again with their new password to obtain a new JWT token.
3. Alternatively, you could automatically issue a new JWT token to the user after they have changed their password, which would eliminate the need for them to log in again.

Whichever approach you take, it is important to ensure that the old JWT token is invalidated and that a new one is issued only after the user's password has been successfully updated.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
After a user changes their password, the JWT token they were issued previously will no longer be valid because the user's credentials have been changed. As a result, you need to update the user's JWT token to ensure that it reflects the new password.

To do this, you can follow these steps:

1. The user logs in using their new password.
2. On the backend, you verify the user's credentials and issue a new JWT token.
3. Send the new JWT token to the client as part of the response to the login request.
4. The client stores the new token in local storage or a cookie, replacing the previous token.
5. The client includes the new token in the header of all subsequent requests to the backend.

By following these steps, the backend can ensure that only authenticated and authorized requests are processed using the latest password for the user.