MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JWT令牌的结构与签名算法解析

2022-10-102.3k 阅读

JWT令牌的结构

JSON Web Token(JWT)是一种用于在网络应用中安全传输信息的开放标准(RFC 7519)。JWT通常由三部分组成,分别是头部(Header)、载荷(Payload)和签名(Signature),它们之间通过点(.)进行分隔,其结构形式如下:header.payload.signature

头部(Header)

JWT的头部通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。下面是一个典型的头部示例:

{
  "alg": "HS256",
  "typ": "JWT"
}
  • alg(algorithm):指定了签名所使用的算法。常见的算法包括HS256(HMAC使用SHA-256哈希算法)、HS384、HS512以及RSA系列算法如RS256、RS384、RS512等。不同的算法在安全性和性能上各有特点。例如,HS256算法相对简单、速度快,适用于服务器之间的信任传递;而RSA算法基于非对称密钥对,安全性更高,常用于第三方认证场景。
  • typ(type):表明这是一个JWT类型的令牌。

之后,这个JSON对象会使用Base64Url编码算法进行编码,成为JWT的第一部分。Base64Url编码是对标准Base64编码的一种变体,它将+替换为-/替换为_,并且去掉了尾部可能出现的=字符,以适应URL和URI的要求。例如,上述头部编码后可能看起来像这样:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

载荷(Payload)

载荷部分是JWT存放实际信息的地方。这些信息被称为声明(claims),它可以是关于实体(通常是用户)和其他数据的陈述。JWT中有三种类型的声明:注册声明(registered claims)、公共声明(public claims)和私有声明(private claims)。

  • 注册声明:是一组预定义的声明,并不是强制要求的,但推荐使用。常见的注册声明有:

  • iss(issuer):签发者,标识令牌的签发主体,例如某个认证服务器。

  • exp(expiration time):过期时间,是一个Unix时间戳,表示令牌的过期时间点。超过这个时间,令牌将被视为无效。这在设置会话有效期等场景中非常有用。

  • sub(subject):主题,通常用于标识与令牌相关的主体,比如用户ID。

  • aud(audience):受众,指定令牌的接收方,可以是一个或多个字符串。例如,如果令牌是为特定的客户端应用程序设计的,那么该应用程序的标识符可以作为受众声明。

  • nbf(not before):生效时间,表示令牌在这个时间点之前是无效的,同样是Unix时间戳。

  • 公共声明:可以由使用JWT的各方定义,只要它们有唯一的名称以避免冲突。例如,你可以定义一个user_role声明来表示用户的角色。

  • 私有声明:是为了在同意使用它们的各方之间共享信息而创建的自定义声明。例如,一个特定应用可能定义app_specific_data这样的私有声明来传递应用内特定的数据。

以下是一个载荷的示例:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516239022 + 3600,
  "user_role": "admin"
}

在这个示例中,sub标识了用户ID,name是用户名,iat(issued at)表示令牌的签发时间,exp指定了过期时间,user_role是一个自定义的公共声明,表明用户的角色是管理员。与头部一样,载荷部分也会使用Base64Url编码,成为JWT的第二部分。例如,上述载荷编码后可能是:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjI+MzYwMCwidXNlcl9yb2xlIjoiYWRtaW4ifQ

签名(Signature)

为了创建签名部分,你需要使用编码后的头部、编码后的载荷、一个密钥(secret)以及头部中指定的签名算法。例如,如果使用的是HS256算法,签名的计算方式如下: HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) 签名有几个重要的作用:

  • 验证:接收方可以使用相同的算法和密钥对收到的JWT进行签名验证,确保令牌在传输过程中没有被篡改。如果签名验证失败,说明令牌可能被篡改或来自不可信的来源。
  • 认证:在使用私钥签名的情况下(如RSA算法),接收方可以通过验证签名来确认令牌确实是由持有对应私钥的一方签发的,从而实现身份认证。
  • 完整性:签名保证了JWT头部和载荷的完整性,任何对这两部分的修改都会导致签名验证失败。

JWT签名算法解析

JWT支持多种签名算法,不同的算法适用于不同的场景。下面详细介绍几种常见的签名算法。

HMAC系列算法

HMAC(Hash - based Message Authentication Code)算法使用一个共享密钥和哈希函数来生成消息认证码。在JWT中,常用的HMAC算法是HMAC SHA系列,如HS256、HS384和HS512。以HS256为例,它使用SHA - 256哈希函数。

