PyJWT 登录鉴权最佳实践【Refresh Token】

news2024/11/15 11:12:05

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

本文将针对 JWT 的缺点,来通过 refresh_token 方案做一个最佳解决方案。

Welcome to PyJWT — PyJWT 2.8.0 documentationicon-default.png?t=N7T8https://pyjwt.readthedocs.io/en/stable/

1. JWT 优缺点

1.1. JWT 优点

  1. 跨域和跨平台: JWT 由于基于 JSON,具有很好的跨语言支持,可以在任何 web 环境和多种编程语言之间使用。
  2. 自包含: JWT内部包含了所有用户状态信息(claims),从而无需在服务器端存储会话信息,便于分布式系统的扩展。
  3. CSRF保护: 由于 JWT 不依赖于 Cookie,因此它天然地防护了跨站请求伪造(CSRF)攻击。
  4. 轻量和可拓展: JWT本身结构紧凑,传输快捷,并且可以存储额外的业务逻辑所需的非敏感信息。
  5. 无状态和可扩展性: JWT 的无状态特性简化了服务器设计,不需要额外的存储如 Redis 来管理会话状态,有利于服务的横向扩展。

1.2. JWT 缺点

  1. Token失效问题: 一旦 JWT 被签发,在有效期内将持续有效,直到过期。服务器无法控制 Token 的失效,除非引入黑名单机制,这增加了系统的复杂度。
  2. 安全性风险: JWT 如果被截获,由于其自包含的特性,如果不采取额外的保护措施(如HTTPS),就可能被利用。
  3. 存储问题: JWT 通常存储在客户端,如果客户端安全措施不到位,可能会导致 Token 的泄露。
  4. 性能问题: 由于每次请求都需要传输 JWT,如果 Token 过大则会增加请求负载,此外服务器每次都需要验证 Token 的签名,这可能会引入一定的性能开销。
  5. Token刷新问题: 在长期有效性的 JWT 系统中,如何安全地刷新 Token 也是一个挑战,需要设计合理的 Token 刷新机制。

1.3. refresh_token 解决方案

既然 JWT 依然存在诸多问题,甚至无法满足一些业务上的需求,但是我们依然可以基于 JWT 在实践中进行一些改进,来形成一个折中的方案。前面讲的 Token,都是 Access Token,也就是访问资源接口时所需要的 Token,还有另外一种 Token,Refresh Token,通常情况下,Refresh Token 的有效期会比较长,而 Access Token 的有效期比较短,当 Access Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Access Token,如果 Refresh Token 也失效了,用户就只能重新登录了。

所以,在 JWT 的实践中,我决定引入 Refresh Token,将会话管理流程改进如下。

  • 客户端使用用户名+密码登录
    • 服务端生成有效时间较短的 Access Token(60 分钟)和有效时间较长的 Refresh Token(24 小时)
    • 关于 Refresh Token 只要重新登录,就要刷新 redis 的 Refresh Token,避免突然过期情况。
    • Access Token 和 Refresh Token 的失效时间设置建议【见后文】
  • 客户端访问需要认证的接口时(因为有的接口可能会设置为@NoAuth),携带 Access Token。
    • 如果 Access Token 没有过期,服务端鉴权后返回给客户端需要的数据。
  • 客户端需要在 Refresh Token 即将过期时(例如,距离过期还有1分钟),自动使用 Refresh Token 去请求新的Access Token。这样可以提高用户体验,避免在用户操作过程中突然需要重新登录。
  • 如果 token 失效,则在需要认证的接口请求中,服务端需返回 401 错误,客户端此时需要重新登录
  • 如果 Refresh Token 没有过期,服务端向客户端下发新的 Access Token,接下来客户端需使用新的 Access Token 访问需要认证的接口。
  • 注销登录:
    • Refresh Token 立即从 redis 删除(即失效)
    • Access Token 放入黑名单中,在认证接口请求校验过程中,需要同步校验一下

2. JWT 介绍

官网地址:Welcome to PyJWT — PyJWT 2.8.0 documentation

安装:pip install pyjwt

JWT规定的协议的格式 RFC 7515 - JSON Web Signature (JWS)

