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

JWT中的加密与解密技术解析

2023-02-217.2k 阅读

JWT基础概念

JWT,即JSON Web Token,是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以JSON对象的形式安全地传输信息。这些信息可以被验证和信任,因为它们是经过数字签名的。JWT通常由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。

  • 头部(Header):一般包含两部分信息,令牌的类型(即JWT)以及所使用的签名算法,例如HMAC SHA256或RSA。一个典型的头部JSON对象如下:
{
  "alg": "HS256",
  "typ": "JWT"
}

之后这个JSON对象会被Base64Url编码,形成JWT的第一部分。

  • 载荷(Payload):这部分是存放实际需要传递的数据的地方。JWT定义了一组预定义的声明(claims),例如iss(签发者)、exp(过期时间)、sub(主题)等,但也可以包含自定义的声明。一个简单的载荷示例如下:
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

同样,这个JSON对象也会被Base64Url编码,成为JWT的第二部分。

  • 签名(Signature):要创建签名部分,需要使用编码后的头部、编码后的载荷、一个密钥(secret)和头部中指定的签名算法。例如,如果使用HMAC SHA256算法,签名会按如下方式创建:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

签名用于验证消息在传输过程中有没有被更改,并且,在使用私钥签名的情况下,还可以验证JWT的发送者的身份。

加密与解密在JWT中的重要性

  • 数据完整性保护:通过签名过程,接收方可以验证JWT在传输过程中是否被篡改。如果有人恶意修改了头部或载荷部分,签名验证将会失败,因为重新计算的签名与原始签名不匹配。这确保了数据从发送方到接收方的完整性。

  • 身份验证:JWT的签名可以用来验证发送者的身份。例如,在使用私钥进行签名的情况下,只有持有对应私钥的一方才能生成有效的签名。服务器可以使用公钥来验证签名,从而确认JWT确实来自可信的发送者,进而实现身份验证。

  • 防止重放攻击:虽然JWT本身并没有直接防止重放攻击的机制,但通过合理设置exp(过期时间)声明,可以限制JWT的有效时间。一旦JWT过期,接收方将拒绝接受该令牌,从而防止攻击者使用旧的JWT进行重放攻击。

JWT中的加密技术

对称加密

  • 原理:对称加密使用同一个密钥进行加密和解密。在JWT的场景中,当使用对称加密算法(如HMAC)时,发送方和接收方都必须知道并使用相同的密钥。发送方使用密钥对头部和载荷进行签名,生成JWT的签名部分。接收方收到JWT后,使用相同的密钥重新计算签名,并与接收到的签名进行比较。如果两者匹配,则说明JWT是可信的。

  • 代码示例(以Python和PyJWT库为例): 首先,确保安装了PyJWT库:

pip install PyJWT

以下是使用对称加密(HS256算法)生成JWT的代码:

import jwt
import datetime

# 定义密钥
secret_key = "your_secret_key"

# 定义载荷
payload = {
    "sub": "1234567890",
    "name": "John Doe",
    "iat": datetime.datetime.utcnow(),
    "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
}

# 生成JWT
token = jwt.encode(
    payload,
    secret_key,
    algorithm='HS256'
)

print(token)

在上述代码中,我们使用jwt.encode方法生成JWT。payload是我们要包含在JWT中的数据,secret_key是对称加密的密钥,algorithm='HS256'指定了使用的对称加密算法为HMAC SHA256。

非对称加密

  • 原理:非对称加密使用一对密钥,即公钥和私钥。私钥用于对数据进行签名,而公钥用于验证签名。在JWT中,发送方使用私钥对头部和载荷进行签名,生成JWT。接收方使用对应的公钥来验证签名。这种方式特别适用于需要验证发送者身份的场景,因为只有持有私钥的一方才能生成有效的签名。

  • 代码示例(以Python和PyJWT库为例): 首先,需要生成一对RSA密钥。可以使用OpenSSL工具生成:

