JWT身份验证相关安全问题

2024-05-29 15:36

本文主要是介绍JWT身份验证相关安全问题,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言:工作中需要基于框架开发一个贴近实际的应用,找到一款比较合适的cms框架,其中正好用到的就是jwt做身份信息验证,也记录一下学习jwt相关的安全问题过程。

 

JWT介绍

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

JWT组成

JWT可分为三部分,分别为头部(header)、载荷(payload)、签名(signature),简单介绍一下每个部分的作用。

整体组成如下,以“.”分隔为三头部、载荷、签名部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT加解密网站:JSON Web Tokens - jwt.io

头部(header)

作用:声明类型、加密算法等

原始格式:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

解base64:

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

typ:声明算法类型,这里是JWT,

alg:声明加密算法,这里是HS256,为对称算法(前后端使用同一密钥进行加密,并非能够解密),常用的还有RS256和ES256两个非对称算法(签名时使用私钥,验证时使用公钥)。

载荷(payload)

作用:存储有效数据

原始格式:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

解base64:

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

载荷部分默认字段:

iss (issuer):JWT的发行者
exp (expiration time):过期时间
sub (subject):JWT面向的主题
aud (audience):JWT的用户
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):JWT唯一标识

注:用户可根据需求自定义字段

签名(signature)

作用:签名部分,服务端校验此字段来验证载荷(payload)字段是否合法

原始格式:SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

该字段加密方式如下:

signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)

这里的HMACSHA256就是在header中alg字段指定的HS256加密算法,而RS256和ES256则是服务端使用私钥加密,好处是可以将验证委托给其他应用,只要散发自己的公钥即可。

JWT用法

JWT被用于身份验证中,作用类似于session,但相比于session这种方式各有优劣,下面简述一下JWT的使用流程

一、用户登录,输入登录所需的信息,后端验证后,返回jwt格式的token

二、用户携带token访问需要身份验证的资源或接口

三、服务端验证jwt格式token中的signature(使用相应加密算法重新加密token中的header和payload,验证是否相等)

四、验证成功,允许访问资源

登录获取token代码示例(lin-cms-springboot)

首先入口是login方法

/**
* 用户登陆
*/
@PostMapping("/login")
public Tokens login(@RequestBody @Validated LoginDTO validator, @RequestHeader(value = "Tag", required = false) String tag) {
UserDO user = userService.getUserByUsername(validator.getUsername());
if (user == null) {
throw new NotFoundException(10021);
}
boolean valid = userIdentityService.verifyUsernamePassword(
user.getId(),
user.getUsername(),
validator.getPassword());
if (!valid) {
throw new ParameterException(10031);
}
return jwt.generateTokens(user.getId());
}

判断用户名和密码正确后,将user.getId(即用户的ID)传入generateTokens方法

public Tokens generateTokens(long identity) {
String access = this.generateToken("access", identity, "lin", this.accessExpire);
String refresh = this.generateToken("refresh", identity, "lin", this.refreshExpire);
return new Tokens(access, refresh);
}

根据固定的字段和传入的用户ID,调用generateToken方法获取token(这里调用了两次,分别生成两个token,access_token和refresh_token,后面会讲)

public String generateToken(String tokenType, long identity, String scope, long expire) {
Date expireDate = DateUtil.getDurationDate(expire);
return this.builder.withClaim("type", tokenType).withClaim("identity", identity).withClaim("scope", scope).withExpiresAt(expireDate).sign(this.algorithm);
}

——————以下调用是com.auth0.jwt第三方库中的内容——————

这里反复调用了withClaim方法