由三部分组成:头部(Header)、负载(Payload)和签名(Signature)。

2.1. header

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256

一种常见的头部是这样的:

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

然后再将其进行base64编码。

2.2. payload 负载

JWT(JSON Web Token)的有效负载(payload)通常包含一系列声明(claims),这些声明是关于实体(通常是指用户)和其他数据的声明。有效负载中包含的声明可以分为三种类型:注册的声明、公共的声明和私有的声明。

  1. 注册的声明:这些是预先定义的声明,它们不是强制的,但它们的使用是推荐的。它们包括:
    • iss (Issuer):声明 token 的发行者。
    • sub (Subject):声明的主题(通常是用户 ID)。
    • aud (Audience):声明的受众。
    • exp (Expiration Time):token 的过期时间。
    • nbf (Not Before):在此时间之前,token 不可用。
    • iat (Issued At):token 的发行时间。
    • jti (JWT ID):JWT 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。
  2. 公共的声明:这些可以被任何人使用,并且不是由 JWT 标准预先定义的。当使用公共的声明时,应该在 IANA JSON Web Token Claims 注册表中进行定义,或者包含一个包含冲突避免命名空间的 URI。
  3. 私有的声明:这些是由发送者和接收者共同定义的声明,不是注册或公共的声明。这些声明是为了在双方之间传递信息而建立的,并且不是 JWT 标准定义的,也不在 IANA 注册表中注册。

一个常见的payload是这样的:


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

在这个例子中,sub 是一个注册的声明,表示用户的唯一标识符。name 和 admin 是私有的声明,它们是关于用户的具体信息。iat 是一个注册的声明,表示 token 的发行时间。

2.3. signature

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

Access Token 和 Refresh Token 的失效时间设置取决于多种因素,包括应用的安全需求、用户体验和常见的行业实践。以下是一些通用的指导原则和建议:

3. Python 实现 demo

以下通过一个简单的 Python demo 程序来实现上述的认证方案。这个 demo 程序会包括几个基本的文件:

  • main.py - 主应用程序,设置 FpastAPI web 服务。
  • auth.py - 处理认证逻辑,例如生成、刷新、验证 Token 以及黑名单处理。
  • blacklist.py - 处理黑名单的逻辑。
  • user.py - 用户模型和用户管理。

以下是每个文件的简单示例实现:

3.1. main.py

from fastapi import FastAPI, HTTPException, Depends, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from auth import (
authenticate_user,
create_access_token,
create_refresh_token,
verify_token,
ALGORITHM,
SECRET_KEY
)
from blacklist import add_token_to_blacklist
import aioredis
from jose import jwt

app = FastAPI()

# 配置 Redis 连接
REDIS_URL = "redis://localhost:6379"
redis = aioredis.from_url(REDIS_URL, decode_responses=True)

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")

@app.post("/login")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    username = form_data.username
    password = form_data.password
    # 这里应该验证用户名和密码,这里只是示例,所以我们假设用户验证成功
    if authenticate_user(username, password):
        access_token = create_access_token(data={"sub": username})
        refresh_token = create_refresh_token(data={"sub": username})
        # 将 refresh token 存储到 Redis 中
        await redis.setex(username, 3600*24, refresh_token)
        return {"access_token": access_token, "token_type": "bearer", "refresh_token": refresh_token}
    else:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )

@app.post("/logout")
async def logout(token: str = Depends(oauth2_scheme)):
    add_token_to_blacklist(token)
    payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    username: str = payload.get("sub")
    if username is not None:
        # 从 Redis 中删除 refresh token
        await redis.delete(username)
    return {"message": "Logout successful"}

# ... 其他的 endpoint 和 helper 函数 ...

3.2. auth.py

from jose import jwt, JWTError
from datetime import datetime, timedelta
from fastapi import HTTPException, status

SECRET_KEY = "your_super_secret_key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
REFRESH_TOKEN_EXPIRE_HOURS = 24

def authenticate_user(username: str, password: str):
    # 这里应该有用户验证的逻辑,这里只是示例
    return True  # 假设用户验证成功