openssl genrsa -out private_key.pem 2048
openssl rsa -in private_key.pem -outform PEM -pubout -out public_key.pem

以下是使用非对称加密(RS256算法)生成JWT的代码:

import jwt
import datetime
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization

# 读取私钥
with open('private_key.pem', 'rb') as key_file:
    private_key = serialization.load_pem_private_key(
        key_file.read(),
        password=None,
        backend=default_backend()
    )

# 定义载荷
payload = {
    "sub": "1234567890",
    "name": "John Doe",
    "iat": datetime.datetime.utcnow(),
    "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
}

# 生成JWT
token = jwt.encode(
    payload,
    private_key,
    algorithm='RS256'
)

print(token)

以下是使用公钥验证JWT签名的代码:

import jwt
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization

# 读取公钥
with open('public_key.pem', 'rb') as key_file:
    public_key = serialization.load_pem_public_key(
        key_file.read(),
        backend=default_backend()
    )

# 假设这是接收到的JWT
token = "your_received_token"

try:
    decoded = jwt.decode(
        token,
        public_key,
        algorithms=['RS256']
    )
    print(decoded)
except jwt.ExpiredSignatureError:
    print('Token has expired')
except jwt.InvalidSignatureError:
    print('Invalid signature')

在上述代码中,生成JWT时使用私钥进行签名,验证JWT时使用公钥进行验证。

JWT中的解密技术

对称解密

在对称加密的JWT中,解密过程实际上就是验证签名的过程。接收方使用与发送方相同的密钥,按照与签名生成时相同的算法(如HMAC SHA256)重新计算签名。如果重新计算的签名与接收到的JWT中的签名一致,则说明JWT是有效的,并且数据没有被篡改。

  • 代码示例(以Python和PyJWT库为例)
import jwt

# 定义密钥
secret_key = "your_secret_key"

# 假设这是接收到的JWT
token = "your_received_token"

try:
    decoded = jwt.decode(
        token,
        secret_key,
        algorithms=['HS256']
    )
    print(decoded)
except jwt.ExpiredSignatureError:
    print('Token has expired')
except jwt.InvalidSignatureError:
    print('Invalid signature')

在上述代码中,jwt.decode方法尝试使用给定的密钥和算法对JWT进行解密(验证签名)。如果签名有效且JWT未过期,则会返回解码后的载荷。

非对称解密

在非对称加密的JWT中,解密同样是验证签名的过程。接收方使用发送方提供的公钥,按照与签名生成时相同的算法(如RS256)来验证签名。如果签名验证通过,则说明JWT是可信的。

  • 代码示例(以Python和PyJWT库为例)
import jwt
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization

# 读取公钥
with open('public_key.pem', 'rb') as key_file:
    public_key = serialization.load_pem_public_key(
        key_file.read(),
        backend=default_backend()
    )

# 假设这是接收到的JWT
token = "your_received_token"

try:
    decoded = jwt.decode(
        token,
        public_key,
        algorithms=['RS256']
    )
    print(decoded)
except jwt.ExpiredSignatureError:
    print('Token has expired')
except jwt.InvalidSignatureError:
    print('Invalid signature')

在这个示例中,jwt.decode方法使用公钥对JWT进行验证,以确认其有效性。

JWT加密与解密中的安全考虑

密钥管理

  • 对称密钥:在对称加密中,密钥的保密性至关重要。因为发送方和接收方使用相同的密钥,如果密钥泄露,攻击者就可以伪造JWT。密钥应该妥善存储,例如使用环境变量、密钥管理服务(KMS)等方式。并且,密钥应该定期更换,以降低密钥泄露带来的风险。

  • 非对称密钥:私钥必须严格保密,只有签名方应该持有私钥。公钥可以公开分发,但也要确保其来源的可靠性,以防止中间人攻击。同样,私钥也应该定期更换,并且在私钥泄露的情况下,要及时采取措施,如撤销相关的JWT。

