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

JWT在Serverless架构中的使用

2024-09-254.9k 阅读

Serverless 架构概述

Serverless 架构简介

Serverless 架构并非意味着完全没有服务器,而是开发者无需关心服务器的管理与维护。在这种架构下,云服务提供商负责服务器资源的动态分配、扩展与管理。开发者只需专注于编写业务逻辑代码,将其以函数的形式上传至云平台。这些函数在事件驱动的模式下运行,即当特定事件(如 HTTP 请求、文件上传等)发生时,云平台自动触发相应的函数执行。

例如,AWS Lambda 就是 Serverless 架构中函数计算的典型代表。开发者可以编写 Python、Node.js 等多种语言的函数代码,并部署到 AWS Lambda 平台。当有 API Gateway 转发过来的 HTTP 请求时,对应的 Lambda 函数就会被触发执行。

Serverless 架构的优势

  1. 成本效益:传统架构下,即使应用处于低负载状态,也需要维持服务器资源以保证可用性,这意味着固定成本的支出。而 Serverless 架构按实际使用的资源量计费,在应用空闲时几乎不产生成本。例如,一个小型创业公司的后端 API 服务,在业务初期请求量较低,使用 Serverless 架构可以显著降低成本。随着业务发展,请求量增加,成本也会根据实际使用量线性增长,无需提前规划和投入大量硬件资源。
  2. 可扩展性:Serverless 架构具备自动弹性扩展的能力。云平台会根据事件的触发频率自动调整函数的执行实例数量。以电商应用的促销活动为例,在活动期间,订单创建、商品查询等操作的请求量会大幅增加。Serverless 架构下,相关的函数可以迅速扩展实例数量以应对高并发,活动结束后,实例数量又会自动减少,避免资源浪费。
  3. 开发效率:开发者无需关注服务器的配置、维护和扩展等繁琐工作,能够将更多精力投入到业务逻辑的实现上。同时,Serverless 架构通常提供丰富的 API 和 SDK,便于与其他云服务集成。例如,开发一个文件处理应用,利用 Serverless 架构可以轻松集成云存储服务(如 AWS S3),快速实现文件上传、下载和处理功能。

Serverless 架构面临的安全挑战

  1. 身份认证与授权:由于 Serverless 架构下函数是分布式、事件驱动的,传统基于服务器的集中式认证方式难以适用。每个函数可能需要独立验证请求的合法性,确保只有授权用户能够访问特定函数或资源。例如,在一个多租户的 Serverless 应用中,不同租户的用户应该只能访问自己的数据相关的函数,这就需要精确的身份认证和授权机制。
  2. 数据安全:数据在函数之间传输以及存储过程中存在风险。函数可能处理敏感信息,如用户的个人资料、金融数据等。若数据在传输过程中未加密,或者存储时未进行妥善保护,可能导致数据泄露。例如,函数从数据库读取用户的信用卡信息进行支付处理,如果传输过程没有加密,就可能被中间人窃取。
  3. 函数安全:上传至云平台的函数代码可能存在漏洞,如 SQL 注入、代码注入等。恶意攻击者可能利用这些漏洞执行恶意代码,获取敏感信息或控制整个应用。同时,函数的依赖项也可能带来安全风险,若使用的开源库存在已知漏洞,可能影响整个应用的安全性。

JWT 原理与基础

JWT 概念

JSON Web Token(JWT)是一种开放标准(RFC 7519),用于在网络应用环境间安全地传输声明。它通常由三部分组成:头部(Header)、载荷(Payload)和签名(Signature),并以点(.)分隔,形成 xxx.yyy.zzz 这样的字符串格式。

JWT 的组成部分

  1. 头部(Header):通常包含两部分信息,一是令牌的类型,如 JWT;二是使用的签名算法,如 HMAC SHA256 或 RSA。以下是一个 JSON 格式的头部示例:
{
  "alg": "HS256",
  "typ": "JWT"
}

然后,这个 JSON 对象会被 Base64Url 编码,形成 JWT 的第一部分。 2. 载荷(Payload):载荷是存放实际需要传递的数据的地方。它可以包含标准声明(如 iss - 发行人、exp - 过期时间、sub - 主题等),也可以包含自定义声明。例如:

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

