Сообщение об ошибке
JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
Также выводится как
- io.jsonwebtoken.security.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
JJWT (Java) — io.jsonwebtoken.security.SignatureException from JwtParser.parseSignedClaims
Что это означает
JJWT пересчитал (HMAC) или проверил (RSA, ECDSA, EdDSA) подпись заголовка и полезной нагрузки токена заданным вами ключом, и результат не совпал с сегментом подписи в токене. Токен разобран без ошибок, и алгоритм соответствует ключу; просто ключ и токен не подходят друг к другу, или токен изменили после подписания.
Почему это происходит
Проверяющая сторона использует другой ключ
частоКлюч из другого окружения, пара ключей, которая заново генерируется при запуске (частая ошибка в демонстрационном коде, попавшем в продакшен), или один экземпляр сервиса с устаревшим секретом.
Секрет HMAC тот же, байты разные
частоОдна сторона использует байты строки в UTF-8, другая сначала декодирует её из base64 (в примерах Spring секреты часто хранятся в base64). В конфигурации секреты выглядят одинаково, а MAC получаются разные.
Ротация ключей без обработки kid
иногдаИздатель подписывает токены новым ключом, а у проверяющей стороны есть только старый. Без локатора ключей, который выбирает ключ по `kid`, проверяющая сторона не может даже определить, какой ключ имелся в виду.
Токен изменён
иногдаЛюбое изменение сегмента заголовка или полезной нагрузки, включая пробельные символы или повторное кодирование, делает подпись недействительной.
Как исправить
- 1.
Выбирайте ключи по kid через локатор ключей
JJWT 0.12+ поддерживает `keyLocator`: он получает заголовок и возвращает нужный ключ. Используйте его вместе с JWKS, который вы обновляете при ротации ключей.
java import io.jsonwebtoken.*; import java.security.Key; // Pick the verification key by the token's kid instead of a single static key. Locator<Key> keys = new LocatorAdapter<>() { @Override protected Key locate(JwsHeader header) { Key key = publicKeysByKid.get(header.getKeyId()); // refreshed from the issuer's JWKS if (key == null) throw new UnsupportedJwtException("unknown kid " + header.getKeyId()); return key; } }; Jws<Claims> jws = Jwts.parser().keyLocator(keys).requireIssuer("https://issuer.example.com/").build().parseSignedClaims(token); - 2.
Согласуйте байты ключа HMAC
Решите, считать ли заданный секрет текстом или строкой base64, и получайте ключ одинаково на всех сторонах. Калькулятор HMAC показывает MAC для обоих вариантов.
java // Both sides must derive the same key bytes. If the secret is stored base64-encoded, decode it. javax.crypto.SecretKey key = io.jsonwebtoken.security.Keys.hmacShaKeyFor( java.util.Base64.getDecoder().decode(System.getenv("JWT_SECRET_B64")));