算法选择

  • 应选择经过广泛认可和安全审计的加密算法,如HMAC SHA256、RS256等。避免使用已经被证明存在安全漏洞的算法。同时,要关注算法的更新和安全公告,及时调整使用的算法。

防止JWT泄露

  • 传输安全:在传输JWT时,应使用安全的通信协议,如HTTPS。这样可以防止JWT在传输过程中被窃听或篡改。

  • 存储安全:如果在客户端存储JWT(如在浏览器的LocalStorage或Cookie中),要注意保护这些存储位置的安全。例如,避免在不安全的页面中访问存储JWT的地方,并且可以使用HttpOnly Cookie来防止JWT被JavaScript读取,从而降低XSS攻击的风险。

JWT在不同后端框架中的应用

在Node.js(Express框架)中的应用

  • 安装依赖
npm install jsonwebtoken
  • 生成JWT示例
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();

// 定义密钥
const secretKey = 'your_secret_key';

// 模拟用户登录
app.post('/login', (req, res) => {
    const { username, password } = req.body;
    // 简单的用户验证
    if (username === 'admin' && password === 'password') {
        const payload = {
            username: 'admin',
            role: 'admin'
        };
        const token = jwt.sign(payload, secretKey, { expiresIn: '1h' });
        res.json({ token });
    } else {
        res.status(401).json({ message: 'Invalid credentials' });
    }
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});
  • 验证JWT示例
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();

// 定义密钥
const secretKey = 'your_secret_key';

// 中间件用于验证JWT
const verifyToken = (req, res, next) => {
    const token = req.headers['authorization'];
    if (!token) {
        return res.status(401).json({ message: 'Token is missing' });
    }
    const bearerToken = token.split(' ')[1];
    jwt.verify(bearerToken, secretKey, (err, decoded) => {
        if (err) {
            return res.status(401).json({ message: 'Invalid token' });
        }
        req.decoded = decoded;
        next();
    });
};

// 受保护的路由
app.get('/protected', verifyToken, (req, res) => {
    res.json({ message: 'This is a protected route', user: req.decoded });
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在上述代码中,首先使用jsonwebtoken库生成JWT,然后通过中间件验证JWT,保护特定的路由。

在Java(Spring Boot框架)中的应用

  • 添加依赖:在pom.xml中添加以下依赖:
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
  • 生成JWT示例
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

@RestController
public class AuthController {

    private static final String SECRET_KEY = "your_secret_key";

    @PostMapping("/login")
    public String login(@RequestBody User user) {
        if ("admin".equals(user.getUsername()) && "password".equals(user.getPassword())) {
            Claims claims = Jwts.claims().setSubject(user.getUsername());
            claims.put("role", "admin");
            Date now = new Date();
            Date expiration = new Date(now.getTime() + 60 * 60 * 1000);

            String token = Jwts.builder()
                   .setClaims(claims)
                   .setIssuedAt(now)
                   .setExpiration(expiration)
                   .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                   .compact();
            return token;
        } else {
            return "Invalid credentials";
        }
    }
}

class User {
    private String username;
    private String password;

    // getters and setters
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
  • 验证JWT示例
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProtectedController {

    private static final String SECRET_KEY = "your_secret_key";

    @GetMapping("/protected")
    public String protectedResource(@RequestHeader("Authorization") String token) {
        try {
            String bearerToken = token.substring(7);
            Claims claims = Jwts.parserBuilder()
                   .setSigningKey(SECRET_KEY)
                   .build()
                   .parseClaimsJws(bearerToken)
                   .getBody();
            return "This is a protected route. User: " + claims.getSubject();
        } catch (SignatureException e) {
            return "Invalid token";
        }
    }
}

在Java Spring Boot示例中,同样实现了JWT的生成和验证过程,保护特定的资源。

通过以上对JWT中加密与解密技术的详细解析以及在不同后端框架中的应用示例,开发者可以更深入地理解和应用JWT,确保后端开发中的安全认证环节更加可靠。同时,要始终关注安全最佳实践,不断优化JWT的使用,以应对日益复杂的网络安全威胁。