优点

  • 简单高效:计算速度快,适用于对性能要求较高且通信双方信任共享密钥的场景,比如在同一组织内部的不同服务之间传递令牌。
  • 对称密钥:使用相同的密钥进行签名和验证,密钥管理相对简单,只要确保密钥在通信双方之间安全共享即可。

缺点

  • 密钥分发:如果有多个服务需要验证JWT,共享密钥的分发和管理可能变得复杂,而且一旦密钥泄露,所有依赖该密钥验证的服务都会受到威胁。
  • 缺乏不可抵赖性:因为签名和验证使用相同的密钥,无法明确证明签名者的身份,在需要不可抵赖性的场景(如法律相关场景)中不太适用。

下面是使用Python和PyJWT库生成和验证HS256签名的JWT的代码示例:

import jwt
import datetime

# 生成JWT
def generate_jwt():
    payload = {
        "sub": "1234567890",
        "name": "John Doe",
        "iat": datetime.datetime.utcnow(),
        "exp": datetime.datetime.utcnow() + datetime.timedelta(seconds=3600)
    }
    secret = "your_secret_key"
    token = jwt.encode(payload, secret, algorithm='HS256')
    return token

# 验证JWT
def verify_jwt(token):
    secret = "your_secret_key"
    try:
        decoded = jwt.decode(token, secret, algorithms=['HS256'])
        return decoded
    except jwt.ExpiredSignatureError:
        print("Token has expired")
    except jwt.InvalidTokenError:
        print("Invalid token")


if __name__ == "__main__":
    generated_token = generate_jwt()
    print("Generated Token:", generated_token)
    verified_payload = verify_jwt(generated_token)
    if verified_payload:
        print("Verified Payload:", verified_payload)

在上述代码中,generate_jwt函数生成一个包含用户信息和过期时间的JWT,使用指定的密钥和HS256算法进行签名。verify_jwt函数则用于验证接收到的JWT的有效性,包括签名验证和过期时间检查。

RSA系列算法

RSA(Rivest - Shamir - Adleman)是一种非对称加密算法,它使用一对密钥:公钥(public key)和私钥(private key)。在JWT中,使用RSA算法进行签名时,签发者使用私钥对JWT进行签名,验证者使用公钥来验证签名。常见的RSA算法变体有RS256、RS384和RS512,分别对应使用SHA - 256、SHA - 384和SHA - 512哈希函数。

优点

  • 安全性高:非对称密钥对的使用提供了更高的安全性,特别是在涉及第三方认证的场景中,公钥可以公开分发,只有持有私钥的一方才能进行签名。
  • 不可抵赖性:由于只有私钥持有者才能进行签名,签名者无法否认自己签发了该JWT,这在一些需要法律认可的场景中非常重要。

缺点

  • 性能较低:与HMAC算法相比,RSA算法的计算量较大,特别是在处理大量JWT时,可能会对服务器性能产生一定影响。
  • 密钥管理复杂:需要妥善管理公钥和私钥,私钥必须严格保密,公钥的分发也需要确保其真实性和完整性。

以下是使用Java和JJWT库生成和验证RS256签名的JWT的代码示例:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

import java.security.Key;
import java.util.Date;

public class JwtExample {
    private static final Key privateKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    private static final Key publicKey = Keys.hmacShaKeyFor(privateKey.getEncoded());

    // 生成JWT
    public static String generateJwt() {
        Date now = new Date();
        Date expiration = new Date(now.getTime() + 3600000);

        Claims claims = Jwts.claims()
               .setSubject("1234567890")
               .put("name", "John Doe");
        claims.putIssuedAt(now);
        claims.putExpiration(expiration);

        return Jwts.builder()
               .setClaims(claims)
               .signWith(privateKey, SignatureAlgorithm.RS256)
               .compact();
    }