def create_access_token(data: dict):
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def create_refresh_token(data: dict):
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(hours=REFRESH_TOKEN_EXPIRE_HOURS)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def verify_token(token: str):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        return username
    except JWTError:
        raise credentials_exception

3.3. blacklist.py

BLACKLIST = set()

def add_token_to_blacklist(token: str):
    BLACKLIST.add(token)

def is_token_in_blacklist(token: str):
    return token in BLACKLIST

请注意,上述代码是一个简化的示例,没有实现用户验证和完整的错误处理。在生产环境中,您应该使用安全的方式处理密码验证,并且使用更复杂的认证系统和错误处理机制。此外,您需要确保 SECRET_KEY 被保密,并且是随机生成的,以及在生产环境中,Redis 应该被相应地配置和保护。为了运行这个 FastAPI 应用程序,您需要在您的项目目录下创建上述的 main.py, auth.py, 和 blacklist.py 文件,并将上述代码粘贴到相应的文件中。然后,您可以使用以下命令来启动服务器:uvicorn main:app --reload使用 --reload 参数会使服务器在代码改变时自动重启,这对于开发过程很有帮助,但不应该在生产环境中使用。

3.4. NoAuth 跳过 token 校验

我们在实际业务开发中,有的接口可能不需要 token 校验机制,或者后端开发调试接口时,就需要跳过 token 校验。

而在 FastAPI 中,可以通过依赖注入系统灵活地控制哪些接口需要进行Token校验,哪些接口不需要。虽然 FastAPI 没有直接提供类似于 @NoAuth 注解的功能,但我们可以通过定义不同的依赖项来实现类似的效果。

【其实,不写这个也行,就是 Depends() 为空即可】

以下是一个简单的示例,展示如何在某些接口上跳过Token校验:

  • 定义一个可选的 token 校验依赖项

首先,我们可以定义一个依赖项函数,该函数尝试校验Token,但如果没有提供Token,它也会允许请求通过。这可以通过捕获特定的异常来实现,或者通过检查Token是否存在。

from fastapi import Depends, HTTPException, Security
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")

async def no_auth(token: str = Security(oauth2_scheme, auto_error=False)):
    if not token:
        return None  # 如果没有提供Token,直接返回 None,表示跳过校验
    try:
        # 这里是Token校验逻辑,如果校验失败,抛出异常
        return verify_token(token)
    except JWTError:
        raise credentials_exception
  • 在需要跳过Token校验的接口上使用这个依赖项Depends(no_auth):
from fastapi import FastAPI, Depends

app = FastAPI()

@app.get("/xxx")
async def no_auth_required(user=Depends(no_auth)):
    return {"message": "No auth required for this endpoint."}

在这个例子中,/xxx 路由不需要Token即可访问。如果提供了Token,它会尝试进行校验,并且如果校验成功,user变量将包含用户信息(在这个例子中是用户名)。这种方法提供了灵活性,允许您根据需要在每个路由函数中决定是否进行Token校验。通过使用不同的依赖项,您可以轻松地为不同的接口设置不同的认证和授权策略。

4. token 失效时间设置建议

4.1. Access Token 失效时间

  • 较短的有效期:Access Token 应当有一个较短的有效期,通常在几分钟到几小时之间。短期有效可以减少如果Token被泄露后可能造成的损害。
  • 推荐时长:一个常见的实践是将 Access Token 的有效期设置为 15 分钟到 1 小时。这样的设置能够在保障安全的同时,减少需要用户重新认证的次数。

4.2. Refresh Token 失效时间

  • 较长的有效期:Refresh Token 的有效期通常比 Access Token 长。这是因为 Refresh Token 用于在 Access Token 过期后获取新的 Access Token,而不需要用户重新登录。
  • 推荐时长:Refresh Token 的有效期可以设置得更长,一般从几天到几个月不等。一些应用可能会根据用户的活跃程度来调整有效期,如在用户活跃时自动续期。
  • 固定过期时间:考虑到安全性,Refresh Token 最好有一个固定的过期时间,并且不应该通过每次使用就延长有效期的方式来续期。
  • 安全考量:如果 Refresh Token 被泄露,攻击者可以使用它来获取新的 Access Token。因此,即使 Refresh Token 有较长的有效期,也需要确保其他安全措施得到执行,比如限制使用特定设备或IP地址、检测异常使用模式、及时注销机制等。

