I. 介绍
什么是JWT?
JWT(JSON Web Token)是一种基于JSON的轻量级开放标准(RFC 7519),用于在网络上以一种紧凑且安全的方式传输数据。JWT
通过数字签名来验证数据的完整性,且签名密钥只有私有密钥持有者知道,因此,即使JWT是公开的,也可以保证其安全。
JWT由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature)。头部包含加密算法和类型等信息,载荷包含需要传输的数据,签名则基于头部、载荷和密钥计算而成,用于验证数据的完整性。
JWT的使用非常灵活,可以用于用户认证、Web应用程序、移动应用程序等多种场景。与传统的Session/Cookie机制相比,JWT具有更好的跨平台和跨语言支持、无状态性,以及可扩展性等优点。
JWT的作用和优势
JWT的作用是在客户端和服务器之间传递信息,以完成认证和授权等操作。
JWT的优势主要包括以下几点:
- 安全性高:
JWT
采用数字签名进行验证,因此可以保证数据不被篡改和伪造。同时,因为不保存任何数据到服务端,所以服务端无需保管会话状态,可以避免一些类似“会话劫持”、“会话超期”等问题。 - 跨平台性好:
JWT
的标准和规范非常简单明了,因此可以跨平台、跨语言使用,支持多种类型键值对作为载荷信息。 - 灵活性高:
JWT
可以用于多种场景,例如前后端分离架构、单点登录、API认证授权等。由于是标准化的通用协议,它可以方便快捷地扩展到其他用例。 - 可扩展性强:由于
JWT
将数据保存到自身载荷当中,因此扩展性非常好。开发人员可以利用自定义字段,添加更多有用的信息,满足每个应用程序的不同需求。
总的来说,JWT
的优势就是一个自包含的传输格式,它可以包含有关用户和其它元数据的任何可序列化的信息。因为没有状态,它可以在不同的服务中传输和有效地扩展。
JWT与Session/Cookie的比较
JWT和Session/Cookie都是在Web应用程序中常用的认证机制,但它们又有一些不同之处。
比较主要体现在以下几个方面:
- 数据存储位置不同:
Session/Cookie
方式在服务器端保存一个会话状态对象,一般存储在服务器的内存或磁盘上;而JWT
在客户端保存认证信息,通常存储在浏览器的localStorage
或sessionStorage
中。 - 安全性不同:
JWT
的安全性更高,因为使用数字签名进行了加密,可以避免中间人攻击等问题。而Session/Cookie方式存在信息泄漏和劫持等风险,如果开发不当甚至导致跨站点脚本攻击(XSS)和跨站点请求伪造(CSRF)等问题。 - 服务器压力不同:Session/Cookie方式需要在服务器端维护会话状态信息,保存和检索这些状态消耗一定的服务器资源和性能。而JWT无需在服务器端保存和维护状态,只要在客户端进行认证和授权即可。
- 代码复杂度不同:Session/Cookie方式需要使用一些特定的技术来维护和使用会话状态,开发和维护的难度较大。而JWT基于标准的JSON和数字签名技术,代码实现和维护相对较简单。
综上所述,两种认证机制各有优缺点,在实际应用中应根据具体需求和场景来决定采用哪种方式来实现认证和授权。
II. JWT的原理和结构
JWT的组成部分和数据格式
JWT由三个部分组成:Header、Payload和Signature。三部分都以点号(.)分隔,形成JWT的完整格式。
Header
:JWT头包含两部分信息:令牌类型(通常为“JWT”)和所使用的加密算法名称(例如“HS256”或“RS256”)等。header部分采用Base64Url编码。
示例:
{ "typ": "JWT", "alg": "HS256" }
Payload
:JWT的载荷,又称为JWT的声明,主要包含需要传输的数据及其它辅助数据,如身份信息、权限信息等。payload部分采用Base64Url编码。
示例:
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
Signature
:JWT的签名,用于验证JWT是否被篡改。签名的计算需要使用Header、Payload以及密钥来完成,具体实现方式基于所使用的加密算法而定。
示例:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
最终形式:
xxxxxxxxxx.yyyyyyyyyyyyyy.zzzzzzzzzzzzzzzzzzzzzzzzzzzz
其中,xxxxxxxxxx
是Header部分的Base64Url编码结果,yyyyyyyyyyyyyy
是Payload部分的Base64Url编码结果,zzzzzzzzzzzzzzzzzzzzzzzzzzzz
是签名的结果。优点是JWT的数据格式简单,可以轻松地在各种格式之间转换。
JWT的加密算法和签名机制
JWT支持多种加密算法和签名机制,常见的有以下几种:
- HS256:采用HMAC算法,并使用SHA-256进行哈希,使用一个密钥来进行数字签名。
- RS256:采用RSA算法进行数字签名,使用私钥对数据进行签名,而使用公钥进行验证。
- ES256:采用ECDSA算法,使用椭圆曲线算法进行数字签名,具有轻量化和快速性能的优势。
- PS256:采用RSA算法,并使用PSS(Probabilistic Signature Scheme)进行数字签名。
JWT的签名机制是将Header和Payload合并,然后使用指定的算法和加密密钥进行数字签名。例如,使用HS256算法及密钥secret
进行签名,可以如下实现:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
将计算结果拼接在Header和Payload后面,就生成了一个完整的JWT。在验证JWT时,使用相同的算法和密钥进行数字签名,如果计算出来的签名值与JWT中的签名部分一致,则表示该JWT有效,可以继续使用。
JWT的有效期和刷新机制
JWT的有效期是由Payload中的iat(issued at)和exp(expiration time)字段确定的。
iat字段表示JWT
的发行时间,exp字段表示JWT
的过期时间。当JWT
在其过期时间之后被使用时,就会被服务器拒绝并提示错误信息。
为了确保JWT
仍然可以在有效期内使用,应该在JWT编写代码时,检查JWT的过期时间,如果已经过期,则需要重新生成JWT。但是,在一些场景下,需要保证JWT的可扩展性和可维护性,提供刷新机制可以更好地解决这个问题。
刷新机制允许客户端向服务器发送请求,以刷新JWT
的过期时间,而无需重新进行身份验证。刷新机制的具体实现方式会因特定API、客户端类型和安全威胁而异,但通常使用的方法有以下几种:
- Cookie刷新:使用Cookie方式存储JWT,可以通过将新JWT发送到客户端并在Cookie中更新JWT的值来实现刷新。
- 基于HTTP头的刷新:例如,在每次请求时,在HTTP头中附加JWT,并通过将新JWT返回到客户端来实现刷新。
- 刷新令牌(Refresh Token):刷新令牌是一个单独的、可扩展的令牌,用于验证并更新现有令牌的激活状态。它有一个独立的过期时间,在令牌到期之前,可以用于生成新的JWT。
总的来说,刷新令牌是一种常见和广泛使用的JWT刷新机制,因为它允许减少对服务器的请求次数,并减少了需要重新验证的客户端响应时间。
III. 基于JWT的认证和授权
JWT的生成和验证过程
JWT的生成和验证过程通常可以分为以下几个步骤:
- 生成JWT:在服务器端,生成JWT需要采用一种合适的编程语言和库,并提供表述认证信息和其他关键信息的有效负载。首先,将
Header
和Payload
使用Base64Url
编码,并使用加密算法和密钥计算签名,组成JWT字符串。 - 返回JWT:一旦JWT被生成,服务器需要将其发送回客户端,以便客户端进行后续的请求。在大多数情况下,服务器将JWT作为响应消息的一部分返回,或者将
JWT
保存在客户端Cookie
或localStorage
中。 - 验证JWT:在客户端发出请求时,它需要将JWT包含在所发送的请求中,以便服务器进行验证。服务器会验证JWT的签名是否正确,并检查
JWT
的过期时间和其他有效负载中的数据。如果JWT
有效,则服务器会响应客户端的请求。
在验证JWT时,可以使用多种编程语言和工具来实现JWT验证。下面是一个简单的验证JWT的实现示例(使用Java语言和jjwt库):
String secret = "my-secret-key"; String jwt = "xxxxxxxxxx.yyyyyyyyyyyyyy.zzzzzzzzzzzzzzzzzzzzzzzzzzzz"; try { Jws<Claims> jws = Jwts.parser().setSigningKey(secret.getBytes("UTF-8")).parseClaimsJws(jwt); Claims claims = jws.getBody(); // 验证成功,执行后续操作 } catch (JwtException | UnsupportedEncodingException e) { // 验证失败,抛出异常或返回错误信息 }
在这个示例中,我们提供了一个JWT和一个秘密密钥,使用jjwt库来验证JWT的签名和有效载荷。如果JWT验证成功,则可以查看有效载荷中包含的数据,否则将抛出异常并打印错误消息。
使用JWT进行用户认证
JWT可以用于实现用户身份认证,是一种常用的方式。
下面是基于JWT的用户身份认证流程:
- 用户登录:用户在客户端(如
Web
应用程序或移动应用程序)提供凭据(例如用户名和密码)进行登录。 - 服务器验证凭据:服务器对提供的凭据进行身份验证,如果成功,则创建一个JWT,并返回给客户端。
- 客户端储存JWT:客户端将
JWT
保存在Cookie
或本地存储中,以便在后续的请求中使用。 - 发送JWT:客户端发送请求时,需要将
JWT
包含在请求头的Authorization
字段中。 - 服务器验证JWT:服务器验证
JWT
签名是否正确,检查JWT是否已过期,如果满足条件,则对请求进行授权。 - 发送响应:服务器将响应返回给客户端,并根据需要提供
JWT
的新版本。
需要注意的是,如果JWT
被其他人截获,他们也可以获得访问资源的权限。为了减少这种风险,应使用HTTPS
来保护JWT
传输。此外,建议将JWT
中包含的有效载荷数据最小化,并对JWT
进行定期刷新,以减少令牌的有效时间并增加破解的难度。
以下是一个使用jsonwebtoken
库进行JWT
认证的示例(使用Node.js语言):
const jwt = require('jsonwebtoken'); const secret = 'my-secret-key'; // 用户登录验证 router.post('/login', function(req, res) { const username = req.body.username; const password = req.body.password; // 对用户提供的凭据进行身份验证 if (verifyUser(username, password)) { // 如果验证成功,则创建一个JWT const token = jwt.sign({ username }, secret, { expiresIn: 3600 }); res.json({ token }); } else { res.sendStatus(401); } }); // 保护需要认证的资源 router.get('/protected-data', ensureAuthenticated, function(req, res) { res.json({ data: 'The protected data' }); }); // 客户端指定JWT function ensureAuthenticated(req, res, next) { const token = req.headers.authorization; // 检查JWT是否已过期,如果未过期,则进行身份验证 try { const decoded = jwt.verify(token, secret); req.user = decoded.username; next(); } catch { res.sendStatus(401); } }
在这个示例中,我们使用jsonwebtoken
库来创建和验证JWT
。首先当用户提供有效的用户名和密码后,服务器将为其创建一个包含用户名的JWT
并返回给客户端。在访问需要身份验证的资源时,需要对JWT进行验证,以确保用户已经通过身份验证。最后,我们使用EnsureAuthenticated
函数来保护API中的所需资源。
基于JWT的用户登录和注销
基于JWT的用户登录和注销过程与其他认证方式的流程类似。
以下是一些典型的登录和注销过程:
1. 用户登录:
a. 用户需要在登录表单中输入用户名和密码。
b. 应用程序将用户提供的凭据发送到服务器进行验证。
c. 如果凭据是有效的,则服务器将为该用户创建一个JWT并将其发送回客户端。新的JWT应该存储在客户端的Cookie或本地存储中。
2. 用户注销:
a. 如果用户希望注销,则应用程序应该从客户端的存储设备中删除JWT。
b. 服务端不会直接处理注销请求,因为JWT是基于状态less的。如果需要锁定帐户,则可以将其标记为僵尸帐户。操作如下:当应用程序接收到JWT时,将检查标记状态以确保它尚未被标记为僵尸帐户。
以下是一个使用jsonwebtoken
库进行基于JWT的用户登录和注销的示例(使用Node.js语言):
const jwt = require('jsonwebtoken'); const secret = 'my-secret-key'; // 用户登录验证 router.post('/login', function(req, res) { const username = req.body.username; const password = req.body.password; // 对用户提供的凭据进行身份验证 if (verifyUser(username, password)) { // 如果验证成功,则创建一个JWT const token = jwt.sign({ username }, secret, { expiresIn: 3600 }); res.json({ token }); } else { res.sendStatus(401); } }); // 用户注销 router.post('/logout', function(req, res) { // 立即识别JWT无效状态,将其标记为僵尸状态 markTokenAsDead(req.token) res.json({ message: '用户成功注销' }); }); // 保护需要认证的资源 router.get('/protected-data', ensureAuthenticated, function(req, res) { res.json({ data: 'The protected data' }); }); // 客户端指定JWT function ensureAuthenticated(req, res, next) { const token = req.headers.authorization; req.token = token; // 检查JWT是否已过期,并查看其是否被标记为僵尸状态 try { const decoded = jwt.verify(token, secret); if (isTokenDead(req.token)) { res.sendStatus(401); } req.user = decoded.username; next(); } catch { res.sendStatus(401); } }
在这个示例中,我们添加了一个新的路由来支持用户注销。当用户注销时,我们将JWT标记为“死”状态,并从客户端移除。在访问需要身份验证的资源时,使用EnsureAuthenticated函数时,需要检查传递的JWT是否已过期并且是否已被标记为僵尸状态。如果JWT未过期并且未被标记为僵尸,则进行身份验证并授权访问,否则返回401错误码。
基于JWT的授权和访问控制
基于JWT的授权和访问控制可以帮助应用程序管理用户和资源之间的访问,保护应用程序的保密性和完整性。以下是一个示例过程,以说明如何使用JWT实现基于角色的访问控制:
- 用户登录:用户输入用户名和密码登录应用程序,并成功通过身份验证。
- 生成JWT:服务器为用户生成一个包含用户角色和其他有效负载信息的JWT,并将其签名。
- 发送JWT:服务器将JWT发送给客户端,并要求稍后使用此JWT进行访问控制。
- 访问控制:在客户端发起将要访问的资源与服务器验证时,需要将JWT附加在请求的头部中,解析并验证该JWT,并将用户角色与所请求的资源进行比对。
- 授权:如果JWT验证成功,并且该用户拥有所需角色,则服务器将通过授权访问资源,否则将返回403错误码。
IV. 前端应用中的JWT使用
在前端应用中使用JWT进行用户认证和授权
在前端应用程序中使用JWT进行用户认证和授权通常需要在服务器端实现用户身份验证和JWT生成,并在客户端使用JavaScript检查和保存JWT。
以下是基于JWT的前端用户认证和授权过程的一个示例:
- 用户登录: 用户通过
Web
应用程序的登录表单输入其用户名和密码,Web应用程序将这些凭据发送到服务器以进行身份验证。 - 服务器验证凭据: 服务器对用户提供的凭据进行身份验证,如果用户通过验证,则使用服务器上的私钥构建一个
JWT
,并将其发送回客户端。 - 客户端储存JWT: 客户端可以将
JWT
作为Cookie
或本地存储的值保存起来。 - 发送JWT: 客户端在访问需要身份验证的资源时,需要将JWT附加在
Authorization
请求头中。 - 服务器验证JWT: 当服务器接收到请求时,它首先将检查请求头中的
Authorization
值。 如果该值存在,则服务器将使用之前使用的JWT生成密钥验证JWT签名的有效性。如果验证成功,则将提取JWT有效负载中的信息以进行授权,否则将返回401错误码。 - 授权: 如果JWT验证成功,则服务器将使用户能够访问资源,并向客户端返回请求。
JWT的存储方式和安全性
JWT可以存储在多种方式中,包括Cookie、本地存储和HTML5 Web存储
等。
以下是一些常见的存储方式及其安全性:
Cookie
:JWT可以存储在Cookie中,但需要注意Cookie的安全性问题。Cookie可以被窃取或被篡改,可能会遭受跨站脚本攻击(XSS)或跨站请求伪造(CSRF)。将JWT存储在Cookie中时,需要将HttpOnly和Secure属性设置为true,以提高安全性。本地存储
:JWT可以存储在本地存储中,包括localStorage或sessionStorage。localStorage保存在浏览器的localStorage对象中,sessionStorage保存在sessionStorage对象中。但这种存储方式的安全性较低,因为它由JavaScript代码控制,易受到XSS攻击。HTML5 Web存储
:HTML5 Web存储API提供了两种类型的存储:IndexedDB和Web SQL。在IndexedDB中,数据可以在浏览器中存储并被安全访问。在Web SQL中,数据可以被轻松查询。但是,它们在某些方面可能不如其他存储方式安全,例如Web SQL可能会面临SQL注入攻击。
为了提高JWT的安全性,建议使用以下建议:
- 限制
JWT
的生命周期并定期更换JWT。这可以确保JWT
丢失时,被恶意使用的机会很小。 - 避免在
JWT
中存储敏感信息,例如密码等。JWT
中只存储可以明文公开的数据。 - 对
JWT
进行数字签名以防止篡改,并使用JSON Web
加密(JWE
)来保护JWT
中的敏感信息。 - 对
JWT
进行加密传输,避免在传输过程中被截取。 - 确保密钥在服务器端安全地存储,有效地对客户端请求进行身份验证。
如何使用JWT保持登录状态
JWT可用于在多个请求之间保持用户的登录状态。
以下是如何使用JWT来实现保持登录状态的一些示例步骤:
- 用户登录: 当用户在Web应用程序的登录页面上输入他们的用户名和密码,并通过身份验证,则Web应用程序会生成一个JWT并将其发送回客户端。
- JWT存储: 客户端可以将JWT存储在Cookie或本地存储中。Cookie和本地存储的设置保持相同。
- 发送JWT: 在JWT生成后,客户端会带上JWT在Authorization头中发送User请求。
- 服务器验证JWT: 当服务器接收到请求时,它会检查请求头中Authorization字段的值。如果该值存在,则服务器将使用之前使用的JWT的密钥验证该JWT签名的有效性。
- 授权: 如果JWT验证成功,则服务器将使用JWT中的信息来处理请求,并向客户端返回请求结果。如果JWT验证失败,则服务器将返回401错误响应。
- 实施注销功能: 当用户选择注销并退出应用程序时,服务器应该使客户端删除JWT。在实现注销功能时,应该清除客户端设置的JWT,在服务器上删除JWT,并向客户端发送确认消息。
在实现上述步骤时,请注意以下事项:
- JWT的有效期应该足够长以保持用户登录状态并避免再次生成JWT,但也不能太长以增加未经授权的访问风险。
- JWT中不应该包含敏感信息,例如密码。
- JWT应使用数字签名或加密以确保不被修改或伪造,仅使用最新版本的JWT密钥进行签名或加密。
- JWT应仅存储必要的用户数据以减少其占用的存储空间。
上述步骤只是一些示例步骤,实际的步骤取决于使用的具体技术和框架。