同样,这个 JSON 对象会被 Base64Url 编码,形成 JWT 的第二部分。需要注意的是,虽然载荷是经过编码的,但任何人都可以解码查看其中内容,所以不要在载荷中放置敏感信息,如密码等。 3. 签名(Signature):要创建签名部分,需要使用编码后的头部、编码后的载荷、一个密钥(secret)和头部中指定的签名算法。例如,如果使用 HMAC SHA256 算法,签名将按如下方式创建:

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

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

JWT 的工作流程

  1. 用户认证:用户在客户端输入凭证(如用户名和密码),客户端将这些凭证发送到认证服务器。认证服务器验证凭证,如果凭证有效,生成一个 JWT。
  2. JWT 传递:认证服务器将生成的 JWT 返回给客户端。客户端通常会将 JWT 存储在本地,如 localStorage 或 cookie 中(注意 localStorage 存在一定安全风险,建议使用 httpOnly cookie 存储 JWT 以防止 XSS 攻击窃取 JWT)。
  3. 请求授权:当客户端需要访问受保护的资源时,它会在请求头中带上 JWT。例如,在 HTTP 请求中,将 JWT 放在 Authorization 头字段中,格式为 Bearer <JWT>。服务器接收到请求后,提取 JWT,并验证其有效性。如果 JWT 有效,服务器将允许客户端访问资源;否则,返回未授权错误。

JWT 在 Serverless 架构中的应用优势

无状态性与 Serverless 架构契合

  1. Serverless 架构的无状态特性:Serverless 架构中的函数是无状态的,每次函数调用都是独立的,不依赖于之前调用的状态。这使得函数可以在不同的实例上运行,便于云平台进行弹性扩展。
  2. JWT 的无状态优势:JWT 本身也是无状态的。服务器在验证 JWT 时,不需要在服务器端存储额外的会话信息。它只需要使用密钥对 JWT 进行签名验证,就可以确定 JWT 的有效性和其中包含的声明信息。这种无状态性与 Serverless 架构的无状态特性完美契合。例如,在一个由多个 Serverless 函数组成的 API 服务中,每个函数都可以独立验证 JWT,无需在函数间共享会话状态,提高了系统的可扩展性和灵活性。

跨函数与跨服务认证

  1. Serverless 架构中的函数与服务交互:在 Serverless 架构中,一个完整的业务流程可能由多个函数和不同的云服务协同完成。例如,一个文件上传和处理的流程,可能涉及到 API Gateway 接收文件上传请求,触发 Lambda 函数将文件存储到 S3 存储桶,然后另一个 Lambda 函数从 S3 读取文件进行处理。
  2. JWT 实现跨函数与跨服务认证:JWT 可以在整个业务流程中传递,实现跨函数和跨服务的统一认证。当用户通过 API Gateway 发起请求时,携带的 JWT 可以在后续触发的各个 Lambda 函数以及与 S3 等服务交互时,用于验证请求的合法性。这样,无需为每个函数或服务单独设置认证机制,简化了认证流程,提高了系统的安全性和可维护性。

减轻后端服务压力

  1. 传统认证方式的压力:在传统的服务器架构中,认证通常依赖于服务器端的会话管理,这需要服务器存储大量的会话信息,并在每次请求时查询这些信息以验证用户身份。随着用户数量和请求量的增加,服务器的负载会显著上升。
  2. JWT 减轻压力的原理:在 Serverless 架构中使用 JWT,服务器无需存储会话信息,只需要验证 JWT 的签名。由于 JWT 的验证过程相对简单,并且可以在函数内部快速完成,大大减轻了后端服务的压力。特别是在高并发场景下,这种优势更加明显,有助于提高整个系统的性能和响应速度。

JWT 在 Serverless 架构中的实现