4.3. 实际设置

  • 依据实际应用场景:您的具体设置应当根据您的应用场景和安全要求来决定。例如,对于高安全性要求的应用(如金融或医疗应用),您可能希望设置较短的有效期。而对于用户体验非常重要的应用,则可能需要较长的有效期以减少用户重新认证的次数。
  • 遵守法规和标准:某些行业可能有关于认证Token有效期的特定法规和标准,确保您的设置符合这些要求。

总的来说,一个合理的起点可能是设置 Access Token 的有效期为 15 分钟到 1 小时,Refresh Token 的有效期为 7 天到 30 天。然后根据您对安全性和用户体验的权衡,以及用户和业务的实际需求,对这些值进行调整。

5. 参考资料

[1] RFC 7519:https://tools.ietf.org/html/rfc7515

[2] 抖音-渡一前端解读 JSON Web Token:https://v.douyin.com/iNuPG1p9/

[3] 其他:使用python实现后台系统的JWT认证 - 简书

[4] 其他:基于 JWT + Refresh Token 的用户认证实践 - 知乎

[5] 其他:滑动验证页面

[6] 其他:什么是 JWT -- JSON WEB TOKEN - 简书

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1515862.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

NASA数据集——2017年美国阿拉斯加以及加拿大北部二氧化碳探测仪监测的大气后向散射系数剖面图数据集

来自二氧化碳探测仪的大气后向散射系数剖面图,2017年 本数据集提供了2017-07-20至2017-08-08期间在美国阿拉斯加以及加拿大育空地区和西北地区上空进行的二氧化碳夜间、白天和季节排放主动传感(ASCENDS)部署期间收集的大气后向散射系数剖面图…

HTML5+CSS3+移动web——CSS 文字控制属性

系列文章目录 HTML5CSS3移动web——HTML 基础-CSDN博客https://blog.csdn.net/ymxk2876721452/article/details/136070953?spm1001.2014.3001.5502HTML5CSS3移动web——列表、表格、表单-CSDN博客https://blog.csdn.net/ymxk2876721452/article/details/136221443?spm1001.2…

C++ //练习 10.34 使用reverse_iterator逆序打印一个vector。

