jwt

简介

JWT是一个含签名并携带用户相关信息的加密串,页面请求校验登录接口时,
请求头中携带JWT串到后端服务,后端通过签名加密串匹配校验,保证信息未
被篡改。校验通过则认为是可靠的请求,将正常返回数据。

构成

JWT由三部分组成,分别是头信息header、有效载荷payload、签名signature,中间以(.)分隔

1
xxx.yyy.zzz

由两部分组成,令牌类型(即:JWT)、散列算法(HMAC、RSASSA、RSASSA-PSS等),例如:

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

然后,这个JSON被编码为Base64Url,形成JWT的第一部分。

payload

JWT的第二部分是payload,其中包含claims。claims是关于实体(常用的是用户信息)
和其他数据的声明,claims有三种类型: registered, public, and private claims。

  • 1.Registered claims: 这些是一组预定义的claims,非强制性的,但是推荐使用,
    iss(发行人), exp(到期时间), sub(主题), aud(观众)等;

  • 2.Public claims: 自定义claims,注意不要和JWT注册表中属性冲突,这里可以查看JWT注册表;

  • 3.Private claims: 这些是自定义的claims,用于在同意使用这些claims的各方之间共享信息,
    它们既不是Registered claims,也不是Public claims。

  • 以下是payload示例:

    1
    2
    3
    4
    5
    {
    "sub": "1234567890",
    "name": "John Doe",
    "admin": true
    }

    然后,再经过Base64Url编码,形成JWT的第二部分。

  • 注意
    对于签名令牌,此信息虽然可以防止篡改,但任何人都可以读取。除非加密,
    否则不要将敏感信息放入到Payload或Header元素中。

Signature

要创建签名部分,必须采用编码的Header,编码的Payload,秘钥,Header中指定的算法,并对其进行签名。
例如,如果要使用HMAC SHA256算法,将按以下方式创建签名:

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

签名用于验证消息在此过程中未被篡改,并且,在使用私钥签名令牌的情况下,
它还可以验证JWT的请求方是否是它所声明的请求方。输出是三个由点分隔的
Base64-URL字符串,可以在HTML和HTTP环境中轻松传递,与SAML等基于XML
的标准相比更加紧凑。

1
2
3
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

jwt存储方式

localStorage/sessionStorage存储

响应正文将包含JWT作为一个访问令牌:

  • 后台api

    1
    2
    3
    4
    5
    6
    HTTP/1.1 200 OK

    {
    "access_token": "eyJhbGciOiJIUzI1NiIsI.eyJpc3MiOiJodHRwczotcGxlL.mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB",
    "expires_in":3600
    }
  • 在客户端,你可以将这个令牌存储在HTML5 Web存储中(假设我们有一个成功的回调函数):

    1
    2
    3
    4
    5
    6
    function tokenSuccess(err, response) {
    if(err){
    throw err;
    }
    $window.sessionStorage.accessToken = response.body.access_token;
    }
  • 回传访问令牌到你受保护的API,你将使用HTTP Authorization Header和Bearer组合。请求你的SPA将会像:

    1
    2
    3
    4
    5
    6
    HTTP/1.1
    GET /xxx
    Host: xxx.com
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsI.
    eyJpc3MiOiJodHRwczotcGxlL.
    mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB

    优点

    如果在Authorization header中发送令牌,则跨域资源共享(CORS)将不会成为问题,因为它不使用cookie,
    这样可以用jwt做简单的单点登录,分离出登录系统做sso。

缺点

  • xss攻击
    Web存储(localStorage/sessionStorage)可以通过同一域上JavaScript访问。
    这意味着任何在你的网站上运行的JavaScript都可以访问Web存储,因为这样容易
    受到跨站点脚本(XSS)攻击。(最好确定用https发送jwt)

cookie存储

  • 后端通过response.set_cookie进行设置
  • 回传访问令牌到你同一领域受保护的API,浏览器将自动包括cookie的值。请求你受保护的API将类似于:
    1
    2
    3
    4
    5
    GET /xxx
    Host: xxx.com
    Cookie: access_token=eyJhbGciOiJIUzI1NiIsI.
    eyJpc3MiOiJodHRwczotcGxlL.
    mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB;

    优点

    Cookies,当使用带有HttpOnly的cookie标志时,通过
    JavaScript是无法访问的,并且对XSS是免疫的。你还可
    以设置安全的cookie标志来保证cokie仅通过HTTPS发送。
    这是过去利用cookie存储令牌或会话数据的主要原因之一。
    现代开发人员不愿使用cookie,因为它们通常要求状态被存
    储在服务器上,从而打破RESTful的最佳实践。如果你在cookie
    上存储JWT,cookie作为存储机制不用将状态存储在服务器上。
    这是因为JWT封装了所有服务器需要服务的请求。

缺点

  • csrf攻击
    cookies容易受到不同类型的攻击:跨站点请求伪造(CSRF)。
  • 解决设置cookie同源、同域、path等

jwt的缺点

无法满足注销场景

传统注销清空session即可,因为服务端保存状态。而jwt是无状态的,一旦被生成
在过期时间没到之前都是可用的,虽然注销后可以删除cookie中的jwt,但不代表这个
jwt不可用。

无法满足修改密码场景