选择合适的 JWT 库

  1. 不同语言的 JWT 库:在 Serverless 架构中,根据所使用的编程语言,有多种 JWT 库可供选择。
    • Node.jsjsonwebtoken 是一个广泛使用的 JWT 库。它提供了简单易用的 API 来生成、验证和解析 JWT。例如,可以通过以下方式安装:npm install jsonwebtoken
    • PythonPyJWT 是 Python 中常用的 JWT 库。可以使用 pip install PyJWT 进行安装。
    • Javajjwt - apijjwt - impl 是 Java 中用于处理 JWT 的库。可以通过 Maven 或 Gradle 引入依赖。
  2. 库的功能特性考量:选择 JWT 库时,除了语言支持外,还需考虑其功能特性。例如,库是否支持多种签名算法,是否具备良好的错误处理机制,以及是否易于与现有的 Serverless 框架集成等。例如,jsonwebtoken 库在 Node.js 中不仅支持常见的 HMAC 和 RSA 签名算法,还提供了丰富的选项来处理过期时间、自定义声明等,方便在 Serverless 函数中灵活使用。

在 Serverless 函数中生成 JWT

  1. Node.js 示例:以 AWS Lambda 函数使用 Node.js 为例,假设我们有一个用户认证函数,当用户认证成功后生成 JWT。首先确保安装了 jsonwebtoken 库。
const jwt = require('jsonwebtoken');
exports.handler = async (event, context) => {
    // 模拟用户认证逻辑,假设用户名和密码正确
    const { username, password } = JSON.parse(event.body);
    if (username === 'validUser' && password === 'validPassword') {
        const payload = {
            username: 'validUser',
            role: 'user'
        };
        const secret = 'your - secret - key';
        const token = jwt.sign(payload, secret, { expiresIn: '1h' });
        return {
            statusCode: 200,
            body: JSON.stringify({ token })
        };
    } else {
        return {
            statusCode: 401,
            body: JSON.stringify({ message: 'Unauthorized' })
        };
    }
};
  1. Python 示例:在 AWS Lambda 函数使用 Python 时,使用 PyJWT 库生成 JWT。假设我们有一个 Flask 应用包装在 Lambda 函数中(使用 flask - lambda - wsgi 等工具)。
import jwt
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    if username == 'validUser' and password == 'validPassword':
        payload = {
            'username': 'validUser',
            'role': 'user'
        }
        secret = 'your - secret - key'
        token = jwt.encode(payload, secret, algorithm='HS256')
        return jsonify({'token': token}), 200
    else:
        return jsonify({'message': 'Unauthorized'}), 401


def lambda_handler(event, context):
    from flask_lambda_wsgi import handle_request
    return handle_request(app, event, context)

在 Serverless 函数中验证 JWT

  1. Node.js 示例:在一个需要保护的 Node.js AWS Lambda 函数中验证 JWT。假设请求头中包含 Authorization 字段,格式为 Bearer <JWT>
const jwt = require('jsonwebtoken');
exports.handler = async (event, context) => {
    const token = event.headers.Authorization && event.headers.Authorization.split(' ')[1];
    if (!token) {
        return {
            statusCode: 401,
            body: JSON.stringify({ message: 'No token provided' })
        };
    }
    const secret = 'your - secret - key';
    try {
        const decoded = jwt.verify(token, secret);
        // 验证成功,继续处理业务逻辑
        return {
            statusCode: 200,
            body: JSON.stringify({ message: 'Authenticated successfully', user: decoded })
        };
    } catch (err) {
        return {
            statusCode: 401,
            body: JSON.stringify({ message: 'Invalid token' })
        };
    }
};
  1. Python 示例:在 Python 的 AWS Lambda 函数中验证 JWT。同样假设请求头中有 Authorization 字段。
import jwt
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/protected', methods=['GET'])
def protected():
    token = request.headers.get('Authorization')
    if not token:
        return jsonify({'message': 'No token provided'}), 401
    token = token.split(' ')[1]
    secret = 'your - secret - key'
    try:
        decoded = jwt.decode(token, secret, algorithms=['HS256'])
        return jsonify({'message': 'Authenticated successfully', 'user': decoded}), 200
    except jwt.ExpiredSignatureError:
        return jsonify({'message': 'Token has expired'}), 401
    except jwt.InvalidTokenError:
        return jsonify({'message': 'Invalid token'}), 401


def lambda_handler(event, context):
    from flask_lambda_wsgi import handle_request
    return handle_request(app, event, context)