public JWTCreator.Builder withClaim(String name, Long value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withClaim(String name, String value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
...
重载的所有withClaim方法具体内容都一样

因为这个方法返回的还是this,所以可以直接循环调用,是为了生成并绑定不同字段的值。

接着调用了assertNonNull方法、addClaim方法

private void assertNonNull(String name) {
if (name == null) {
throw new IllegalArgumentException("The Custom Claim's name can't be null.");
}
}
​
private void addClaim(String name, Object value) {
if (value == null) {
this.payloadClaims.remove(name);
} else {
this.payloadClaims.put(name, value);
}
}

assertNonNull方法就是判空处理,addClaim方法就将键值对put到payloadClaims这个map对象中,也就是最终生成的payload字段。

这里执行完成后,接着还跳回generateToken方法,因为调用完withClaim方法后,就会调用withExpiresAt(expireDate).sign(this.algorithm),

withExpiresAt方法,就是添加exp字段

public JWTCreator.Builder withExpiresAt(Date expiresAt) {
this.addClaim("exp", expiresAt);
return this;
}

接着进入最关键的sign方法

public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCreationException {
if (algorithm == null) {
throw new IllegalArgumentException("The Algorithm cannot be null.");
} else {
this.headerClaims.put("alg", algorithm.getName());
if (!this.headerClaims.containsKey("typ")) {
this.headerClaims.put("typ", "JWT");
}
​
String signingKeyId = algorithm.getSigningKeyId();
if (signingKeyId != null) {
this.withKeyId(signingKeyId);
}
​
return (new JWTCreator(algorithm, this.headerClaims, this.payloadClaims)).sign();
}
}

headerClaims方法就是向header字段中添加typ和alg的值,重点在最终return的地方,接着跟入new JWTCreator(algorithm, this.headerClaims, this.payloadClaims)

JWTCreator类的实例化,其中传入的参数分别是加密算法对象、header、payload

private JWTCreator(Algorithm algorithm, Map<String, Object> headerClaims, Map<String, Object> payloadClaims) throws JWTCreationException {
this.algorithm = algorithm;
​
try {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(ClaimsHolder.class, new PayloadSerializer());
mapper.registerModule(module);
mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
this.headerJson = mapper.writeValueAsString(headerClaims);
this.payloadJson = mapper.writeValueAsString(new ClaimsHolder(payloadClaims));
} catch (JsonProcessingException var6) {
throw new JWTCreationException("Some of the Claims couldn't be converted to a valid JSON format.", var6);
}
}

简单看一下上面的逻辑,就是对算法、header、payload进行绑定,然后接着往下走,跳回上一步的sign方法,在对JWTCreator实例化后,紧接着又调用了sign方法,不过这个sign方法没有传入参数,也就是下面这个方法

private String sign() throws SignatureGenerationException {
String header = Base64.encodeBase64URLSafeString(this.headerJson.getBytes(StandardCharsets.UTF_8));
String payload = Base64.encodeBase64URLSafeString(this.payloadJson.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = this.algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8));
String signature = Base64.encodeBase64URLSafeString(signatureBytes);
return String.format("%s.%s.%s", header, payload, signature);
}

方法中对header和payload的生成就是简单的对json数据进行base64编码,最终生成signature字段的操作为this.algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8)),接着跟入sign方法

public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException {
try {
return this.crypto.createSignatureFor(this.getDescription(), this.secret, headerBytes, payloadBytes);
} catch (InvalidKeyException | NoSuchAlgorithmException var4) {
throw new SignatureGenerationException(this, var4);
}
}

进入到createSignatureFor方法,传入了加密算法、密钥、header和payload

byte[] createSignatureFor(String algorithm, byte[] secretBytes, byte[] headerBytes, byte[] payloadBytes) throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(secretBytes, algorithm));
mac.update(headerBytes);
mac.update((byte)46);
return mac.doFinal(payloadBytes);
}

就不继续往里跟了(里面最后是调javax.crypto.Mac类中的方法进行的加密,调来调去太乱了),大致原理我们已经搞清楚了,差不多就是根据header和payload字段,然后用secret当salt做一次hash,最后再base64编码传出来,就是我们最终的token。

以上就是从登录到获取token的大致过程。

通过jwt进行身份校验代码示例

这里是拦截器中的一个方法,就不一步一步的跟了,

public boolean handleLogin(HttpServletRequest request, HttpServletResponse response, MetaInfo meta) {
//获取请求头中的Authorization头
String tokenStr = verifyHeader(request, response);
Map<String, Claim> claims;
try {
//在这里做的校验
claims = jwt.decodeAccessToken(tokenStr);
} catch (TokenExpiredException e) {
throw new io.github.talelin.autoconfigure.exception.TokenExpiredException(10051);
} catch (AlgorithmMismatchException | SignatureVerificationException | JWTDecodeException | InvalidClaimException e) {
throw new TokenInvalidException(10041);
}
return getClaim(claims);
}

最终验证的地方在com.auth0.jwt.algorithms.HMACAlgorithm#verify方法中

