什么是JWT验证机制?

本文介绍了JWT(Json Web Token)的原理和优势,包括其构成部分、生成流程以及安全性注意事项。同时,详细阐述了Django REST framework JWT扩展的安装配置和使用方法,展示了如何在用户注册后生成并使用JWT进行身份验证。

一、JWT:

在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token认证机制。
1.什么是JWT:

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
1.1 起源:

说起JWT,我们应该来谈一谈基于token的认证和传统的session认证的区别。

1.1.1 传统的session认证:

我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。

但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来.
基于session认证所显露的问题
session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

1.1.2 基于token的鉴权机制:

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

流程上是这样的:

用户使用用户名密码来请求服务器
服务器进行验证用户的信息
服务器通过验证发送给用户一个token
客户端存储token,并在每次请求时附送上这个token值
服务端验证token值,并返回数据
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *。

2. JWT长什么样?

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

2.1 JWT的构成:

第一部分我们称它为头部(header);
第二部分我们称其为载荷(payload, 类似于飞机上承载的物品);
第三部分是签证(signature).

2.1.1 header:

jwt的头部承载两部分信息:

声明类型,这里是jwt
声明加密的算法 通常直接使用 HMAC SHA256
完整的头部就像下面这样的JSON:
{
  'typ': 'JWT',
  'alg': 'HS256'
}

然后将头部被进行base64加密(该加密是可以被对称解密的), 构成第一部分

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

2.1.2 payload

在和就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

标准中注册的声明

公共的声明

私有的声明
标准中注册的声明 (建议但不强制使用) :

iss: jwt签发者

sub: jwt所面向的用户

aud: 接收jwt的一方

exp: jwt的过期时间,这个过期时间必须要大于签发时间

nbf: 定义在什么时间之前,该jwt都是不可用的.

iat: jwt的签发时间

jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

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

定义一个payload:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

2.1.3 signature

JWT的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的)
payload (base64后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用 . 连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

3. 如何应用:

一般是在请求头里加入Authoriztion, 并加上Beaer标注:

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

服务端会验证token,如果验证通过就会返回相应的资源。整个流程就是这样的:


4.总结

4.1优点

因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
它不需要在服务端保存会话信息, 所以它易于应用的扩展
4.2安全相关

不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
保护好secret私钥,该私钥非常重要。
如果可以,请使用https协议
二、Django REST framework JWT:

我们在验证完用户的身份后(检验用户名和密码), 需要向用户欠打JWT,在需要用到用户身份信息的时候,还需核验用户的JWT。
关于签发和核验JWT, 我们可以使用Django REST framework JWT扩展来完成。
文档网站:http://getblimp.github.io/django-rest-framework-jwt/

1.安装配置:

安装:

pip install djangorestframework-jwt
配置:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}
​
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
}
# JWT_EXPIRATION_DELTA 指明token的有效期
​

2. 使用

Django REST framework JWT扩展的说明文档中提供了手动签发JWT的方法:

from rest_framework_jwt.settings import api_settings

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)

在注册成功后,联通返回token,需要在注册视图中创建token。
修改CreateUserSerializer序列化器,在create方法中增加手动创建token的方法

from rest_framework_jwt.settings import api_settings
​
class CreateUserSerializer(serializers.ModelSerializer):
    """
    创建用户序列化器
    """
    ...
    token = serializers.CharField(label='登录状态token', read_only=True)  # 增加token字段
​
    class Meta:
        ...
        fields = ('id', 'username', 'password', 'password2', 'sms_code', 'mobile', 'allow', 'token')  # 增加token
        ...
​
    def create(self, validated_data):
        """
        创建用户
        """
        # 移除数据库模型类中不存在的属性
        del validated_data['password2']
        del validated_data['sms_code']
        del validated_data['allow']
        user = super().create(validated_data)
​
        # 调用django的认证系统加密密码
        user.set_password(validated_data['password'])
        user.save()
​
        # 补充生成记录登录状态的token
        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        user.token = token
​
        return user

前端保存token
我们可以将JWT保存在cookie中,也可以保存在浏览器的本地存储里,我们保存在浏览器本地存储中

浏览器的本地存储提供了sessionStorage 和 localStorage 两种:

sessionStorage 浏览器关闭即失效
localStorage 长期有效
使用方法
sessionStorage.变量名 = 变量值 // 保存数据
sessionStorage.变量名 // 读取数据
sessionStorage.clear() // 清除所有sessionStorage保存的数据