修改密码后,盗号者在原jwt有效期内还是可以使用,如果你没有把password加入jwt(应该都不会把)。

无法满足token续签场景

jwt严格定义过期时间,一旦生成,就无法续签延时。

django/flask实操jwt登录

以django为例的jwt使用

大致流程概述

这里其实可以自己手动定制一个简单的jwt,并不复杂,但没必要-vvvvvv-嘿嘿,但还是可以,但……。

  • 1.生成payload里面带有user_id,exp(过期时间),username,email,aud,iat(issued at)甚至可以带权限
  • 2.调准过期时间,payload用json.dumps编码为字符,并encode为二进制。
  • 3.分别对payload,header(也需要dumps、encode一下下)(在setting中配置)进行base64编码
  • 4.通过header中的algorithm拿到算法,获取secretkey(alg.prepare_key(key))
    再获取signature:alg.sign(signing_input, secretkey)
  • 5.对signature进行base64,然后把三个段结合在一起。
    1
    2
    3
    4
    5
    #默认自带算法
    'none': NoneAlgorithm(),
    'HS256': HMACAlgorithm(HMACAlgorithm.SHA256),
    'HS384': HMACAlgorithm(HMACAlgorithm.SHA384),
    'HS512': HMACAlgorithm(HMACAlgorithm.SHA512)

安装和简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pip install djangorestframework-jwt
pip install django-cors-header
from rest_framework_jwt.serializers import jwt_encode_handler, jwt_payload_handler
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
# 伪代码
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer)
# token和payload的生成
re_dict = serializer.data
payload = jwt_payload_handler(user)
print(request.auth)
re_dict['token'] = jwt_encode_handler(payload)
re_dict['name'] = user.name if user.name else user.username
headers = self.get_success_headers(serializer.data)
return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)

jwt_payload_handler生成payload

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
注意这里payload的信息
def jwt_payload_handler(user):
username_field = get_username_field()
username = get_username(user)
warnings.warn(
'The following fields will be removed in the future: '
'`email` and `user_id`. ',
DeprecationWarning
)

payload = {
'user_id': user.pk,
'username': username,
'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA
}
if hasattr(user, 'email'):
payload['email'] = user.email
if isinstance(user.pk, uuid.UUID):
payload['user_id'] = str(user.pk)

payload[username_field] = username

# Include original issued at time for a brand new token,
# to allow token refresh
if api_settings.JWT_ALLOW_REFRESH:
payload['orig_iat'] = timegm(
datetime.utcnow().utctimetuple()
)

if api_settings.JWT_AUDIENCE is not None:
payload['aud'] = api_settings.JWT_AUDIENCE

if api_settings.JWT_ISSUER is not None:
payload['iss'] = api_settings.JWT_ISSUER

return payload

jwt_encode_handler获取jwt_token

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def jwt_encode_handler(payload):
key = api_settings.JWT_PRIVATE_KEY or jwt_get_secret_key(payload)
return jwt.encode(
payload,
key,
api_settings.JWT_ALGORITHM
).decode('utf-8')

# jwt.encode 主要设置jwt本地过期时间、编码payload
def encode(self,
payload, # type: Union[Dict, bytes]
key, # type: str
algorithm='HS256', # type: str
headers=None, # type: Optional[Dict]
json_encoder=None # type: Optional[Callable]
):
# Check that we get a mapping
if not isinstance(payload, Mapping):
raise TypeError('Expecting a mapping object, as JWT only supports '
'JSON objects as payloads.')

# Payload
for time_claim in ['exp', 'iat', 'nbf']:
# Convert datetime to a intDate value in known time-format claims
if isinstance(payload.get(time_claim), datetime):
payload[time_claim] = timegm(payload[time_claim].utctimetuple()) # type: ignore

json_payload = json.dumps(
payload,
separators=(',', ':'),
cls=json_encoder
).encode('utf-8')

return super(PyJWT, self).encode(
json_payload, key, algorithm, headers, json_encoder
)


# super(PyJWT, self).encode(
json_payload, key, algorithm, headers, json_encoder
) # 获取jwt_token
def encode(self,
payload, # type: Union[Dict, bytes]
key, # type: str
algorithm='HS256', # type: str
headers=None, # type: Optional[Dict]
json_encoder=None # type: Optional[Callable]
):
segments = []

if algorithm is None:
algorithm = 'none'

if algorithm not in self._valid_algs:
pass

# Header
header = {'typ': self.header_typ, 'alg': algorithm}

if headers:
self._validate_headers(headers)
header.update(headers)

json_header = force_bytes(
json.dumps(
header,
separators=(',', ':'),
cls=json_encoder
)
)

segments.append(base64url_encode(json_header))
segments.append(base64url_encode(payload))

# Segments
signing_input = b'.'.join(segments)
try:
alg_obj = self._algorithms[algorithm]
key = alg_obj.prepare_key(key)
signature = alg_obj.sign(signing_input, key)

except KeyError:
if not has_crypto and algorithm in requires_cryptography:
raise NotImplementedError(
"Algorithm '%s' could not be found. Do you have cryptography "
"installed?" % algorithm
)
else:
raise NotImplementedError('Algorithm not supported')

segments.append(base64url_encode(signature))

return b'.'.join(segments)
分享到