    // 验证JWT
    public static void verifyJwt(String token) {
        try {
            Claims claims = Jwts.parserBuilder()
                   .setSigningKey(publicKey)
                   .build()
                   .parseClaimsJws(token)
                   .getBody();
            System.out.println("Verified Payload: " + claims);
        } catch (Exception e) {
            System.out.println("Invalid token: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        String generatedToken = generateJwt();
        System.out.println("Generated Token: " + generatedToken);
        verifyJwt(generatedToken);
    }
}

在这个Java代码示例中,generateJwt方法使用私钥对包含用户信息和过期时间的载荷进行RS256签名生成JWT。verifyJwt方法则使用公钥来验证接收到的JWT的有效性。

ECDSA系列算法

椭圆曲线数字签名算法(Elliptic Curve Digital Signature Algorithm,ECDSA)也是一种非对称加密算法,基于椭圆曲线密码学。在JWT中,常见的ECDSA算法变体有ES256、ES384和ES512,分别对应使用不同的椭圆曲线和哈希函数。

优点

  • 安全性高:在相同的密钥长度下,椭圆曲线密码学提供了比RSA更高的安全性,特别是在移动设备和资源受限的环境中,其密钥长度可以更短,同时保持较高的安全性。
  • 性能较好:相较于RSA,ECDSA在签名和验证过程中的计算量较小,性能更优,适合对性能和安全性都有要求的场景。

缺点

  • 兼容性:某些较旧的系统或库可能对ECDSA的支持不够完善,在使用时需要确保相关系统和库的兼容性。
  • 密钥生成复杂:椭圆曲线密钥的生成相对复杂,需要更多的数学知识和特定的算法来生成安全的密钥。

以下是使用Node.js和jsonwebtoken库生成和验证ES256签名的JWT的代码示例:

const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// 生成私钥和公钥
const privateKey = crypto.generateKeyPairSync('ec', {
    namedCurve: 'P - 256',
    publicKeyEncoding: {
        type: 'spki',
        format: 'pem'
    },
    privateKeyEncoding: {
        type: 'pkcs8',
        format: 'pem'
    }
});

// 生成JWT
function generateJwt() {
    const payload = {
        sub: '1234567890',
        name: 'John Doe',
        iat: Math.floor(Date.now() / 1000),
        exp: Math.floor(Date.now() / 1000) + 3600
    };
    return jwt.sign(payload, privateKey.privateKey, { algorithm: 'ES256' });
}

// 验证JWT
function verifyJwt(token) {
    try {
        const decoded = jwt.verify(token, privateKey.publicKey, { algorithms: ['ES256'] });
        console.log('Verified Payload:', decoded);
    } catch (error) {
        console.log('Invalid token:', error.message);
    }
}

const generatedToken = generateJwt();
console.log('Generated Token:', generatedToken);
verifyJwt(generatedToken);

在Node.js代码中,首先使用crypto模块生成ECDSA的私钥和公钥。generateJwt函数使用私钥对载荷进行ES256签名生成JWT,verifyJwt函数则使用公钥验证JWT的有效性。

JWT在后端开发中的应用场景

JWT在后端开发中有多种应用场景,以下是一些常见的场景:

用户认证与授权

  • 认证:当用户登录系统时,后端服务器验证用户的凭据(如用户名和密码)。如果验证成功,服务器生成一个包含用户身份信息(如用户ID、用户名等)的JWT,并将其返回给客户端。客户端在后续的请求中,将JWT包含在请求头(通常是Authorization头,格式为Bearer <token>)中发送给后端。后端服务器接收到请求后,验证JWT的签名和有效性。如果验证通过,就可以确认请求来自已认证的用户。
  • 授权:JWT的载荷中可以包含用户的角色信息(如adminuser等)或权限声明。后端服务器在接收到请求并验证JWT有效后,根据JWT中的角色或权限信息,决定是否允许用户执行请求的操作。例如,只有具有admin角色的用户才能访问某些管理接口。

单点登录(SSO)

在单点登录系统中,多个应用系统共享一个身份认证服务。当用户在其中一个应用系统登录成功后,认证服务生成一个JWT并返回给该应用系统。这个JWT可以在其他相关的应用系统之间共享,用户在访问其他应用系统时,无需再次登录。每个应用系统只需验证JWT的有效性和签名,即可确认用户的身份。这样可以提高用户体验,减少用户在不同应用系统之间重复登录的麻烦。

跨服务通信

在微服务架构中,不同的服务之间可能需要相互通信并验证对方的身份。例如,服务A需要调用服务B的接口。服务A可以生成一个包含自身身份信息和相关请求数据的JWT,并将其发送给服务B。服务B接收到请求后,验证JWT的签名和有效性,确认请求来自可信的服务A,然后处理请求。这种方式可以确保微服务之间通信的安全性和可靠性。

JWT的安全性考量

虽然JWT提供了一种方便且安全的信息传输方式,但在使用过程中仍需要注意一些安全性问题。

密钥管理

  • 密钥保密:对于HMAC算法,共享密钥必须严格保密,不能泄露给未经授权的第三方。对于RSA和ECDSA等非对称算法,私钥必须妥善保管,只有授权的服务器才能持有私钥进行签名。公钥的分发也需要确保其真实性,防止中间人替换公钥进行攻击。
  • 密钥更新:定期更新密钥可以降低密钥泄露带来的风险。特别是在长期运行的系统中,即使密钥目前没有泄露,随着时间推移,被破解的可能性也会增加。当更新密钥时,需要确保新旧密钥的过渡过程平稳,不会影响系统的正常运行。

防止重放攻击

重放攻击是指攻击者截取并重新发送合法的JWT,以达到未经授权访问的目的。为了防止重放攻击,可以在JWT中添加一个唯一的标识符(如jti声明),并在服务器端维护一个已使用过的JWT标识符列表。每次接收到JWT时,服务器检查jti是否在已使用列表中,如果存在,则拒绝该JWT。另外,设置较短的JWT过期时间也可以减轻重放攻击的风险,因为过期的JWT将无法通过验证。

防止篡改攻击

虽然JWT的签名机制可以检测到令牌是否被篡改,但在传输过程中仍需防止中间人篡改JWT。使用HTTPS协议进行通信可以确保数据在传输过程中的加密和完整性,防止中间人篡改JWT的内容。同时,在验证JWT时,不仅要验证签名,还要检查JWT的其他声明(如expnbf等)是否符合预期,以确保令牌的有效性。

限制JWT的使用范围

在JWT的载荷中,通过aud(受众)声明指定令牌的接收方。服务器在验证JWT时,除了验证签名和其他基本声明外,还应检查aud声明是否与自身匹配。这样可以确保JWT只能被预期的接收方使用,防止令牌被意外或恶意地用于其他服务。

JWT与其他认证方式的比较

与传统的基于会话(session - based)的认证方式相比,JWT有其独特的优势和劣势。

优势

  • 无状态:JWT是无状态的,服务器不需要在内存中存储用户的会话信息。这使得服务器更容易进行水平扩展,因为每个请求都包含了足够的认证和授权信息,服务器无需依赖共享的会话存储。而基于会话的认证方式需要在服务器端维护会话状态,在分布式系统中,会话的同步和管理可能变得复杂。
  • 便于跨域使用:由于JWT是自包含的,并且通常通过HTTP头传递,它在跨域场景中更容易使用。相比之下,基于会话的认证可能会受到跨域限制的影响,因为会话通常依赖于Cookie,而Cookie在跨域时存在诸多限制。
  • 易于理解和实现:JWT的结构简单,采用JSON格式,开发人员容易理解和实现。无论是生成、解析还是验证JWT,都有丰富的库支持,降低了开发成本。

劣势

  • 数据体积较大:JWT通常比基于会话的认证方式产生的数据量更大,因为它需要在令牌中包含所有相关的用户信息和声明。这可能会增加网络传输的开销,特别是在移动设备或网络带宽有限的环境中。
  • 令牌存储问题:客户端需要存储JWT,并且由于JWT通常在有效期内一直有效,客户端存储的JWT存在被盗用的风险。而基于会话的认证方式,服务器可以更灵活地控制会话的有效期,例如在用户注销时立即销毁会话。

与OAuth 2.0相比,JWT与OAuth 2.0并不相互排斥,实际上,JWT可以作为OAuth 2.0中的一种令牌类型。

  • OAuth 2.0的优势:OAuth 2.0是一个更全面的授权框架,它定义了多种角色(如资源所有者、授权服务器、资源服务器等)和流程,适用于复杂的第三方授权场景,例如用户授权第三方应用访问自己在某个服务中的资源。
  • JWT的优势:JWT本身更侧重于信息的安全传输和自包含验证,它可以在OAuth 2.0的框架中作为承载令牌(Bearer Token),为OAuth 2.0提供更简洁、自验证的令牌形式。例如,在OAuth 2.0的授权码模式中,授权服务器颁发的访问令牌可以是一个JWT,资源服务器可以直接验证JWT的签名和有效性,而无需与授权服务器进行额外的交互来验证令牌。

总结

JWT作为一种在后端开发中广泛应用的安全认证和信息传输方式,其结构清晰、签名算法多样,适用于多种应用场景。理解JWT的结构和签名算法,对于正确使用JWT进行安全认证至关重要。在使用过程中,需要充分考虑其安全性,妥善管理密钥,防止各种安全攻击。与其他认证方式相比,JWT有其独特的优势和劣势,开发人员应根据具体的业务需求和系统架构选择合适的认证方式。通过合理运用JWT,可以提高后端系统的安全性、可扩展性和用户体验。

希望通过本文的介绍,读者对JWT令牌的结构与签名算法有了更深入的理解,并能够在实际项目中安全、有效地应用JWT。