C Primer(第5版) 练习 10.34 练习 10.34 使用reverse_iterator逆序打印一个vector。 环境:Linux Ubuntu(云服务器) 工具:vim 代码块 /**************************************************************…

springboot整合最新版minio和minio的安装(完整教程,新人必看)

概述:这种东西,多写点,方便以后自己使用 目录 第一步:docker安装配置minio 第一步:拉取镜像 第二步:创建用于存储MinIO数据的卷 如果是最新版minio直接就使用最后的那个命令创建容器 第三步&#xff…

Linux:kubernetes(k8s)lable和selecto标签和选择器的使用(12)

通过标签是可以让我们的容器和容器之间相互认识,简单来说一边打了标签,一边使用选择器去选择就可以快速的让他们之间耦合 定义标签有两种办法,一个是文件中,一个是命令行里 我们在前几章编进文件的时候里面都有lable比如 这个就是…

观测云在 .NET 业务中分析性能问题的最佳实践

背景 某药业集团是一家以创新技术驱动的线下医疗数据 SaaS 平台建设和运营公司,其主营的某智慧医疗平台产品,围绕线下医疗场景痛点提供一体化服务解决方案。近期集团对其生物检材在线递检系统进行功能升级开发及 IaaS 平台迁移。在针对新系统和新基础设…

Java异常分类(三)

ClassCastException异常: class Animal{} class Dog extends Animal{} class Cat extends Animal{} public class Test5{public static void main(String[] args){Animal a new Dog();Cat c (Cat)a;} } 执行结果如图所示: 解决 ClassCastException 的…

第一期:LED点亮与熄灭

Q:设计简单的逻辑电路,通过DE2-115开发板上的滑动开关控制LEDR亮和熄灭状态。 A:基本原理:高电平"1"和"0"分别使LEDR点亮和熄灭,Verilog HDL的assign赋值语句即可实现; 滑动开关作为输…

使用 ReclaiMe Pro 恢复任意文件系统(Win/Linux/MacOS)

天津鸿萌科贸发展有限公司是 ReclaiMe Pro 数据恢复软件授权代理商。 ReclaiMe Pro 是一个通用工具包,几乎可以用于从所有文件系统(从 Windows 系列文件系统、Linux 和 MacOS)中恢复数据。此外,考虑到数据恢复工作的具体情况&…

MYSQL 是如何保证binlog 和redo log同时提交的?

MYSQL 一个事务在提交的时候能够保证binlog和redo log是同时提交的,并且能在宕机恢复后保持binlog 和redo log的一致性。 先来看看什么是redo log 和binlog,以及为什么要保持它们的一致性。 什么是redo log,binlog redo log是innodb引擎层…

腾讯云服务器CVM性能强大、安全、稳定详细介绍,2024年更新

腾讯云服务器CVM提供安全可靠的弹性计算服务,腾讯云明星级云服务器,弹性计算实时扩展或缩减计算资源,支持包年包月、按量计费和竞价实例计费模式,CVM提供多种CPU、内存、硬盘和带宽可以灵活调整的实例规格,提供9个9的数…

2.1_4 编码和调制

文章目录 2.1_4 编码和调制一、第一部分(一)基带信号与宽带信号(二)编码与调制 二、第二部分(一)数字数据编码为数字信号(二)数字数据调制为模拟信号(三)模拟…

【C语言】C语言内存函数

👑个人主页:啊Q闻 🎇收录专栏:《C语言》 🎉道阻且长,行则将至 前言 这篇博客是关于C语言内存函数(memcpy,memmove,memset,memcmp)的使用以及部分的模拟实现 memcpy,memmove,memset,memc…

基于private_key_jwt的客户端身份验证方法

参考文档 spring-authorization-server官网 【版本1.2.2】、 JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants规范。 针对spring-authorization-server官网在Core Model / Components部分提到的RegisteredClient对象中涉及到clien…

springboot+vue学生选课系统 java+ssm+idea+_mysql

系统包含三种角色:管理员、老师、学生,系统分为前台和后台两大模块,主要功能如下。 ide工具:IDEA 或者eclipse 编程语言: java 学生网上选课系统可以实现教室管理,老师管理,课程管理,教学计划管…

基于FPGA的OV7725摄像头的HDMI显示(含源码)

1、概述 本文FPGA通过SCCB接口初始化OV7725摄像头寄存器,然后采集OV7725的摄像头数据,使用DDR3对数据进行暂存,最后将数据输出到HDMI显示器上进行显示。 该工程对应系统框图如下所示,主要包含OV7725驱动及数据处理模块、DDR3读写控…

数据结构(二)——线性表(双链表)

2.3.3 双链表 单链表:单链表结点中只有一个指向其后继的指针,使得单链表只能从前往后依次遍历,无法逆向检索,有时候不太方便 双链表的定义:双链表结点中有两个指针prior和next,分别指向其直接前驱和直接后继 表头结点…

5分钟搞懂MySQL存储引擎

文章目录 什么是存储引擎👋?指定存储引擎✅查看mysql提供什么存储引擎查看mysql当前默认的存储引擎修改mysql默认的存储引擎设置表的存储引擎 常用存储引擎🧰InnoDBMyISAMMemoryInnoDB 和 MyISAM的区别 什么是存储引擎👋&#xff…

23万条数据集,可以用来区分钓鱼网站!

文章目录 一、何为钓鱼网站?二、数据集介绍引用数据集数据展示字段解释 三、数据分析数据读取使用ucimlrepo读取数据 四、下载地址 一、何为钓鱼网站? 在数字化时代,网络安全问题日益严重,其中钓鱼网站是一种常见的网络威胁。钓鱼…

基于Java (spring-boot)的进销存管理系统

一、项目介绍 首页,基础信息管理,备忘录,进销管理,仓库管理,系统管理 二、作品包含 三、项目技术 后端语言:Java 项目架构:B/S架构 数据库:MySQL 前端技术:Vue 后端技术&…