揭秘JWT:从CTF实战到Web开发,使用JWT令牌验证
揭秘JWT:从CTF实战到Web开发,使用JWT令牌验证
介绍
JWT(JSON Web Tokens)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在网络上安全地传输信息。这种信息可以验证和信任,因为它是数字签名的。JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
「优点」:
「无状态」:服务器不需要保存会话信息,减轻了服务器负担。 「可扩展性」:易于在分布式系统中使用,支持跨域身份验证。 「安全性」:通过数字签名确保信息的完整性和来源可信。
「缺点」:
「令牌大小」:由于包含头部、负载和签名,JWT的大小可能相对较大。 「性能」:每次请求都需要验证JWT,可能会增加服务器的处理时间。 「敏感信息泄露」:虽然Payload部分可以加密,但如果不当处理,仍可能泄露敏感信息。
简单复现
CTFShow-web345

让我们查看网页源码,貌似是告诉我们有这个admin后台页面

抓包

修改请求头为admin/index.php

将第一段进行解码,发现是jwt的第一段编码配置,但是加密方式为None,这段token也没有第三段,只有两段,说明根本没有进行加密

第二段就是payload,元数据

尝试将用户user改为admin获取后台权限

302重定向了

尝试把jwt的配置信息给删掉

成功了

python详解jwt
JWT(JSON Web Tokens)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在网络上安全地传输信息。这种信息可以验证和信任,因为它是数字签名的。JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
它的主要应用场景:
授权:这是JWT最常见的使用场景。一旦用户登录,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录(SSO)是目前广泛使用JWT的一项特性,因为它的开销很小,并且可以轻松地跨域使用。 信息交换: JWT是在各方之间安全传输信息的好方式。因为JWT可以被签名,例如,使用公钥/私钥对,你可以确定发送方就是它们所说的那个人。此外,由于签名是使用标头和有效负载计算的,您还可以验证内容是否被篡改。
python代码示例
import jwt import time # 设置headers,即加密算法的配置 headers = { "alg": 'HS256', 'typ': 'JWT' } key = 'abcdefghijklmnopqrstuvwxyz' # 随机的salt密匙,只有token生成者(同时也是校验者)自己能有,用于校验生成的token是否合法 exp = int(time.time()+1) # 设置过期时间, time.time()返回当前时间的时间戳,*1000即可得到毫秒的时间戳 payload = { 'name': 'xiaoyu', # 这个名称可以进行加密 'exp': exp } # 配置主题信息,一般是登录成功的用户之类的,因为jwt的主题信息很容易被解码,所以不要放敏感信息 # 当然也可以将敏感信息加密后再放进payload # 生成token token = jwt.encode(payload=payload, key=key, headers=headers) print(token) # 将token进行解码,第二个参数key用于校验 info = jwt.decode(token, key,algorithms=['HS256']) print(info) #等待两秒后再次验证token,因超时将导致验证失败 time.sleep(2) try: info=jwt.decode(token, key, algorithms=['HS256']) print(info) except Exception as e: print(e) info = jwt.decode(token, key, algorithms=['HS256'],options={'verify_signature': False}) print(info) # 结果 # eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoieGlhb3l1IiwiZXhwIjoxNzE5OTE3NzgwfQ.TGzJclF64WW_Tpxm_p84IYhegL2Pjun5CXbMuizLnzA # {'name': 'xiaoyu', 'exp': 1719917780} # Signature has expired # {'name': 'xiaoyu', 'exp': 1719917780}
jwt生成token所需要的字段
JWT由三部分组成:Header(头部)、Payload(负载)和Signature(签名)。
headers:头部通常包含两部分:令牌的类型(即JWT)和所使用的哈希算法(如HMAC SHA256或RSA)。 payload:负载包含了Claim,Claim是一些实体(通常指用户)的状态和额外的元数据,有三种类型的Claim:注册Claim、公共Claim和私有Claim。

❝iss:jwt签发者
sub:jwt所面向的用户
aud:接收jwt的一方
exp:jwt的过期时间,这个过期时间必须大于签发时间
nbf:定义在什么时间之前,该jwt都是不可用的
iat:jwt的签发时间
jti:jwt的唯一标识身份,主要用来作为一次性token,从而回避重放攻击。
❞
secret_key: 签名是对Header和Payload的签名,防止数据篡改。首先,需要将Header和Payload使用Base64编码,然后用 .
连接,之后使用Header中指定的签名算法(HS256)进行签名。
jwt生成token的过程
由上面的简单示例可以看出,jwt生成token主要由三部分,用.
号隔开,分别代表:编码后的headers、payload,以及校验字段
通过对
headers
的json数据进行base64url编码生成第一部分通过对
payload
的json数据进行base64url编码生成第二部分将第一部分和第二部分通过
.
拼接起来,然后对拼接后的内容结合签名密钥进行HS256加密生成密文(加密算法可以自己选,默认HS256),然后再进行base64url编码,从而生成第三部分三个部分通过
.
拼接起来,作为token
❝base64url编码:先进行base64编码,然后将其中的
❞+
替换成-
、/
替换成_
,并且最后一般会将=
都去掉
例如,下面是一个加密后的payload
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoieGlhb3l1IiwiZXhwIjoxNzE5OTE3NzgwfQ.TGzJclF64WW_Tpxm_p84IYhegL2Pjun5CXbMuizLnzA ------------------------------------ ---------------------------------------------- ------------------------------------------- -------------- headers ------------- -------- payload --------------------------- --------------- 校验字段--------------------
解密后