localStorage.变量名 = 变量值 // 保存数据
localStorage.变量名 // 读取数据
localStorage.clear() // 清除所有localStorage保存的数据

在前端js/register.js文件中增加保存token

var vm = new Vue({
    ...
    methods: {
        ...
        on_submit: function(){
            axios.post(...)
                .then(response => {
                    // 记录用户的登录状态
                    sessionStorage.clear();
                    localStorage.clear();
                    localStorage.token = response.data.token;
                    localStorage.username = response.data.username;
                    localStorage.user_id = response.data.id;
                    location.href = '/index.html';
                })
                .catch(...)
        }
    }
})

 

### 设计高效的JWT刷新机制 高效且安全JWT刷新机制需要综合考虑令牌的存储、验证流程以及防止滥用的最佳实践。以下是关于这些方面的详细说明: #### 1. 刷新令牌的存储 刷新令牌通常具有较长的有效期,因此其存储方式至关重要。推荐将刷新令牌存储在服务器端的安全数据库中,并与用户ID关联[^3]。此外,可以为每个刷新令牌生成一个唯一标识符(JTI),以防止重复使用和重放攻击。 ```python # 示例:存储刷新令牌到数据库 def store_refresh_token(user_id, refresh_token): jti = jwt.decode(refresh_token, 'refresh_secret_key', algorithms=['HS256'])['jti'] db.execute("INSERT INTO refresh_tokens (user_id, token, jti) VALUES (?, ?, ?)", (user_id, refresh_token, jti)) ``` #### 2. 验证流程 在验证刷新令牌时,首先检查其是否存在于数据库中,然后解析令牌以验证签名和过期时间。如果验证成功,则生成新的访问令牌和刷新令牌,并更新数据库中的记录。 ```python # 示例:验证刷新令牌并生成新令牌 def verify_and_refresh(refresh_token): try: payload = jwt.decode(refresh_token, 'refresh_secret_key', algorithms=['HS256']) jti = payload['jti'] user_id = payload['user_id'] # 检查刷新令牌是否存在于数据库中 result = db.execute("SELECT * FROM refresh_tokens WHERE user_id = ? AND jti = ?", (user_id, jti)).fetchone() if not result: raise Exception("Invalid refresh token") # 生成新的访问令牌和刷新令牌 access_token, new_refresh_token = generate_tokens(user_id, 'secret_key', 'refresh_secret_key') # 更新数据库中的刷新令牌 db.execute("UPDATE refresh_tokens SET token = ?, jti = ? WHERE user_id = ?", (new_refresh_token, payload['jti'], user_id)) return access_token, new_refresh_token except jwt.ExpiredSignatureError: raise Exception("Token expired") except jwt.InvalidTokenError: raise Exception("Invalid token") ``` #### 3. 防止滥用的最佳实践 为了防止刷新令牌被滥用,可以采取以下措施: - **限制刷新令牌的使用次数**:为每个刷新令牌设置最大使用次数,超过后需重新登录。 - **IP绑定**:将刷新令牌与用户的登录IP地址绑定,增加安全性[^3]。 - **滑动过期策略**:每次刷新令牌时,延长其有效期,但不超过设定的最大值。 ```python # 示例:实现滑动过期策略 def sliding_expiry(current_time, last_refresh_time, max_age): if current_time - last_refresh_time > max_age / 2: return current_time + max_age else: return last_refresh_time + max_age ``` ### 相关代码示例 以下是一个完整的JWT刷新机制的代码示例: ```python import jwt import time import sqlite3 db = sqlite3.connect(':memory:') def generate_tokens(user_id, secret_key, refresh_secret_key): access_token = jwt.encode({ 'user_id': user_id, 'exp': int(time.time()) + 300 }, secret_key, algorithm='HS256') refresh_token = jwt.encode({ 'user_id': user_id, 'exp': int(time.time()) + 86400, 'jti': str(time.time()) # 唯一标识符 }, refresh_secret_key, algorithm='HS256') return access_token, refresh_token def store_refresh_token(user_id, refresh_token): jti = jwt.decode(refresh_token, 'refresh_secret_key', algorithms=['HS256'])['jti'] db.execute("INSERT INTO refresh_tokens (user_id, token, jti) VALUES (?, ?, ?)", (user_id, refresh_token, jti)) db.commit() def verify_and_refresh(refresh_token): try: payload = jwt.decode(refresh_token, 'refresh_secret_key', algorithms=['HS256']) jti = payload['jti'] user_id = payload['user_id'] result = db.execute("SELECT * FROM refresh_tokens WHERE user_id = ? AND jti =
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值