All in Token架构实战:如何解决微服务认证的横向扩展难题

1次阅读
没有评论

共计 3294 个字符,预计需要花费 9 分钟才能阅读完成。

image.webp

痛点分析

在传统的微服务架构中,Session 认证机制逐渐暴露出诸多问题。这些问题在系统规模扩大后尤为明显,主要体现在以下几个方面:

All in Token 架构实战:如何解决微服务认证的横向扩展难题

  • 内存消耗问题 :每个用户的 Session 信息都需要保存在服务端内存中,当用户量达到百万级别时,对内存的消耗非常可观。我曾经在一个项目中遇到过,仅 Session 存储就占用了近 30% 的服务器内存资源。

  • 跨服务认证复杂 :当请求需要经过多个微服务时,每个服务都需要验证 Session 的有效性。这要么需要共享 Session 存储,增加了系统复杂度;要么需要重复验证,降低了系统性能。

  • 横向扩展困难 :当需要增加服务实例时,Session 的同步和共享成为瓶颈。虽然可以使用 Redis 等分布式缓存来解决,但这又引入了新的依赖和潜在的单点故障。

方案对比

Token 认证方案主要有 JWT 和 Opaque Token 两种,它们各有优缺点:

JWT(JSON Web Token)

  • 优点
  • 自包含:所有必要信息都在 Token 中,减少服务端查询
  • 无状态:服务端不需要存储会话信息
  • 跨语言:基于标准,各语言都有成熟实现

  • 缺点

  • Token 无法主动失效
  • Payload 内容可能被解码(虽然不能篡改)
  • 体积较大

Opaque Token

  • 优点
  • 体积小
  • 服务端完全控制
  • 可以立即失效

  • 缺点

  • 每次验证都需要服务端查询
  • 增加了认证服务的压力

选型决策树
1. 是否需要立即失效能力?是→Opaque;否→下一步
2. Token 中是否需要携带业务信息?是→JWT;否→Opaque
3. 对网络传输效率敏感?是→Opaque;否→JWT

核心实现

JWT 生成示例

public class JwtGenerator {
    // 生成 RSA 密钥对
    public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        return keyPairGenerator.generateKeyPair();}

    // 生成 JWT Token
    public String generateToken(User user, KeyPair keyPair) {Instant now = Instant.now();
        return Jwts.builder()
            .setSubject(user.getId())
            .claim("roles", user.getRoles())
            .setIssuedAt(Date.from(now))
            .setExpiration(Date.from(now.plus(1, ChronoUnit.HOURS)))
            .signWith(keyPair.getPrivate(), SignatureAlgorithm.RS256)
            .compact();}
}

双 Token 刷新机制

public class TokenService {
    // 生成访问 Token 和刷新 Token
    public TokenPair generateTokenPair(User user) {String accessToken = generateToken(user, 1, TimeUnit.HOURS);
        String refreshToken = generateToken(user, 7, TimeUnit.DAYS);
        return new TokenPair(accessToken, refreshToken);
    }

    // 使用刷新 Token 获取新访问 Token
    public String refreshAccessToken(String refreshToken) {if (!validateToken(refreshToken)) {throw new InvalidTokenException("Invalid refresh token");
        }
        Claims claims = parseToken(refreshToken);
        return generateToken(claims.getSubject(), 1, TimeUnit.HOURS);
    }
}

性能优化

公钥本地缓存

每次验证 JWT 时,最耗时的操作是获取公钥。可以将公钥缓存在本地:

public class JwtValidator {private final Map<String, PublicKey> publicKeyCache = new ConcurrentHashMap<>();

    public boolean validateToken(String token, String keyId) {
        PublicKey publicKey = publicKeyCache.computeIfAbsent(keyId, 
            k -> fetchPublicKeyFromAuthServer(keyId));
        // 使用公钥验证 Token...
    }
}

Redis 黑名单

对于需要立即失效的 Token,可以使用 Redis 存储黑名单:

public class TokenBlacklist {
    private final RedisTemplate<String, String> redisTemplate;

    public void addToBlacklist(String token, long expireInSeconds) {redisTemplate.opsForValue().set("blacklist:"+token, "1", expireInSeconds, TimeUnit.SECONDS);
    }

    public boolean isBlacklisted(String token) {return redisTemplate.hasKey("blacklist:"+token);
    }
}

安全防护

防范重放攻击

在 Token 中加入时间戳和随机数:

public String generateToken(User user) {return Jwts.builder()
        .setSubject(user.getId())
        .claim("nonce", UUID.randomUUID().toString()) // 随机数
        .claim("timestamp", System.currentTimeMillis()) // 时间戳
        // 其他 claims...
        .signWith(privateKey)
        .compact();}

敏感操作二次认证

对于敏感操作(如修改密码、支付等),强制要求二次认证:

@PostMapping("/sensitive-operation")
public ResponseEntity<?> sensitiveOperation(@RequestHeader("Authorization") String token,
    @RequestBody SensitiveRequest request) {

    // 验证主 Token
    Claims claims = jwtValidator.validateToken(token);

    // 检查二次认证 Token
    if (!request.hasValidSecondFactorToken()) {return ResponseEntity.status(HttpStatus.FORBIDDEN)
            .body(new ErrorResponse("Second factor authentication required"));
    }

    // 执行业务逻辑...
}

避坑指南

  • 避免存储敏感数据 :Token 可能被解码,不要在 Payload 中存储密码、密钥等敏感信息
  • 控制 Payload 大小 :HTTP Header 通常有大小限制(如 8KB),过大的 Token 可能导致问题
  • 合理设置有效期 :访问 Token 建议 1 - 2 小时,刷新 Token 建议 7 天
  • 使用 HTTPS:防止 Token 在传输过程中被截获

开放问题

Token 的有效期设置是个需要权衡的问题:
– 过短:用户体验差,需要频繁重新登录
– 过长:安全风险增加

如何平衡安全性和用户体验?欢迎在评论区分享你的经验和见解。

正文完
 0
评论(没有评论)