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

JWT在移动应用中的最佳实践

2022-12-242.4k 阅读

JWT 基础概念

什么是 JWT

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

JWT 的结构

  1. 头部(Header):通常由两部分组成,令牌的类型(即 JWT)和所使用的签名算法,如 HMAC SHA256 或 RSA。

    {
        "alg": "HS256",
        "typ": "JWT"
    }
    

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

  2. 载荷(Payload):是 JWT 的第二部分,其中包含声明(claims)。声明是关于实体(通常是用户)和其他数据的陈述。有三种类型的声明:注册声明(如 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. 无状态:服务器不需要在内存中存储关于用户会话的信息。每个请求都包含了足够的信息来验证用户身份,这使得应用更容易扩展,因为服务器可以独立处理每个请求,无需共享会话状态。
  2. 紧凑:由于 JWT 是紧凑的,非常适合在 HTTP 头部或 URL 参数中传输,在移动应用中,这种紧凑性尤为重要,因为移动设备的网络带宽和存储资源相对有限。
  3. 自包含:JWT 的载荷包含了所有相关的用户信息,这意味着服务器在验证 JWT 后,无需再查询数据库来获取用户的额外信息,减少了数据库的查询次数,提高了响应速度。

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

用户身份验证

在移动应用中,用户登录后,后端服务器生成一个 JWT 并返回给移动客户端。客户端在后续的请求中将 JWT 包含在 HTTP 头部中,后端服务器验证 JWT 的签名和有效性,从而确定用户的身份。例如,一个社交移动应用,用户登录成功后,服务器生成 JWT,客户端在发送发布动态、获取好友列表等请求时,将 JWT 放在 Authorization 头部:

Authorization: Bearer <JWT>

服务器通过验证 JWT 来确保请求来自已登录的合法用户。

授权

除了身份验证,JWT 还可以用于授权。通过在 JWT 的载荷中包含用户的权限信息,服务器可以在接收到请求时,根据 JWT 中的权限声明来判断用户是否有权限执行特定的操作。例如,在一个企业移动办公应用中,某些用户可能具有创建文档的权限,而其他用户只有查看权限。在 JWT 载荷中可以这样表示:

{
    "sub": "12345",
    "name": "Alice",
    "permissions": ["create_document", "view_document"]
}

当 Alice 尝试创建文档时,服务器验证 JWT 中的 permissions 字段,确认她有 create_document 权限,才允许操作执行。

跨服务通信

在一些复杂的移动应用架构中,可能涉及多个后端服务之间的通信。JWT 可以作为一种安全的方式在这些服务之间传递用户身份和相关信息。例如,一个移动电商应用,后端有订单服务、支付服务和库存服务。当用户下单时,订单服务生成一个包含用户信息和订单相关信息的 JWT,并将其传递给支付服务和库存服务。这些服务可以通过验证 JWT 来确保请求的合法性和数据的完整性。

JWT 在移动应用中的最佳实践

密钥管理

  1. 选择强密钥:用于签名 JWT 的密钥必须足够强大,以防止暴力破解。对于 HMAC 算法,密钥长度应至少为 256 位。避免使用简单的字符串作为密钥,例如不要使用像 “mysecret” 这样容易被猜到的密钥。
  2. 安全存储密钥:密钥应该安全地存储在服务器端,避免将密钥硬编码在代码中。一种常见的做法是将密钥存储在环境变量中,在服务器启动时加载。例如,在 Node.js 应用中,可以这样获取密钥:
const jwt = require('jsonwebtoken');
const secret = process.env.JWT_SECRET;

在服务器的部署环境中,通过设置环境变量 JWT_SECRET 来提供实际的密钥值。 3. 定期更换密钥:为了增加安全性,应定期更换 JWT 的签名密钥。当更换密钥时,需要确保在一定的过渡时间内,服务器能够同时验证使用新旧密钥签名的 JWT,直到所有旧的 JWT 过期。

JWT 有效期设置

  1. 合理设置过期时间:JWT 的过期时间(exp 声明)应该根据应用的实际需求合理设置。对于移动应用,一般来说,较短的过期时间可以提高安全性,但可能会给用户带来频繁登录的不便。例如,对于一些对安全性要求较高的金融移动应用,可以设置 JWT 的有效期为 15 分钟到 1 小时。而对于一些普通的社交移动应用,可以设置为 1 天到 7 天。
  2. 使用刷新令牌:为了避免用户频繁登录,可以引入刷新令牌机制。当 JWT 过期时,移动客户端可以使用刷新令牌向服务器请求一个新的 JWT。刷新令牌应该具有较长的有效期,并且存储在安全的地方,如移动设备的本地存储或钥匙串(iOS)、Keystore(Android)中。以下是一个简单的 Node.js 示例,展示如何实现刷新令牌:
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();
const refreshTokens = [];

// 生成 JWT
function generateAccessToken(user) {
    return jwt.sign({ username: user.username }, process.env.JWT_SECRET, { expiresIn: '15m' });
}

// 生成刷新令牌
function generateRefreshToken(user) {
    const token = jwt.sign({ username: user.username }, process.env.REFRESH_TOKEN_SECRET);
    refreshTokens.push(token);
    return token;
}

// 刷新 JWT
app.post('/refresh', (req, res) => {
    const refreshToken = req.body.token;
    if (!refreshToken) return res.status(401).send('Token is required');
    if (!refreshTokens.includes(refreshToken)) return res.status(403).send('Invalid token');

    try {
        const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
        const accessToken = generateAccessToken(decoded);
        res.json({ accessToken });
    } catch (err) {
        res.status(400).send('Invalid token');
    }
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

安全传输

  1. 使用 HTTPS:在移动应用与后端服务器之间传输 JWT 时,必须使用 HTTPS 协议。HTTPS 可以加密通信内容,防止中间人攻击,避免 JWT 在传输过程中被窃取或篡改。
  2. 避免在 URL 中传递 JWT:虽然 JWT 可以放在 URL 参数中传输,但这是不安全的做法,因为 URL 可能会被记录在服务器日志、浏览器历史记录等地方,增加了 JWT 泄露的风险。应始终将 JWT 放在 HTTP 头部中传输,例如 Authorization 头部。

验证和解析

  1. 严格验证:后端服务器在接收到包含 JWT 的请求时,必须严格验证 JWT 的签名、过期时间、发行人等信息。不同的编程语言都有相应的 JWT 库来帮助进行验证。例如,在 Python 中使用 PyJWT 库:
import jwt
from flask import Flask, request, jsonify

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key'

@app.route('/protected', methods=['GET'])
def protected():
    token = None
    if 'Authorization' in request.headers:
        token = request.headers['Authorization'].split(' ')[1]

    if not token:
        return jsonify({'message': 'Token is missing!'}), 401

    try:
        data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
        return jsonify({'message': 'This is a protected route', 'data': data}), 200
    except jwt.ExpiredSignatureError:
        return jsonify({'message': 'Token has expired!'}), 401
    except jwt.InvalidTokenError:
        return jsonify({'message': 'Invalid token!'}), 401

if __name__ == '__main__':
    app.run(debug=True)
  1. 防止重放攻击:重放攻击是指攻击者截获并重新发送合法的 JWT 来获取未授权的访问。可以通过在 JWT 中添加唯一标识符(如 jti 声明),并在服务器端维护一个已使用的 JWT 列表,每次验证 JWT 时检查其 jti 是否已在列表中。如果已存在,则拒绝该请求。

移动客户端存储

  1. 选择安全的存储方式:在移动客户端,JWT 应该存储在安全的地方。在 iOS 中,可以使用钥匙串(Keychain)来存储 JWT,钥匙串提供了加密的存储空间,只有授权的应用可以访问。在 Android 中,可以使用 Keystore 来安全地存储敏感信息,包括 JWT。
  2. 限制存储时间:即使使用了安全的存储方式,也应该限制 JWT 在移动客户端的存储时间。当 JWT 过期或用户注销时,应及时从客户端存储中删除 JWT,以防止潜在的安全风险。

JWT 与移动应用架构结合

微服务架构

在微服务架构的移动应用后端中,JWT 可以作为不同微服务之间传递用户身份和授权信息的通用方式。每个微服务都可以独立验证 JWT 的有效性,而不需要依赖共享的会话存储。例如,一个包含用户服务、订单服务和支付服务的移动电商微服务架构:

  1. 用户服务负责用户登录,并生成 JWT 返回给移动客户端。
  2. 当客户端发起订单创建请求时,订单服务接收到包含 JWT 的请求,验证 JWT 后处理订单创建逻辑。
  3. 如果订单创建成功,订单服务将 JWT 传递给支付服务,支付服务同样验证 JWT 后进行支付处理。

前后端分离架构

在前后端分离的移动应用开发模式中,JWT 是实现后端 API 安全认证的理想选择。前端移动应用通过调用后端 API 来获取数据和执行操作。后端 API 使用 JWT 来验证请求的合法性。例如,一个使用 React Native 作为前端框架,Node.js + Express 作为后端框架的移动应用:

  1. 前端 React Native 应用在用户登录成功后,将用户名和密码发送到后端 Express 服务器。
  2. Express 服务器验证用户凭证后,生成 JWT 并返回给前端。
  3. 前端在后续的 API 请求中,将 JWT 放在 Authorization 头部,后端 Express 服务器使用 express - jwt 中间件来验证 JWT,确保请求合法。

常见问题及解决方案

JWT 大小限制

  1. 问题:JWT 的大小是有限制的,特别是在移动应用中,网络带宽有限,过大的 JWT 可能导致传输问题。如果 JWT 的载荷中包含过多的数据,可能会超出限制。
  2. 解决方案:尽量减少 JWT 载荷中的数据,只包含必要的信息,如用户标识、权限等。对于一些不常用或较大的数据,可以在需要时从数据库中查询,而不是放在 JWT 中。另外,如果确实需要传输较大的数据,可以考虑将数据存储在服务器端,通过 JWT 中的标识符来引用这些数据。

兼容性问题

  1. 问题:不同的移动平台和后端技术栈对 JWT 的支持可能存在差异,例如某些较旧的移动设备上的应用可能不支持最新的 JWT 规范或特定的签名算法。
  2. 解决方案:在选择 JWT 的签名算法和规范版本时,要考虑移动应用的目标平台和后端技术的兼容性。尽量选择广泛支持的算法,如 HMAC SHA256。同时,在开发过程中进行充分的测试,确保在不同的设备和平台上都能正确地生成、验证和解析 JWT。

性能问题

  1. 问题:在高并发场景下,频繁地验证 JWT 可能会对服务器性能产生影响,特别是当使用复杂的签名算法或需要查询数据库来验证 JWT 中的某些信息时。
  2. 解决方案:可以通过缓存来提高性能,例如将已验证的 JWT 及其相关信息缓存起来,对于相同的 JWT,在缓存有效期内无需再次验证。另外,选择性能较高的签名算法,避免不必要的数据库查询,也可以优化服务器性能。例如,在 Node.js 中可以使用 node - cache 库来实现简单的缓存功能:
const NodeCache = require('node - cache');
const jwt = require('jsonwebtoken');
const cache = new NodeCache();

function verifyJwt(token) {
    const cached = cache.get(token);
    if (cached) {
        return cached;
    }

    try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        cache.set(token, decoded, 300); // 缓存 5 分钟
        return decoded;
    } catch (err) {
        return null;
    }
}

安全漏洞防范

  1. 问题:JWT 可能面临一些安全漏洞,如密钥泄露、签名算法被破解、JWT 劫持等。
  2. 解决方案:加强密钥管理,定期更换密钥,对密钥进行加密存储。使用安全的签名算法,并关注算法的安全性更新。在移动客户端和服务器端采取措施防止 JWT 劫持,如使用 HTTPS 传输、设置合理的 HttpOnlySecure 标志(如果适用)。同时,定期进行安全审计和漏洞扫描,及时发现和修复潜在的安全问题。

通过遵循以上最佳实践,可以在移动应用开发中安全、有效地使用 JWT,提高应用的安全性和用户体验。无论是小型的移动应用还是大型的企业级移动应用,JWT 都为后端开发提供了一种可靠的安全认证机制。在实际开发过程中,要根据应用的具体需求和特点,灵活运用这些实践方法,并不断关注 JWT 技术的发展和安全动态,以确保应用的安全性和稳定性。在与移动应用架构结合时,要充分考虑不同架构模式的特点,使 JWT 与架构完美融合,发挥其最大的优势。对于常见问题,要提前做好防范措施,通过优化和调整来避免性能瓶颈和安全风险。总之,JWT 在移动应用中的应用需要综合考虑多方面因素,以实现最佳的效果。