public void verify(DecodedJWT jwt) throws SignatureVerificationException {
byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature());
​
try {
//然后调用verifySignatureFor方法校验
boolean valid = this.crypto.verifySignatureFor(this.getDescription(), this.secret, jwt.getHeader(), jwt.getPayload(), signatureBytes);
if (!valid) {
throw new SignatureVerificationException(this);
}
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException var4) {
throw new SignatureVerificationException(this, var4);
}
}

跟入

boolean verifySignatureFor(String algorithm, byte[] secretBytes, String header, String payload, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException {
return this.verifySignatureFor(algorithm, secretBytes, header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8), signatureBytes);
}
​
boolean verifySignatureFor(String algorithm, byte[] secretBytes, byte[] headerBytes, byte[] payloadBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException {
return MessageDigest.isEqual(this.createSignatureFor(algorithm, secretBytes, headerBytes, payloadBytes), signatureBytes);
}

跟入

byte[] createSignatureFor(String algorithm, byte[] secretBytes, byte[] headerBytes, byte[] payloadBytes) throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(secretBytes, algorithm));
mac.update(headerBytes);
mac.update((byte)46);
return mac.doFinal(payloadBytes);
}

可以看到,这里的createSignatureFor方法,那么最终实现的原理也就是根据传入的header和payload,再调用创建签名方法根据secret密钥创建签名,如果最终创建出来的签名和你传入的jwttoken中的Signature字段值相等,则判定为真。

JWT安全问题

secret硬编码

将加密使用的secret密钥硬编码在框架中,如果开发者不注意的话,使用默认密钥,没有进行修改,那么只要获取到密钥,就可以伪造token。

知道密钥后,那么可以通过该网站https://jwt.io或者编写脚本伪造token

例如该cms:

1659882676_62efccb4e7be5d7abeaa3.png!small?1659882677806

通过在线网站生成token:

1659882686_62efccbe2184e186d88e3.png!small?1659882686985

那么我们直接使用这个token即可访问网站中需要身份验证的接口资源。

1659882697_62efccc9a29fc0fb41501.png!small?1659882698632

后端未校验Signature字段或

攻击方法:可任意修改或者直接删除

原理:后端未对Signature字段进行校验,就取payload中的数据进行后续操作

alg=none签名绕过漏洞(CVE-2015-2951)

攻击方法:将header中的alg的键值改为none,然后将Signature删除即可

原理:后端未对传入的header中的alg字段进行校验,直接使用其中指定的加密算法对Signature

针对以上两种安全问题,如果没有原始jwt_token,可以使用如下脚本生成token:

使用之前需要先使用pip安装PyJWT,而不是JWT,直接python37 -m pip install PyJWT即可

import jwt
payload = {
"identity": 1,
"scope": "lin",
"type": "access",
"exp": 1659797574
}
print(jwt.encode(payload,None,algorithm="none"))

Secret爆破

上面我们知道硬编码的话我们可以任意伪造token,其实原理就是知道secret密钥,除了开源CMS的这种泄露,或者其他系统备份文件、日志之类的泄露密钥,我们还可以通过爆破的方法获取密钥(如果不是弱密钥,难度非常大)

可以使用这个工具:https://github.com/ticarpi/jwt_tool

修改非对称密码算法为对称密码算法(CVE-2016-10555)

这个漏洞只针对使用非对称加密算法(RS256)做校验的系统,当后端使用RS256加密时,使用私钥加密,而校验时用到的是公私钥对中的公钥做校验,而公钥是公开的,我们很容易获取。

那么当我们修改RS256为HS256时,后端会以为使用的是HS256对称加密做校验,即使用公钥当作HS256校验时的secret来进行加密对比是否相等,把公钥当成secret来使用,也就相当于泄露了secret,所以我们就可以使用公钥来伪造token。

伪造密钥(CVE-2018-0114)

我理解这个漏洞和上面的漏洞比较像,上一个漏洞是后台新人了我们提供的header中的算法,这个是使用了JWS,里面也是存储的公钥,那么我们自己生成公私钥对,然后使用私钥生成token,再将自己生成的公钥放到JWS中,让后台使用这个公钥解密,这样就可以巧妙地绕过后台的验证。

网络安全学习资源分享:

给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

因篇幅有限,仅展示部分资料,朋友们如果有需要全套《网络安全入门+进阶学习资源包》,需要点击下方链接即可前往获取

CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享(安全链接,放心点击)

同时每个成长路线对应的板块都有配套的视频提供: 

大厂面试题

视频配套资料&国内外网安书籍、文档

当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料

