помещение
Это пятая статья из серии "Холодный рис по-новому обжаривается".
В этой статье будет рассмотрен стандарт с открытым исходным кодом для создания токенов доступа.JWT,вводитьJWTспецификации, лежащие в основе принципы реализации, основные сценарии использования и применения.
Спецификация JWT
Жаль, что его нельзя найти в Википедии.JWTвход, но отjwt.io, вы можете увидеть описание:
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties
Из этого текста можно извлечь, чтоJWTдокумент спецификацииRFC 7519, в котором подробно описаноJWTосновная концепцияClaimsЗначение, макет и реализация алгоритма и т. д. будут подробно описаны ниже.
Основные понятия JWT
JWTполное имяJSON Web Token, если понимать буквально, чувство основано наJSONФормат токена, используемого для передачи по сети. По факту,JWTявляется компактнымClaimsДекларативный формат, предназначенный для передачи в средах с ограниченным пространством, общих сценариях, таких какHTTPПараметры заголовка запроса на авторизацию иURIпараметры запроса.JWTположитClaimsпреобразовать вJSONформат и этоJSONКонтент будет применяться какJWSПолезная нагрузка структуры или приложенияJWEСтруктурированная (зашифрованная) необработанная строка с помощью кода аутентификации сообщения (Message Authentication Codeили для краткостиMAC) и/или пара криптографических операцийClaimsЦифровая подпись или защита целостности.
Вот кратко упомянутые три концепции в других документах спецификации:
-
JWE(Технический документRFC 7516):JSON Web Encryption, а значит на основеJSONЗашифрованное содержимое структуры данных, механизм шифрования шифрует любую последовательность октетов, обеспечивает защиту целостности и повышает сложность взлома,JWEКомпактная схема сериализации выглядит следующим образом
BASE64URL(UTF8(JWE Protected Header)) || '.' ||
BASE64URL(JWE Encrypted Key) || '.' ||
BASE64URL(JWE Initialization Vector) || '.' ||
BASE64URL(JWE Ciphertext) || '.' ||
BASE64URL(JWE Authentication Tag)
-
JWS(Технический документRFC 7515):JSON Web Signature, что означает использоватьJSONструктуры данных иBASE64URLЗакодированное представление имеет цифровую подпись или код аутентификации сообщения (MAC) сертифицированный контент, цифровая подпись илиMACспособный обеспечить защиту целостности,JWSМакет компактной сериализации выглядит следующим образом:
ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' ||
BASE64URL(JWS Payload)) || '.' ||
BASE64URL(JWS Signature)
-
JWA(Технический документRFC 7518):JSON Web Algorithm,JSON Webалгоритм, цифровая подпись илиMACалгоритм, применяемый кJWSСписок доступных алгоритмов выглядит следующим образом:
В целом,JWTНа самом деле существует две реализации, основанные наJWEРеализация зависит от алгоритма шифрования и дешифрования,BASE64URLКодирование и аутентификация личности для улучшения передачиClaimsсложность взлома, и на основеJWSВ реализации используетсяBASE64URLСпособ кодирования и цифровая подпись на передачеClaimsОбеспечивает защиту целостности, то есть только гарантированную передачуClaimsСодержимое не подделывается, но открывается открытый текст.текущий мейнстримJWTБольшая часть фреймворка не реализованаJWE, поэтому следующие в основном черезJWSПодробное обсуждение реализации.
Претензии в JWT
ClaimЕсть претензии, претензии, требования или претензии, но автор считает, что любой перевод не очень смысловой, что здесь оговореноClaimКлючевые слова напрямую служат именами.JWTОсновная роль заключается в защитеClaimsцелостность (или шифрование данных), гарантированнаяJWTво время передачиClaimsНе подделан (или не взломан).ClaimsсуществуетJWTв исходном содержании естьJSONстрока формата, где одинClaimдаK-Vструктура, какJsonNodeодин изfield-value, вот предопределенные в часто используемых спецификацияхClaim:
| короткое имя | полное имя | значение |
|---|---|---|
| iss | Issuer | Эмитент |
| sub | Subject | основной корпус |
| aud | Audience | (приемная) цель |
| exp | Expiration Time | Дата истечения срока годности |
| nbf | Not Before | раньше установленного времениJWTне могут быть приняты к обработке |
| iat | Issued At |
JWTВременная метка выпуска |
| jti | JWT ID |
JWTуникальный идентификатор |
эти предопределенныеClaimНе требует обязательного использования, когда выбирать какойClaimполностью зависит от пользователя, и для того, чтобы сделатьJWTболее компактные, этиClaimВсе они определяются с использованием коротких имен. построенный в разладеClaimВ условиях конфликта пользователи могут настраивать новые общедоступныеClaim,как:
| короткое имя | полное имя | значение |
|---|---|---|
| cid | Customer ID | Пользовательский ИД |
| rid | Role ID | идентификатор роли |
Необходимо отметить, что вJWSреализуется,Claimsбудет действовать какpayloadЧастичноBASE64Кодирование, открытый текст будет открыт напрямую, а конфиденциальная информация, как правило, не должна разрабатываться как пользовательскаяClaim.
Заголовок в JWT
существуетJWTДокумент спецификации называет ихHeaderзаJOSE Header,JOSEполное имяJavascript Object Signature Encryption, это,JavascriptПлатформа подписи и шифрования объектов,JOSE HeaderНа самом деле этоJavascriptПараметры подписи объекта и шифрования заголовка.Перечислено нижеJWSобычно используется вHeader:
| короткое имя | полное имя | значение |
|---|---|---|
| alg | Algorithm | для защитыJWSалгоритм шифрования и дешифрования |
| jku | JWK Set URL | ГруппаJSONЗашифрованный открытый ключURL, один из которых дляJWSключ цифровой подписи |
| jwk | JSON Web Key | для сопряженияJWSОткрытый ключ, соответствующий ключу цифровой подписи |
| kid | Key ID | для защитыJWSвходящий ключ |
| x5u | X.509 URL |
X.509Связанный |
| x5c | X.509 Certificate Chain |
X.509Связанный |
| x5t | X.509 Certificate SHA-1 Thumbprin |
X.509Связанный |
| x5t#S256 | X.509 Certificate SHA-256 Thumbprint |
X.509Связанный |
| typ | Type | тип, напр.JWT,JWSилиJWEи т.д |
| cty | Content Type | Тип контента, определитесьpayloadчастьMediaType
|
Два самых распространенныхHeaderэтоalgиtyp,Например:
{
"alg": "HS256",
"typ": "JWT"
}
Схема JWT
В основном введениеJWSкомпоновка, как упоминалось ранее,JWSизкомпактная планировкаследующее:
ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' ||
BASE64URL(JWS Payload)) || '.' ||
BASE64URL(JWS Signature)
На самом деле естьнекомпактная компоновка, пройдетJSONПолностью отображаетсяHeaderпараметр,Claimsи подпись группы:
{
"payload":"<payload contents>",
"signatures":[
{"protected":"<integrity-protected header 1 contents>",
"header":<non-integrity-protected header 1 contents>,
"signature":"<signature 1 contents>"},
...
{"protected":"<integrity-protected header N contents>",
"header":<non-integrity-protected header N contents>,
"signature":"<signature N contents>"}]
}
Существует также плоское представление некомпактного макета:
{
"payload":"<payload contents>",
"protected":"<integrity-protected header contents>",
"header":<non-integrity-protected header contents>,
"signature":"<signature contents>"
}
вHeaderЧасть параметра может относиться к предыдущему разделу, а часть подписи может относиться к следующему подразделу, а остальное кратко упоминается.payloadчасть,payload(полезная нагрузка) фактически завершенаClaims, предполагаяClaimsизJSONФорма:
{
"iss": "throwx",
"jid": 1
}
Затем сплющенный некомпактный форматpayloadУзлы:
{
......
"payload": {
"iss": "throwx",
"jid": 1
}
......
}
Алгоритм подписи JWS
JWSГенерация подписи зависит от алгоритмов хеширования или шифрования и дешифрования. Алгоритмы, которые можно использовать, показаны на рисунке, опубликованном ранее, напримерHS256, в частностиHMAC SHA-256, то есть путем хешированияSHA-256для закодированногоHeaderиClaimsСтрока хэшируется один раз, и псевдокод, сгенерированный подписью, выглядит следующим образом:
## 不进行编码
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
256 bit secret key
)
## 进行编码
base64UrlEncode(
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload)
[256 bit secret key])
)
Операции других алгоритмов в основном аналогичны, и сгенерированная подпись напрямую добавляется с префиксом.сращенный вbase64UrlEncode(header).base64UrlEncode(payload)Затем сгенерируйте полныйJWS.
Генерация, разбор и проверка JWT
был проанализирован доJWTНекоторые основные понятия, макеты и алгоритмы подписи здесь основаны на предыдущей теории.JWTоперации генерации, синтаксического анализа и проверки. сначала импортироватьcommon-codecБиблиотека упрощает некоторые операции кодирования, шифрования и дешифрования и представляет основнойJSONФреймворк выполняет сериализацию и десериализацию:
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.0</version>
</dependency>
Для простоты,HeaderПараметры жестко запрограммированы как:
{
"alg": "HS256",
"typ": "JWT"
}
Используемый алгоритм подписиHMAC SHA-256, длина введенного ключа шифрования должна быть256 bit(Если вы используете только символы, состоящие из английского языка и цифр, вы должны32символов), здесь для простоты используйте00000000111111112222222233333333в видеKEY. определениеClaimsЧасти следующие:
{
"iss": "throwx",
"jid": 10087, # <---- 这里有个笔误,本来打算写成jti,后来发现写错了,不打算改
"exp": 1613227468168 # 20210213
}
генерироватьJWTКод выглядит следующим образом:
@Slf4j
public class JsonWebToken {
private static final String KEY = "00000000111111112222222233333333";
private static final String DOT = ".";
private static final Map<String, String> HEADERS = new HashMap<>(8);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static {
HEADERS.put("alg", "HS256");
HEADERS.put("typ", "JWT");
}
String generateHeaderPart() throws JsonProcessingException {
byte[] headerBytes = OBJECT_MAPPER.writeValueAsBytes(HEADERS);
String headerPart = new String(Base64.encodeBase64(headerBytes,false ,true), StandardCharsets.US_ASCII);
log.info("生成的Header部分为:{}", headerPart);
return headerPart;
}
String generatePayloadPart(Map<String, Object> claims) throws JsonProcessingException {
byte[] payloadBytes = OBJECT_MAPPER.writeValueAsBytes(claims);
String payloadPart = new String(Base64.encodeBase64(payloadBytes,false ,true), StandardCharsets.UTF_8);
log.info("生成的Payload部分为:{}", payloadPart);
return payloadPart;
}
String generateSignaturePart(String headerPart, String payloadPart) {
String content = headerPart + DOT + payloadPart;
Mac mac = HmacUtils.getInitializedMac(HmacAlgorithms.HMAC_SHA_256, KEY.getBytes(StandardCharsets.UTF_8));
byte[] output = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
String signaturePart = new String(Base64.encodeBase64(output, false ,true), StandardCharsets.UTF_8);
log.info("生成的Signature部分为:{}", signaturePart);
return signaturePart;
}
public String generate(Map<String, Object> claims) throws Exception {
String headerPart = generateHeaderPart();
String payloadPart = generatePayloadPart(claims);
String signaturePart = generateSignaturePart(headerPart, payloadPart);
String jws = headerPart + DOT + payloadPart + DOT + signaturePart;
log.info("生成的JWT为:{}", jws);
return jws;
}
public static void main(String[] args) throws Exception {
Map<String, Object> claims = new HashMap<>(8);
claims.put("iss", "throwx");
claims.put("jid", 10087L);
claims.put("exp", 1613227468168L);
JsonWebToken jsonWebToken = new JsonWebToken();
System.out.println("自行生成的JWT:" + jsonWebToken.generate(claims));
}
}
Журнал вывода выполнения выглядит следующим образом:
23:37:48.743 [main] INFO club.throwable.jwt.JsonWebToken - 生成的Header部分为:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
23:37:48.747 [main] INFO club.throwable.jwt.JsonWebToken - 生成的Payload部分为:eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9
23:37:48.748 [main] INFO club.throwable.jwt.JsonWebToken - 生成的Signature部分为:7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
23:37:48.749 [main] INFO club.throwable.jwt.JsonWebToken - 生成的JWT为:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
自行生成的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
допустимыйjwt.ioПодтвердите это на:
РазобратьJWTпроцесс построенияJWTОбратный процесс , сначала основанный на записи через точку.разделить на три части, а затемBASE64Декодирование, а затем получение открытого текста, состоящего из трех частей, параметров заголовка и полезной нагрузки необходимо выполнить один раз.JSONДесериализация восстанавливаетJSONструктура:
public Map<Part, PartContent> parse(String jwt) throws Exception {
System.out.println("当前解析的JWT:" + jwt);
Map<Part, PartContent> result = new HashMap<>(8);
// 这里暂且认为所有的输入JWT的格式都是合法的
StringTokenizer tokenizer = new StringTokenizer(jwt, DOT);
String[] jwtParts = new String[3];
int idx = 0;
while (tokenizer.hasMoreElements()) {
jwtParts[idx] = tokenizer.nextToken();
idx++;
}
String headerPart = jwtParts[0];
PartContent headerContent = new PartContent();
headerContent.setRawContent(headerPart);
headerContent.setPart(Part.HEADER);
headerPart = new String(Base64.decodeBase64(headerPart), StandardCharsets.UTF_8);
headerContent.setPairs(OBJECT_MAPPER.readValue(headerPart, new TypeReference<Map<String, Object>>() {
}));
result.put(Part.HEADER, headerContent);
String payloadPart = jwtParts[1];
PartContent payloadContent = new PartContent();
payloadContent.setRawContent(payloadPart);
payloadContent.setPart(Part.PAYLOAD);
payloadPart = new String(Base64.decodeBase64(payloadPart), StandardCharsets.UTF_8);
payloadContent.setPairs(OBJECT_MAPPER.readValue(payloadPart, new TypeReference<Map<String, Object>>() {
}));
result.put(Part.PAYLOAD, payloadContent);
String signaturePart = jwtParts[2];
PartContent signatureContent = new PartContent();
signatureContent.setRawContent(signaturePart);
signatureContent.setPart(Part.SIGNATURE);
result.put(Part.SIGNATURE, signatureContent);
return result;
}
enum Part {
HEADER,
PAYLOAD,
SIGNATURE
}
@Data
public static class PartContent {
private Part part;
private String rawContent;
private Map<String, Object> pairs;
}
Здесь попробуйте использовать ранее произведенныйJWTДля разбора:
public static void main(String[] args) throws Exception {
JsonWebToken jsonWebToken = new JsonWebToken();
String jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs";
Map<Part, PartContent> parseResult = jsonWebToken.parse(jwt);
System.out.printf("解析结果如下:\nHEADER:%s\nPAYLOAD:%s\nSIGNATURE:%s%n",
parseResult.get(Part.HEADER),
parseResult.get(Part.PAYLOAD),
parseResult.get(Part.SIGNATURE)
);
}
Результаты анализа следующие:
当前解析的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
解析结果如下:
HEADER:PartContent(part=HEADER, rawContent=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9, pairs={typ=JWT, alg=HS256})
PAYLOAD:PartContent(part=PAYLOAD, rawContent=eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9, pairs={iss=throwx, jid=10087, exp=1613227468168})
SIGNATURE:PartContent(part=SIGNATURE, rawContent=7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs, pairs=null)
проверятьJWTпостроен на разбореJWTНа основе завершения необходимо сделать один раз на проанализированных параметрах заголовка и полезной нагрузке.MACПодпись сверяется с проанализированной подписью. Кроме того, вы можете настроить конкретные проверкиClaimтакие элементы, как время истечения срока действия и эмитент. Как правило, если проверка не пройдена, разные исключения времени выполнения будут настроены для разных ситуаций, чтобы упростить дифференциацию сценариев.Здесь они объединены для удобства.IllegalStateException:
public void verify(String jwt) throws Exception {
System.out.println("当前校验的JWT:" + jwt);
Map<Part, PartContent> parseResult = parse(jwt);
PartContent headerContent = parseResult.get(Part.HEADER);
PartContent payloadContent = parseResult.get(Part.PAYLOAD);
PartContent signatureContent = parseResult.get(Part.SIGNATURE);
String signature = generateSignaturePart(headerContent.getRawContent(), payloadContent.getRawContent());
if (!Objects.equals(signature, signatureContent.getRawContent())) {
throw new IllegalStateException("签名校验异常");
}
String iss = payloadContent.getPairs().get("iss").toString();
// iss校验
if (!Objects.equals(iss, "throwx")) {
throw new IllegalStateException("ISS校验异常");
}
long exp = Long.parseLong(payloadContent.getPairs().get("exp").toString());
// exp校验,有效期14天
if (System.currentTimeMillis() - exp > 24 * 3600 * 1000 * 14) {
throw new IllegalStateException("exp校验异常,JWT已经过期");
}
// 省略其他校验项
System.out.println("JWT校验通过");
}
Точно так же, используя сгенерированный вышеJWTДля проверки результаты следующие:
当前校验的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
当前解析的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
23:33:00.174 [main] INFO club.throwable.jwt.JsonWebToken - 生成的Signature部分为:7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs
JWT校验通过
В приведенном выше коде есть проблема жесткого кодирования, просто чтобы использовать простейшийJWSПереработан метод реализацииJWTПроцесс генерации, разбора и проверки, алгоритм также использует очень низкую сложность и безопасностьHS256, поэтому не рекомендуется тратить много времени на продакшн для реализацииJWS, можно выбрать готовыйJWTбиблиотека классов, напримерauth0иjjwt.
Сценарии использования JWT и реальный бой
JWTСущность — это токен, и в большинстве сценариев он используется как сессия.ID(session_id) использование, эффект есть'维持会话的粘性'и нести информацию аутентификации (при использованииJWTтермины, которые следует безопасно пройтиClaims). Я помню тот, который я использовал давным-давноSession IDРешение генерируется и сохраняется серверомSession ID,возвращениеSession IDНужно написать пользовательскийCookie, а затем пользователь должен нестиCookie,Session IDБудет отображать некоторую информацию об аутентификации пользователя, все из которых управляются сервером, очень распространенным примером являетсяTomcatпоявление в контейнереJ(ava)SESSIONID. В отличие от предыдущей схемы,JWTЭто токен без сохранения состояния, его не нужно сохранять на сервере, и ни передаваемые данные, ни данные сеанса не должны сохраняться.JWTпросто следуйтеClaimsчестность и легитимностьJWTКогда все действительные данные закодированы и сохранены вJWTв строке. Только причинойJWTне имеет гражданства, после выдачи он получаетJWTЧерез него клиент может взаимодействовать с сервером,JWTПосле утечки это может вызвать серьезные проблемы с безопасностью, поэтому на практике вам обычно необходимо сделать следующее:
-
JWTВам нужно установить дату истечения срока действия, т.expэтоClaimПроверка суммы должна быть включена -
JWTЧерный список должен быть установлен, как правило, используетсяjtiэтоClaimВсе, технически можно использовать комбинацию фильтров Блума и баз данных (в случае небольшого количества простых операций можно даже использоватьRedisизSETтип данных) -
JWSНасколько это возможно, используйте алгоритм подписи с высоким уровнем безопасности, такой какRSXXX -
ClaimsСтарайтесь не писать конфиденциальную информацию, насколько это возможно - Сценарии высокого риска, такие как платежные операции, не могут полагаться исключительно на
JWTАутентификация требует вторичной аутентификации, такой как SMS, отпечаток пальца и т. д.
PS: Есть много проектов, в которых коллеги работают, сохраняя JWT, На самом деле это противоречит концепции дизайна JWT и использует JWT в качестве традиционного идентификатора сеанса.
JWTОбычно используется в сценариях аутентификации сAPIШлюз работает очень хорошо. в большинстве случаев,APIШлюз будет иметь некоторые общие интерфейсы, которые не требуют аутентификации, другие требуют аутентификации.JWTзаконность и добычаJWTСодержимое полезной нагрузки сообщения в вызове сделано для этого сценария:
- Для записи контроллера вы можете предоставить пользовательскую аннотацию, чтобы определить конкретный интерфейс, который необходимо выполнить.
JWTАутентификация, эта сцена находится вSpring Cloud GatewayТребуется индивидуальная реализация вJWTпроверенныйWebFilter - Для простой маршрутизации и переадресации может быть
URIСбор белого списка, попадание в белый список делать не нужноJWTАутентификация, эта сцена находится вSpring Cloud GatewayТребуется индивидуальная реализация вJWTпроверенныйGlobalFilter
нижеSpring Cloud Gatewayиjjwt, вставьте некоторый магистральный код и не раскрывайте подробности из-за нехватки места. Импорт зависимостей:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR10</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
затем написатьJwtSpiи соответствующая реализацияHMAC256JwtSpiImpl:
@Data
public class CreateJwtDto {
private Long customerId;
private String customerName;
private String customerPhone;
}
@Data
public class JwtCacheContent {
private Long customerId;
private String customerName;
private String customerPhone;
}
@Data
public class VerifyJwtResultDto {
private Boolean valid;
private Throwable throwable;
private long jwtId;
private JwtCacheContent content;
}
public interface JwtSpi {
/**
* 生成JWT
*
* @param dto dto
* @return String
*/
String generate(CreateJwtDto dto);
/**
* 校验JWT
*
* @param jwt jwt
* @return VerifyJwtResultDto
*/
VerifyJwtResultDto verify(String jwt);
/**
* 把JWT添加到封禁名单中
*
* @param jwtId jwtId
*/
void blockJwt(long jwtId);
/**
* 判断JWT是否在封禁名单中
*
* @param jwtId jwtId
* @return boolean
*/
boolean isInBlockList(long jwtId);
}
@Component
public class HMAC256JwtSpiImpl implements JwtSpi, InitializingBean, EnvironmentAware {
private SecretKey secretKey;
private Environment environment;
private int minSeed;
private String issuer;
private int seed;
private Random random;
@Override
public void afterPropertiesSet() throws Exception {
String secretKey = Objects.requireNonNull(environment.getProperty("jwt.hmac.secretKey"));
this.minSeed = Objects.requireNonNull(environment.getProperty("jwt.exp.seed.min", Integer.class));
int maxSeed = Objects.requireNonNull(environment.getProperty("jwt.exp.seed.max", Integer.class));
this.issuer = Objects.requireNonNull(environment.getProperty("jwt.issuer"));
this.random = new Random();
this.seed = (maxSeed - minSeed);
this.secretKey = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public String generate(CreateJwtDto dto) {
long duration = this.random.nextInt(this.seed) + minSeed;
Map<String, Object> claims = new HashMap<>(8);
claims.put("iss", issuer);
// 这里的jti最好用类似雪花算法之类的序列算法生成,确保唯一性
claims.put("jti", dto.getCustomerId());
claims.put("uid", dto.getCustomerId());
claims.put("exp", TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + duration);
String jwt = Jwts.builder()
.setHeaderParam("typ", "JWT")
.signWith(this.secretKey, SignatureAlgorithm.HS256)
.addClaims(claims)
.compact();
// 这里需要缓存uid->JwtCacheContent的信息
JwtCacheContent content = new JwtCacheContent();
// redis.set(KEY[uid],toJson(content),expSeconds);
return jwt;
}
@Override
public VerifyJwtResultDto verify(String jwt) {
JwtParser parser = Jwts.parserBuilder()
.requireIssuer(this.issuer)
.setSigningKey(this.secretKey)
.build();
VerifyJwtResultDto resultDto = new VerifyJwtResultDto();
try {
Jws<Claims> parseResult = parser.parseClaimsJws(jwt);
Claims claims = parseResult.getBody();
long jti = Long.parseLong(claims.getId());
if (isInBlockList(jti)) {
throw new IllegalArgumentException(String.format("jti is in block list,[i:%d]", jti));
}
long uid = claims.get("uid", Long.class);
// JwtCacheContent content = JSON.parse(redis.get(KEY[uid]),JwtCacheContent.class);
// resultDto.setContent(content);
resultDto.setValid(Boolean.TRUE);
} catch (Exception e) {
resultDto.setValid(Boolean.FALSE);
resultDto.setThrowable(e);
}
return resultDto;
}
@Override
public void blockJwt(long jwtId) {
}
@Override
public boolean isInBlockList(long jwtId) {
return false;
}
}
ПослеJwtGlobalFilterиJwtWebFilterЧастичная реализация:
@Component
public class JwtGlobalFilter implements GlobalFilter, Ordered, EnvironmentAware {
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private List<String> accessUriList;
@Autowired
private JwtSpi jwtSpi;
private static final String JSON_WEB_TOKEN_KEY = "X-TOKEN";
private static final String UID_KEY = "X-UID";
private static final String JWT_ID_KEY = "X-JTI";
@Override
public void setEnvironment(Environment environment) {
accessUriList = Arrays.asList(Objects.requireNonNull(environment.getProperty("jwt.access.uris"))
.split(","));
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// OPTIONS 请求直接放行
HttpMethod method = request.getMethod();
if (Objects.nonNull(method) && Objects.equals(method, HttpMethod.OPTIONS)) {
return chain.filter(exchange);
}
// 获取请求路径
String requestPath = request.getPath().value();
// 命中请求路径白名单
boolean matchWhiteRequestPathList = Optional.ofNullable(accessUriList)
.map(paths -> paths.stream().anyMatch(path -> pathMatcher.match(path, requestPath)))
.orElse(false);
if (matchWhiteRequestPathList) {
return chain.filter(exchange);
}
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(JSON_WEB_TOKEN_KEY);
if (!StringUtils.hasLength(token)) {
throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), "token is null");
}
VerifyJwtResultDto resultDto = jwtSpi.verify(token);
if (Objects.equals(resultDto.getValid(), Boolean.FALSE)) {
throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), resultDto.getThrowable());
}
headers.set(JWT_ID_KEY, String.valueOf(resultDto.getJwtId()));
headers.set(UID_KEY, String.valueOf(resultDto.getContent().getCustomerId()));
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
@Component
public class JwtWebFilter implements WebFilter {
@Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
@Autowired
private JwtSpi jwtSpi;
private static final String JSON_WEB_TOKEN_KEY = "X-TOKEN";
private static final String UID_KEY = "X-UID";
private static final String JWT_ID_KEY = "X-JTI";
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// OPTIONS 请求直接放行
HttpMethod method = exchange.getRequest().getMethod();
if (Objects.nonNull(method) && Objects.equals(method, HttpMethod.OPTIONS)) {
return chain.filter(exchange);
}
HandlerMethod handlerMethod = requestMappingHandlerMapping.getHandlerInternal(exchange).block();
if (Objects.isNull(handlerMethod)) {
return chain.filter(exchange);
}
RequireJWT typeAnnotation = handlerMethod.getBeanType().getAnnotation(RequireJWT.class);
RequireJWT methodAnnotation = handlerMethod.getMethod().getAnnotation(RequireJWT.class);
if (Objects.isNull(typeAnnotation) && Objects.isNull(methodAnnotation)) {
return chain.filter(exchange);
}
HttpHeaders headers = exchange.getRequest().getHeaders();
String token = headers.getFirst(JSON_WEB_TOKEN_KEY);
if (!StringUtils.hasLength(token)) {
throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), "token is null");
}
VerifyJwtResultDto resultDto = jwtSpi.verify(token);
if (Objects.equals(resultDto.getValid(), Boolean.FALSE)) {
throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), resultDto.getThrowable());
}
headers.set(JWT_ID_KEY, String.valueOf(resultDto.getJwtId()));
headers.set(UID_KEY, String.valueOf(resultDto.getContent().getCustomerId()));
return chain.filter(exchange);
}
}
И, наконец, некоторые свойства конфигурации:
jwt.hmac.secretKey='00000000111111112222222233333333'
jwt.exp.seed.min=360000
jwt.exp.seed.max=8640000
jwt.issuer='throwx'
jwt.access.uris=/index,/actuator/*
Использование ям, с которыми столкнулся JWT
Автор несет ответственностьAPIшлюз используетJWTАлгоритм, используемый в сценариях аутентификации, использует несколько более высокий уровень безопасности.RS256,использоватьRSAАлгоритм генерации подписи. В начале запуска проектаJWTСрок годности фиксируется7дней, производственный журнал обнаружилAPIФеномен «подвешенной смерти» периодически возникает в шлюзе, а конкретные проявления заключаются в следующем:
-
NginxИнтерфейс самопроверки периодически вызывает тайм-аут, и некоторые или все сообщения запрашиваются.APIУзел шлюза не работает -
APIмашины, на которой находится шлюзCPUЦиклически растет, стабилен при низком пользовательском трафике - пройти через
ELKВыполните поиск и устранение неисправностей журнала и обнаружите, что ошибка возникает в течение периода времени.JWTЦентрализованные просроченные и восстановленные трассировки журналов
Результаты расследования показывают, чтоJWTИспользуется при истечении срока действия и регенерации централизованноRSAАлгоритм подписания такойCPUИнтенсивная эксплуатация при регенерации большого количестваJWTвызовет сервис на машинеCPUПерегрузка работой.Первоначальное решение:
-
JWTПри генерации добавить к сроку действия случайное число, например360000(1小时的毫秒数) ~ 8640000(24小时的毫秒数)Возьмите случайное значение между и добавьте к текущей метке времени плюс7день, чтобы получитьexpценность
Этот метод для некоторых старых сценариев пользовательского маркетинга (старые пользователи не входили в систему в течение длительного времени, их клиентская сторона кэшировалаJWTобычно просрочен) не имеет никакого эффекта. Иногда операция пробуждает старых пользователей с помощью маркетинговых мероприятий, и большое количество старых пользователей может резко повторно войти в систему и восстановиться в больших количествах.JWTВ этом случае предлагается два решения:
- первая сборка
JWTвремя, рассмотрите возможность продления срока действия, но чем дольше время, тем выше риск - продвигать
APIАппаратная конфигурация машины, на которой расположен шлюз, особенноCPUКонфигурация, многие поставщики облачных услуг теперь имеют гибкие планы расширения, которые вполне могут справиться с такими сценариями всплесков трафика.
резюме
Основной потокJWTСхемаJWS, эта схема только кодирование и подпись, а не шифрование, обязательно обратите на это внимание,JWSРешение не имеет состояния и небезопасно.Для ключевых операций необходимо выполнять несколько аутентификаций, а также должен быть реализован механизм черного списка, чтобы предотвратитьJWTПроблема безопасности возникает после утечки.JWTНе хранится на сервере, в этом его преимущество и недостаток. Многие программные архитектуры не могут быть идеальными, и в настоящее время мы можем только взвесить все за и против.
Использованная литература:
(Конец этой статьи c-3-w e-a-20210219)