将各段尝试使用base64
进行解密,第一段headers
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 base64解密后:{"alg":"HS256","typ":"JWT"}
第二段payload
eyJuYW1lIjoieGlhb3l1IiwiZXhwIjoxNzE5OTE3NzgwfQ base64解密后:{"name":"xiaoyu","exp":1719917780}
第三段校验字段
TGzJclF64WW_Tpxm_p84IYhegL2Pjun5CXbMuizLnzA base64解密后:Ll�rQz�e������ ���lˢ̹�
jwt异常处理
jwt校验抛出的异常类基本都在jwt
和jwt.exceptions
下,举例:
import jwt import time from jwt import exceptions # 配置第一段 headers = { 'tyb': 'JWT', 'alg': 'HS256' } salt = 'demmskfhkjagh' # 随便输入即可 exp = int(time.time() + 1) # jwt过期时间-1为立即失效 payload = { # 你需要加密的数据 'name': 'xiaoyu', 'password': '123456', 'age': '21', 'gender': 'man', 'exp': exp } token=jwt.encode(payload, salt, headers=headers) time.sleep(2)# 延迟2s使token过期 try: info=jwt.decode(token,salt,algorithms=['HS256']) print(info) except exceptions.ExpiredSignatureError: print('token已过期') except jwt.DecodeError: print('token认证失败') except jwt.InvalidTokenError: print('非法的token')
Web开发中简单示例
传统token
from flask import Flask, request import json import uuid app = Flask(__name__) db_source = { 'user_table': { 'xiaoyu': { 'pwd': '123456' } }, 'user_token': {}, 'user_info_table': { 'xiaoyu': { 'age': 21 } } } @app.route("/login/<username>/<password>/") def login(username, password): if (not username) or (not password): return {'status': 1, 'code': '400', 'msg': '用户或密码不允许为空!'} if not db_source['user_table'].get(username, None): return {'status': 1, 'code': '401', 'msg': '用户不存在!'} if db_source['user_table'][username]['pwd'] != password: return {'status': 1, 'code': '402', 'msg': '用户名或密码错误!'} token = str(uuid.uuid4()) db_source['user_token'][token] = username return {'status': 1, 'code': '200', 'data': {'token': token}} @app.route('/user_info', methods=['GET']) def user_info(): '''查看用户信息,需要和本地保存的token进行校验''' token = request.args.get('token', None) if not token: return json.dumps({'status': 1, 'code': '500', 'msg': 'token不允许为空!'}, ensure_ascii=False) if not db_source['user_token'].get(token, None): return json.dumps({'status': 1, 'code': '501', 'msg': 'token校验失败!'}, ensure_ascii=False) # 当token校验成功则返回用户信息 username = db_source['user_token'][token] info = db_source['user_info_table'][username] return json.dumps({'status': 1, 'code': '200', 'data': {username: info}}) if __name__ == '__main__': app.run(debug=True)
测试效果我们的用户名为xiaoyu,密码为123456





可以发现这个基于uuid的token使用过之后没有过期时间,永久的存储非常的不方便,数据库也要新加一个字段token有效期,而jwt可以很好的解决这个问题
基于jwt
import jwt from flask import Flask, request import json import uuid from jwt import exceptions app = Flask(__name__) db_source = { 'user_table': { 'xiaoyu': { 'pwd': '123456' } }, 'user_info_table': { 'xiaoyu': { 'age': 21 } } } SECRET_KEY = "ljksljfasiieksf" # @app.route('/create_token/<name>') def create_token(name): ''' 基于jwt创建token函数 :param name: :return: ''' global SECRET_KEY # 加密配置 headers = { 'alg': 'HS256', 'typ': 'JWT' } exp = int(time.time() + 20) # 配置token有效时间为20s payload = { 'name': name, # 这里只配置用户名或者用户id,防止信息泄露 'exp': exp } token = jwt.encode(payload=payload, key=SECRET_KEY, headers=headers) return token def validate_token(token): ''' 校验token :param token: :return: ''' payload=None msg=None try: payload=jwt.decode(token,SECRET_KEY,algorithms=['HS256']) except jwt.DecodeError as e: msg='token verify failed!' # 错误 except jwt.ExpiredSignatureError: msg='token lose efficacy!' # 失效 except jwt.InvalidTokenError: msg='token unlawfulness!' # 非法 return (payload,msg) @app.route("/login/<username>/<password>/") def login(username, password): if (not username) or (not password): return {'status': 1, 'code': '400', 'msg': 'user or pass is None!'} if not db_source['user_table'].get(username, None): return {'status': 1, 'code': '401', 'msg': 'not user!'} if db_source['user_table'][username]['pwd'] != password: return {'status': 1, 'code': '402', 'msg': 'username or password error!'} # token = str(uuid.uuid4()) # db_source['user_token'][token] = username token=create_token(username) return {'status': 1, 'code': '200', 'data': {'token': token}} # eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoieGlhb3l1IiwiZXhwIjoxNzE5OTk1MDM4fQ.siRTK3IKV4uhgFmEUMvfxBfmKqjg7G4B7eeXatP-Q-M @app.route('/user_info', methods=['GET']) def user_info(): '''查看用户信息,需要和本地保存的token进行校验''' token = request.args.get('token', None) if not token: return {'status': 1, 'code': '500', 'msg': 'token is NULL!'} # 校验token,并进行解码 payload,msg=validate_token(token) if msg: return {'status': 1, 'code': '501', 'msg': msg} # 获取解码后的用户名 username = payload['name'] # 从数据源中读取解码后的用户信息 info = db_source['user_info_table'][username] return {'status': 1, 'code': '200', 'data': {username: info}} if __name__ == '__main__': app.run(debug=True)
文章参考:https://www.jianshu.com/p/03ad32c1586c
原文链接:https://mp.weixin.qq.com/s/RT2SNHlrCcbA8IazJ6usqQ