所有资料共282G,朋友们如果有需要全套《网络安全入门+进阶学习资源包》,可以扫描下方二维码或链接免费领取~ 

 读者福利 | CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享(安全链接,放心点击)

特别声明:

此教程为纯技术分享!本教程的目的决不是为那些怀有不良动机的人提供及技术支持!也不承担因为技术被滥用所产生的连带责任!本教程的目的在于最大限度地唤醒大家对网络安全的重视,并采取相应的安全措施,从而减少由网络安全而带来的经济损失。

这篇关于JWT身份验证相关安全问题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1013928

相关文章

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

sqlite3 相关知识

WAL 模式 VS 回滚模式 特性WAL 模式回滚模式(Rollback Journal)定义使用写前日志来记录变更。使用回滚日志来记录事务的所有修改。特点更高的并发性和性能;支持多读者和单写者。支持安全的事务回滚,但并发性较低。性能写入性能更好,尤其是读多写少的场景。写操作会造成较大的性能开销,尤其是在事务开始时。写入流程数据首先写入 WAL 文件,然后才从 WAL 刷新到主数据库。数据在开始

购买磨轮平衡机时应该注意什么问题和技巧

在购买磨轮平衡机时,您应该注意以下几个关键点: 平衡精度 平衡精度是衡量平衡机性能的核心指标,直接影响到不平衡量的检测与校准的准确性,从而决定磨轮的振动和噪声水平。高精度的平衡机能显著减少振动和噪声,提高磨削加工的精度。 转速范围 宽广的转速范围意味着平衡机能够处理更多种类的磨轮,适应不同的工作条件和规格要求。 振动监测能力 振动监测能力是评估平衡机性能的重要因素。通过传感器实时监

缓存雪崩问题

缓存雪崩是缓存中大量key失效后当高并发到来时导致大量请求到数据库,瞬间耗尽数据库资源,导致数据库无法使用。 解决方案: 1、使用锁进行控制 2、对同一类型信息的key设置不同的过期时间 3、缓存预热 1. 什么是缓存雪崩 缓存雪崩是指在短时间内,大量缓存数据同时失效,导致所有请求直接涌向数据库,瞬间增加数据库的负载压力,可能导致数据库性能下降甚至崩溃。这种情况往往发生在缓存中大量 k

客户案例:安全海外中继助力知名家电企业化解海外通邮困境

1、客户背景 广东格兰仕集团有限公司(以下简称“格兰仕”),成立于1978年,是中国家电行业的领军企业之一。作为全球最大的微波炉生产基地,格兰仕拥有多项国际领先的家电制造技术,连续多年位列中国家电出口前列。格兰仕不仅注重业务的全球拓展,更重视业务流程的高效与顺畅,以确保在国际舞台上的竞争力。 2、需求痛点 随着格兰仕全球化战略的深入实施,其海外业务快速增长,电子邮件成为了关键的沟通工具。

安全管理体系化的智慧油站开源了。

AI视频监控平台简介 AI视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒,省去繁琐重复的适配流程,实现芯片、算法、应用的全流程组合,从而大大减少企业级应用约95%的开发成本。用户只需在界面上进行简单的操作,就可以实现全视频的接入及布控。摄像头管理模块用于多种终端设备、智能设备的接入及管理。平台支持包括摄像头等终端感知设备接入,为整个平台提

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

2024网安周今日开幕,亚信安全亮相30城

2024年国家网络安全宣传周今天在广州拉开帷幕。今年网安周继续以“网络安全为人民,网络安全靠人民”为主题。2024年国家网络安全宣传周涵盖了1场开幕式、1场高峰论坛、5个重要活动、15场分论坛/座谈会/闭门会、6个主题日活动和网络安全“六进”活动。亚信安全出席2024年国家网络安全宣传周开幕式和主论坛,并将通过线下宣讲、创意科普、成果展示等多种形式,让广大民众看得懂、记得住安全知识,同时还

【VUE】跨域问题的概念,以及解决方法。

目录 1.跨域概念 2.解决方法 2.1 配置网络请求代理 2.2 使用@CrossOrigin 注解 2.3 通过配置文件实现跨域 2.4 添加 CorsWebFilter 来解决跨域问题 1.跨域概念 跨域问题是由于浏览器实施了同源策略,该策略要求请求的域名、协议和端口必须与提供资源的服务相同。如果不相同,则需要服务器显式地允许这种跨域请求。一般在springbo