JWT 在 Serverless 架构中的安全注意事项

密钥管理

  1. 密钥的重要性:JWT 的安全性很大程度上依赖于密钥的保密性。如果密钥泄露,攻击者可以伪造有效的 JWT,从而访问受保护的资源。在 Serverless 架构中,由于函数可能分布在不同的环境中运行,密钥的管理变得尤为重要。
  2. 密钥存储与保护
    • 使用云服务的密钥管理服务:许多云服务提供商提供了密钥管理服务,如 AWS Key Management Service(KMS)、Google Cloud KMS 等。可以将 JWT 密钥存储在这些服务中,并通过 API 进行访问。这些服务提供了加密存储、访问控制和审计等功能,增强了密钥的安全性。
    • 避免硬编码密钥:在 Serverless 函数代码中,绝对不要硬编码 JWT 密钥。硬编码的密钥容易在代码泄露时导致安全风险。例如,将密钥存储在环境变量中,在函数运行时从环境变量中读取密钥。在 AWS Lambda 中,可以通过 Lambda 控制台的环境变量设置来存储密钥,函数代码可以通过 process.env.SECRET_KEY(Node.js)或 os.environ.get('SECRET_KEY')(Python)来获取密钥。

防止 JWT 劫持

  1. JWT 劫持风险:JWT 通常存储在客户端,如 localStorage 或 cookie 中。如果客户端存在漏洞,如 XSS 漏洞,攻击者可能窃取 JWT,然后使用它来访问受保护的资源,这就是 JWT 劫持风险。
  2. 防范措施
    • 使用 httpOnly cookie:如果将 JWT 存储在 cookie 中,设置 httpOnly 属性为 true。这样,JWT 无法通过 JavaScript 访问,降低了 XSS 攻击窃取 JWT 的风险。例如,在 Node.js 的 Express 应用中,可以通过以下方式设置 cookie:
const express = require('express');
const app = express();
app.use((req, res) => {
    res.cookie('jwt', 'your - jwt - token', { httpOnly: true });
    res.send('Cookie set');
});
  • 安全的传输协议:确保在客户端和服务器之间传输 JWT 时使用 HTTPS 协议。HTTPS 对传输的数据进行加密,防止中间人窃取 JWT。

过期时间设置与刷新

  1. 合理设置过期时间:JWT 中的过期时间(exp 声明)应该根据应用的安全需求合理设置。如果过期时间过长,即使 JWT 被窃取,攻击者也有较长时间可以利用它访问资源;如果过期时间过短,用户可能需要频繁重新认证,影响用户体验。例如,对于一些对安全性要求较高的金融应用,JWT 的过期时间可以设置为较短,如 15 分钟;而对于一些普通的信息展示应用,过期时间可以设置为 1 小时或更长。
  2. JWT 刷新机制:为了在不过度影响用户体验的情况下保证安全性,可以实现 JWT 刷新机制。当 JWT 过期时,客户端可以使用一个刷新令牌(Refresh Token)来获取新的 JWT。刷新令牌通常存储在服务器端,并且有较长的过期时间。例如,在一个基于 OAuth 2.0 的认证流程中,客户端在获取 JWT 的同时也获取一个刷新令牌。当 JWT 过期时,客户端将刷新令牌发送到认证服务器,认证服务器验证刷新令牌有效后,生成新的 JWT 返回给客户端。

验证签名算法

  1. 签名算法验证的必要性:JWT 的头部中指定了签名算法,服务器在验证 JWT 时必须确保使用正确的签名算法。如果攻击者篡改了 JWT 头部的签名算法,服务器使用错误的算法验证,可能导致验证通过,从而使伪造的 JWT 能够访问受保护资源。
  2. 验证实现:在验证 JWT 时,从 JWT 头部提取签名算法,并确保使用的 JWT 库按照提取的算法进行验证。例如,在 jsonwebtoken 库中,verify 方法会根据 JWT 头部的算法自动选择正确的验证方式。但在一些自定义的验证逻辑中,需要开发者手动处理算法验证。在 Python 的 PyJWT 库中,可以通过以下方式验证签名算法:
import jwt

