大部分文章译自原文:https://exceptionfactory.com/posts/2021/10/23/improving-jwt-authentication-in-apache-nifi/ 同时结合译文,参照NIFI(1.15)源码进行分析讲述举例说明
JSON Web Tokens
为众多Web应用程序和框架提供了灵活的身份验证和授权标准。RFC 7519概述了JWT的基本要素,枚举了符合公共声明属性的所需编码,格式和已注册的声明
属性名称(payload里属性称为声明
)。RFC 7515中的JSON Web签名和RFC 7518中的JSON Web算法描述了JWT的支持标准,其他的比如OAuth 2.0框架的安全标准构建在这些支持标准上,就可以在各种服务中启用授权。
用于生成和验证JSON Web Tokens
的库可用于所有主流的编程语言,这使得它成为许多平台上(身份验证)的流行方法。由于它的灵活性和几个库中的实现问题,一些人批评了JWT的应用程序安全性。尽管与传统的服务器会话管理相比,JWT有一定程度的复杂性,但JSON格式
、标准字段命名
和加密的签名
的这些特性还是使JSON Web Tokens
得到了广泛的应用。
JWT标准定义了令牌的三个元素:header
、payload
和signature
。每个元素使用Bas64编码的字符串组成,以便与HTTP头所需的ASCII字符集相兼容。序列化的令牌结构使用句点(.)
字符分隔这三个元素。header
和payload
元素包含一个或多个属性的JSON对象,signature
元素包含了header
和payload
元素的二进制签名。RFC 7519 3.1节提供了一个JWT示例,其中包括每个元素的编码和解码表示。
大多数JWT都包括一个带有签名算法
的header
,该签名算法
描述了加密密钥的类型
和哈希算法
。JSON Web签名标准定义了利用基于哈希消息验证码的对称密钥
算法,以及几种非对称密钥
算法。两种类型的加密密钥策略都依赖于SHA-2哈希算法,其输出大小可选,分别为256、384或512位。
比如header
指定使用SHA-256的对称密钥HMAC验证,可以在JSON中表示如下:
{"typ":"JWT","alg":"HS256"}
Base64编码后为
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
JWT的payload
包含了一些可扩展数量的属性,称之为声明
。RFC 7519第4.1节定义了一套已经注册了的用于提供基本身份和有效性细节的声明
(我们自定义声明时应别名于这些声明名称关键字)。具体的实现服务中的payload
还可以包括自定义的声明
,以提供额外的授权状态信息。
比如payload
指定了一个带有用户名和过期时间戳的声明,可以使用以下JSON表示:
{"sub":"username","exp":1640995200}
Base64编码后为
eyJzdWIiOiJ1c2VybmFtZSIsImV4cCI6MTY0MDk5NTIwMH0
signature
提供了header
和payload
的可验证签名。更改header
或payload
的任何部分都会导致不同的签名。使用对称密钥或非对称密钥对的私钥生成signature
,这个signature
就可以(使用公钥)被用来去验证header
和payload
是否被篡改,是否还是服务最初发布的原始值。RFC 7519第6节描述了不安全的jwt,其中签名元素为空字符串,签名算法为空,但是这种实现并不常见,需要额外的安全措施,并不适合大多数使用场景。
Apache NiFi从0.4.0
版本起就开始利用JSON Web Tokens
来提供持久的用户界面访问。除了使用X.509证书
的TLS双向认证
外,jwt还支持大多数NiFi认证策略,包括LDAP
、Kerberos
、OpenID Connect
和SAML
。在成功交换凭证之后,NiFi服务生成并返回一个JWT, web浏览器将使用它来处理所有后续请求。这种方法将对身份提供者的影响最小化,还简化了完成登录过程后的应用程序访问。
尽管JWT的生成、签名和验证对NiFi用户或管理员并不直接可见,但这些功能对于应用程序的安全性来说是必不可少的。NiFi最近的变化改进了JWT处理的各个方面,增强了服务器和客户端处理中的应用程序安全性。这些更新涵盖了NiFi在登录处理过程中产生的所有JSON Web Tokens
的密钥生成
、密钥存储
、签名验证
和令牌撤销
。在评估认证策略和考虑整体系统安全时,根据这些更新的实现来理解NiFi JWT处理还是很有用的。
对JWT处理的更新几乎涉及到实现的每个方面,从支持库到客户机请求格式。最初的实现和更新后的实现都依赖于Spring Security来提供web应用程序安全的基础结构。
NiFi 1.14.0和更早版本的JSON Web令牌实现包括以下特性:
对称密钥
H2数据库中存储
对称密钥删除对称密钥
的令牌撤销
Authorization
头和使用本地存储(Local Storage)
来存储TokenJWT处理的更新包括以下特性:
非对称密钥
对,密钥大小为4096位密钥对
基于可配置的持续时间进行更新,默认为1小时State Provider记录
失效的令牌标识符
,实现令牌撤销
cookie
来存储Token重构NiFi JWT涉及到对nifi-web-security
模块的大量代码更改,包括配置和请求处理组件。更改JWT生成和处理还提供了引入新单元测试来验证组件行为的机会。Spring Security框架的最新开发允许用标准实现替换几个自定义类。虽然NiFi没有实现OAuth 2.0
规范,但更新后的JWT实现使用了几个Spring Security OAuth 2.0
组件,它们提供了可配置的令牌验证。一个新的配置类将支持的组件连接在一起,各个元素使用私有变量来指定各个方面,比如键大小和处理算法。虽然一些属性可以作为NiFi应用程序属性公开,但内部默认值为所有部署提供了高级别的安全性。
使用默认值就够用了
自JWT处理在NiFi 0.4.0
中首次亮相以来,就使用JJWT
库实现令牌的生成、签名和验证。JJWT
库里包含了大量的特性和大量的测试,而Spring Security OAuth 2.0
依赖于Nimbus JOSE JWT
库,后者提供了一些额外的功能,例如使用JSON Web Keys对令牌验证的简化支持。这两个库都为JWT处理提供了坚实的基础,但对于依赖于Spring Security OAuth 2.0
的应用程序来说,Nimbus JOSE JWT
是构建自定义功能的最直接的选择。随着Spring Security
依赖的引入,包括spring-security-oauth2-resource-server
和spring-security-oauth2-jose
,迁移到Nimbus JOSE JWT
是首选。
Spring Security OAuth 2.0
库提供了许多用于实现令牌身份验证的有用组件。JwtAuthenticationProvider
实现了标准的Spring Security AuthenticationProvider
接口,并允许与NiFi授权组件相匹配的自定义身份验证转换策略。利用Spring Security
消除了对自定义类的需要。Spring Security
还提供了通用的JwtDecoder
和OAuth2TokenValidator
接口,用于抽象令牌的解析和验证。通过可扩展和可组合的实现,Spring Security OAuth 2.0
模块简化了NiFi JWT处理,并与web安全配置的其余部分自然匹配。
Nimbus库
包含了几个标准接口,包括JWTProcessor
和JWSKeySelector
,它们为声明验证和签名验证提供了扩展点。这些接口的实现支持与Spring Security OAuth 2.0
组件的直接集成,还提供了针对离散特性进行单元测试的机会。Nimbus库
还包括一套完整的JWT对象建模类,这使得它更容易实现特性,而无需担心直接JSON解析和序列化。
用于JSON Web signature
生成和验证的加密密钥
是实现安全性的一个基本元素。关键是要有足够的长度
和随机性
。一个弱密钥或被破坏的密钥可能被对手获取并冒充其他用户或提供升级特权的恶意jwt。
NiFi 1.14.0
及之前版本
使用java.util.UUID.randomUUID()
为每个经过身份验证的用户生成唯一的对称密钥
。为每个用户提供一个唯一的密钥可以确保一个被破坏的密钥不能用于为不同的用户生成JWT。尽管随机UUID方法生成36个字符的字符串,但有效的随机性还是要小得多。
随机UUID方法使用java.security.SecureRandom
生成16个随机字节,但是UUID版本4需要使用一个字节来表示UUID版本,一个字节来表示变体,将有效的随机字节数减少到14,或122位。尽管潜在的随机值的数量仍然非常大,但按照RFC 7518 Section 3.2里的描述,122位还不到使用SHA-256的HMAC所需的最小密钥长度的一半。
更新后的JWT实现将HMAC SHA-256
算法替换为基于RSA密钥对
的数字签名。NiFi不是为每个用户创建一个密钥,而是生成一个密钥大小为4096位的共享密钥对
。RFC 7518 Section 3.5要求使用RSASSA-PSS时密钥最小为2048位,NiFi值为4096符合当前推荐的强RSA密钥对。NiFi使用标准的Java KeyPairGenerator
接口,该接口委托给已配置的Java安全提供程序,并利用SecureRandom类进行随机生成。
NiFi新版的JWT的
RSA密钥对
中,私钥用于生成signature
,公钥要验证signature
。
为了减少潜在的密钥泄露,NiFi以可配置的时间间隔生成新的密钥对,默认为1小时。nifi中的以下属性,可配置属性调整秘钥更新间隔:
nifi.security.user.jws.key.rotation.period
该属性支持使用ISO 8601标准的间隔时间,默认值为PT1H
。更频繁地生成新密钥对会使用额外的计算资源,而较少频繁地更新会影响被破坏的密钥保持有效的时间长度。
JwtAuthenticationSecurityConfiguration
配置类在生成bean的keyGenerationCommand()
方法中,会利用Spring的ThreadPoolTaskScheduler
,注册一个KeyGenerationCommand
的定时任务,调度周期就是nifi.security.user.jws.key.rotation.period
(默认一小时)。KeyGenerationCommand
的run
方法会被调度生成秘钥对,以及一个UUID(JWT ID
),然后更新内存中的私钥,将新的公钥存在Local State中。
最初的NiFi JWT实现将生成的对称密钥存储在位于文件系统上的H2数据库中。数据库表为每个用户建立一条记录,这条记录将生成的UUID与用户标识符关联起来。在NiFi 1.10.0
之前,H2数据库在初次登录后为每个用户保留相同的UUID对称密钥。这种方法不支持任何类型的JWT撤销,依赖于过期声明
来使令牌撤销。
在NiFi 1.10.0
发布更新后,注销用户界面删除了用户当前的对称密钥,有效地撤销了当前令牌,并强制在后续登录时生成一个新的UUID。尽管有这些改进,但还是使用了没有任何额外保护的H2数据库存储对称密钥。
更新后的实现利用非对称加密的属性,将生成的私钥
与公钥``分开存储
。NiFi将当前的私钥保存在内存中
,并将相关的公钥存储在Local State Provider
中。这种方法允许NiFi在应用程序重启后仍可以使用公钥验证当前令牌,同时避免不安全的私钥存储。默认的Local State Provider将条目保存在NiFi安装目录下名为local的目录中。
私钥用于生成签名,存在内存中。公钥用于校验签名是否合法,存在Local State中。 源码
StandardJwsSignerProvider
中的currentSigner
里存的有私钥,只在内存,无持久化。 在源码StandardVerificationKeyService
的save
方法里可以看到,存到state中的是key就是所谓的JWT ID
(生成秘钥对的时候同时生成的UUID),value是一个VerificationKey
对象序列化后的字符串,其中包含了公钥,算法和公钥的过期时间等信息(新生成的公钥过期时间由nifi.security.user.jws.key.rotation.period
配置决定,默认一小时,但后面在签名时,会被新生成的Token的过期时间所覆盖)。
基于密钥生成和密钥存储的改变,新的NiFi JWT实现使用PS512
JSON Web签名算法代替HS256
(HMAC
的SHA-256
算法依赖于对称密钥来生成签名和验证,而其他算法则使用私钥进行签名,使用公钥进行验证)。由于NiFi同时充当令牌颁发者和资源服务器,HMAC SHA-256算法提供了一个可接受的实现。但是,在令牌创建和验证中使用相同的密钥,需要对敏感信息进行持久的存储,而迁移到基于非对称密钥对
的算法会消除这一需求。
在技术术语中,使用HMAC SHA-256
生成的JWT的签名部分不是一个加密签名,而是一个提供数据完整性度量的消息验证码。PS512
算法是利用非对称密钥对的几个选项之一。RS512
和PS512
都使用RSA密钥对,但PS512
使用更新的RSA签名方案和RFC 8017 Section 8.1中的Appendix-Probabilistic Signature Scheme
策略。与RSASSA-PKCS1-v1_5
相比,RSASSA-PSS
标准提供了更好的安全性,RSASSA-PKCS1-v1_5
嵌入了一个哈希函数规范,该规范可能会被较弱的替代方案替代。尽管RFC 8017 Section 8指出,目前还没有针对支持RS512
的签名策略的已知攻击,但还是推荐使用PS512
算法。其他新的非对称密钥对算法也可用,如RFC 8037 3.1节中定义的Edwards-curve Ed25519
,这些算法需要额外的支持库,NiFi可以考虑在未来的版本中包含这些支持库。
随着NIFI从对称密钥
向共享的非对称密钥对
的转变,有必要引入一种新的实现令牌撤销
的方法。
过期机制
强制令牌拥有有限的生命周期,最长可达12小时
,而令牌撤销
可以确保完成注销过程后令牌不再有效。
NiFi版本1.10.0
到1.14.0
通过删除用户的对称密钥实现了有效的令牌撤销
,而更新后的实现则是通过记录和跟踪被撤销的令牌标识符来实现的令牌撤销
。
JWT ID声明提供了标识唯一令牌的标准方法。在令牌生成期间,NiFi分配一个随机UUID作为JWT ID
。当用户发起注销过程时,NiFi记录下这个对应的JWT ID,NiFi根据记录的JWT ID拒绝未来的请求,这种方式使NiFi能够处理令牌发放和令牌失效之间的间隔状态。JWT ID
的记录依赖于NiFiLocal State Provider
,在重启时会被清理一遍(清理那些过期的)。这种撤销策略只存储最少的信息,更加细粒度的使用了标准的JWT属性。同时NiFi使用可配置的秘钥更新周期来查找和删除过期的失效记录。
令牌失效有两种,一种是令牌过期,一种是用户发起注销引起的
令牌撤销
。 前文提及,公钥存储在Local State
,key就是JWT ID
,value是一个对象序列化后的字符串,里面包含了公钥的过期时间。源码JwtAuthenticationSecurityConfiguration
配置类在生成bean的keyExpirationCommand()
方法中,会利用Spring的ThreadPoolTaskScheduler
,注册一个KeyExpirationCommand
的定时任务,调度周期就是nifi.security.user.jws.key.rotation.period
(默认一小时)。KeyExpirationCommand
会调用StandardVerificationKeyService
中deleteExpired()
方法,用来清理过期的公钥记录。 【注意】:虽然公钥有过期时间(默认一小时),会被定时清理,但是这个过期时间会在生成Token时被Token中的过期时间覆盖,比如生成的token默认过期时间12小时,则公钥的过期时间也会更新成12小时。而每次生成的JWT ID
不同,Local State(可以简单理解成一个map)中是可以同时存在多个时段的公钥信息。举个形象点的例子,NIFI启动后生成了一个共享的秘钥对,其中公钥存储到了Local State中,过期时间是默认值一小时(假定我们没有修改nifi.security.user.jws.key.rotation.period
)。过了40分钟后,此时公钥过期时间还剩下20分钟,然后用户张三登陆了NiFi,NIFI程序验证通过了张三的用户名和密码后,要生成并返回JWT,假定生成的Token的过期时间是12小时,其中在生成signature
的时候会将这个12小时的过期时间更新在当前的公钥存储里,于是乎此时公钥过期时间变成了12小时。然后再过20分钟(满一小时了),NIFI程序自动生成了新的秘钥对,内存中的私钥被替换成了新的,Local State中增加了新的公钥,即张三登陆时拿到的那个Token所对应的所需要的公钥还在Local State中。 用户完成登出过程后程序会调用StandardJwtLogoutListener
的logout(final String bearerToken)
方法,方法中会调用StandardJwtRevocationService
的setRevoked(final String id, final Instant expiration)
方法,将当前的JWT ID
记录下来,下一次这个Token发起请求的时候就会拒绝访问。同理公钥存储的过期清理的定时任务,JWT ID
也有定时任务进行过期清理,这里不赘述。
在JWT处理的最初实现中,NiFi使用HTTP Authorization
header传递令牌,使用RFC 6750 Section 2.1中定义的Bearer
方案。在成功交换凭证之后,NiFi用户界面使用Local Storage
存储JWT进行持久访问。基于令牌寿命和跨浏览器实例的持久存储,用户界面维护一个经过身份验证的会话,而不需要额外的访问凭据请求。该接口还利用令牌的存在来指示是否显示登出链接。
使用标准HTTP Authorization
header提供了在后续请求中传递JWT的直接方法,但是利用Local Storage
会引起关于令牌本身安全性的潜在问题。浏览器Local Storage
在应用程序重新启动时持续存在,如果用户在没有完成NiFi注销过程的情况下关闭浏览器,令牌将保持持久性,并可用于未来的浏览器会话。而在NiFi用户界面中执行的所有JavaScript代码都可以使用本地存储,可能导致NIFI受到跨站点脚本攻击。基于这些原因,Web应用程序安全方面建议不要将任何敏感信息持久化到Local Storage
。
除了潜在的安全问题外,使用Local Storage
还会在不同的浏览器实例中访问应用程序资源。NiFi内容查看器等特性需要实现自定义的一次性密码身份验证策略,当浏览器试图加载高级用户界面扩展的资源时,也会导致访问问题。
Local Storage:https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html#local-storage:Also known as Offline Storage, Web Storage. Underlying storage mechanism may vary from one user agent to the next. In other words, any authentication your application requires can be bypassed by a user with local privileges to the machine on which the data is stored. Therefore, it's recommended to avoid storing any sensitive information in local storage where authentication would be assumed.
为了解决安全和可用性问题,NiFi最近的更新使用了HTTP会话cookie
替换了JWTLocal Storage
。会话cookie
实现使用HttpOnly属性来限制访问,使其对JavaScript不可用,这缓解了一些潜在的漏洞。浏览器在重新启动时不维护会话cookie
,这避免了与有效或陈旧令牌的持久性相关的问题。
新的实现使用了SameSite属性的Strict设置,该设置指示支持浏览器避免在第三方站点发起的请求中发送cookie。会话cookie还使用Cookie Name Prefixes来通知支持它的浏览器,cookie必须包含Secure属性,要求在后续的请求中传输使用HTTPS。
由于JavaScript对HTTP会话cookie
的访问限制,更新后的实现还采用了一种不同的方法来注销支持状态。NiFi用户界面将过期时间戳存储在Session Storage
中,而不是将整个令牌存储在Local Storage
中。与会话cookie类似,浏览器在关闭时从Session Storage
中删除项目。此策略依赖于存储最小数量的信息,且使用寿命较短,从而避免了与令牌本身相关的安全问题和潜在的持久性问题。
NiFi中的JSON Web Tokens
并不是Web应用程序安全最明显的方面,但它们在许多部署配置中起到了至关重要的作用。作为一个顶级的开源项目,开发一个最佳的JWT实现需要考虑许多因素。NiFi 0.4.0
中JWT支持的最初部署解决了各种用例,但技术进步和最近的库开发为改进实现提供了几个机会。更新后的JWT集成增强了服务器和浏览器代码中的安全性,为潜在的和理论上的攻击提供了额外的保护。web应用安全的大部分方面都需要不断的评估,NiFi JWT支持也不例外。
关于请求NIFI后端API,以表单形式将
username
password
(application/x-www-form-urlencoded
)Post到https://XX:8443/access/token,返回token然后增加到Cookie(name是__Secure-Authorization-Bearer
)。 如果想避免到NIFI界面登陆,直接重定向到流程,同域的还好说,将token添加到cookie中就好了,而如果是跨域就有些麻烦了。跨域的话最直接的方式就是反向代理(比如nginx)NIFI的地址,使与自定义的web应用同域。还有一种稍微复杂点的需要开发的操作,我是这么干的,我自定义了一套无侵入源码NIFI的多用户多租户的登陆以及授权(一个nar),在NIFI免安全认证开放一个Get请求API(自定义的无侵入源码的war),向这个API传递token和groupId参数,然后在NIFI程序里设置cookie并重定向,最后这种方案有时间的话再写篇文章进行说明。