token = 'your - jwt - token'
try:
    decoded = jwt.decode(token, 'your - secret - key', algorithms=['HS256'])
except jwt.InvalidAlgorithmError:
    print('Invalid signature algorithm')

与其他安全机制结合

与身份提供商集成

  1. 身份提供商的作用:身份提供商(IdP)如 Okta、Auth0 等,可以提供集中式的用户身份管理和认证服务。在 Serverless 架构中与身份提供商集成,可以借助其强大的认证和授权功能,提高系统的安全性和可管理性。
  2. 集成方式
    • 使用 OpenID Connect(OIDC):许多身份提供商支持 OIDC 协议。在 Serverless 架构中,可以配置 API Gateway 或 Lambda 函数与 OIDC 兼容的身份提供商进行集成。例如,在 AWS API Gateway 中,可以配置 OIDC 身份验证,将用户认证请求转发到身份提供商。用户在身份提供商完成认证后,身份提供商返回包含用户信息的 JWT。API Gateway 验证 JWT 后,将请求转发到后端的 Lambda 函数,函数可以根据 JWT 中的用户信息进行授权决策。
    • SAML 集成:对于一些企业级应用,可能会使用 SAML 协议与身份提供商集成。通过配置 SAML 身份验证,Serverless 应用可以利用企业现有的 SAML 身份基础设施进行用户认证。例如,在一个企业内部的 Serverless 应用中,与企业的 Active Directory Federation Services(AD FS)集成,使用 SAML 协议实现单点登录(SSO)功能。

基于角色的访问控制(RBAC)

  1. RBAC 原理:基于角色的访问控制是一种广泛使用的授权模型。它将权限分配给角色,用户通过被分配到不同的角色来获取相应的权限。在 Serverless 架构中,结合 JWT 使用 RBAC 可以实现细粒度的访问控制。
  2. 结合 JWT 实现 RBAC:可以在 JWT 的载荷中包含用户的角色信息。例如,在用户认证成功生成 JWT 时,将用户角色添加到载荷中:
const payload = {
    username: 'validUser',
    role: 'admin'
};
const token = jwt.sign(payload, secret, { expiresIn: '1h' });

在 Serverless 函数中验证 JWT 后,根据载荷中的角色信息进行授权决策。例如,只有 admin 角色的用户才能访问特定的管理功能的函数:

const decoded = jwt.verify(token, secret);
if (decoded.role === 'admin') {
    // 允许访问管理功能
} else {
    return {
        statusCode: 403,
        body: JSON.stringify({ message: 'Forbidden' })
    };
}

输入验证与数据过滤

  1. 输入验证的重要性:即使通过 JWT 进行了身份认证和授权,恶意用户仍可能尝试通过发送恶意输入来攻击 Serverless 函数,如 SQL 注入、代码注入等。因此,输入验证和数据过滤是保证系统安全的重要环节。
  2. 实现方法
    • 使用参数校验库:在不同的编程语言中,有许多参数校验库可以使用。例如,在 Node.js 中可以使用 joi 库来验证请求参数。假设我们有一个接收用户 ID 的函数,使用 joi 进行验证:
const Joi = require('joi');
const schema = Joi.object({
    userId: Joi.string().pattern(/^[0 - 9a - fA - F]{24}$/).required()
});
exports.handler = async (event, context) => {
    const { error } = schema.validate(event.queryStringParameters);
    if (error) {
        return {
            statusCode: 400,
            body: JSON.stringify({ message: 'Invalid input' })
        };
    }
    // 继续处理业务逻辑
};
  • 数据过滤:对于可能包含危险字符的输入,进行数据过滤。例如,在处理用户输入的文本时,过滤掉 <script> 等可能用于 XSS 攻击的标签。在 Python 中,可以使用 bleach 库进行 HTML 数据过滤:
import bleach

user_input = '<script>alert("XSS")</script>'
cleaned_input = bleach.clean(user_input)

通过上述对 JWT 在 Serverless 架构中的全面介绍,包括原理、应用优势、实现方式、安全注意事项以及与其他安全机制的结合,希望能帮助开发者在 Serverless 架构下构建更加安全可靠的后端应用。