1. Введение
Json Web Token(JWT
) В последние годы он обычно используется для разделения интерфейса и сервера.Tokenтехнологии, в настоящее время является самым популярным решением для междоменной аутентификации. вы можете прочитать статьюСтатья для понимания технологии токенов веб-сеансов без сохранения состояния JWT.чтобы понятьJWT
. Сегодня мы напишем универсальныйJWT
Служить.DEMOСпособ приобретения в конце статьи, а реализация наjwt
связанные пакеты
2. spring-security-jwt
spring-security-jwt
даSpring Security Cryptoкоторый предоставилJWT
Инструментарий .
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>${spring-security-jwt.version}</version>
</dependency>
Существует только один основной класс:org.springframework.security.jwt.JwtHelper
. Он предоставляет два очень полезных статических метода.
3. JWT-кодирование
JwtHelper
Первый предоставленный статический метод:encode(CharSequence content, Signer signer)
Это метод, используемый для генерации jwt, необходимо указатьpayloadа такжеsignerАлгоритм подписи.payloadсохранены некоторые доступныеНе чувствительныйИнформация:
-
iss
эмитент jwt -
sub
Пользователи jwt -
aud
Сторона, получающая jwt -
iat
Время выпуска jwt -
exp
Время истечения jwt, это время истечения должно быть больше, чем время выдачиiat
-
jti
Уникальная идентификация jwt в основном используется как одноразовый токен, чтобы избежать повторных атак.
В дополнение к основной информации, предоставленной выше, мы можем определить некоторую информацию, которую нам нужно передать, например, набор разрешений целевого пользователя и так далее.Помните, что нельзя передавать конфиденциальную информацию, такую как пароли.,так какJWT
Первые два абзаца используютсяBASE64
Кодировка почти текстовая.
3.1 Создание полезной нагрузки в JWT
Давайте сначала построимpayload :
/**
* 构建 jwt payload
*
* @author Felordcn
* @since 11:27 2019/10/25
**/
public class JwtPayloadBuilder {
private Map<String, String> payload = new HashMap<>();
/**
* 附加的属性
*/
private Map<String, String> additional;
/**
* jwt签发者
**/
private String iss;
/**
* jwt所面向的用户
**/
private String sub;
/**
* 接收jwt的一方
**/
private String aud;
/**
* jwt的过期时间,这个过期时间必须要大于签发时间
**/
private LocalDateTime exp;
/**
* jwt的签发时间
**/
private LocalDateTime iat = LocalDateTime.now();
/**
* 权限集
*/
private Set<String> roles = new HashSet<>();
/**
* jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
**/
private String jti = IdUtil.simpleUUID();
public JwtPayloadBuilder iss(String iss) {
this.iss = iss;
return this;
}
public JwtPayloadBuilder sub(String sub) {
this.sub = sub;
return this;
}
public JwtPayloadBuilder aud(String aud) {
this.aud = aud;
return this;
}
public JwtPayloadBuilder roles(Set<String> roles) {
this.roles = roles;
return this;
}
public JwtPayloadBuilder expDays(int days) {
Assert.isTrue(days > 0, "jwt expireDate must after now");
this.exp = this.iat.plusDays(days);
return this;
}
public JwtPayloadBuilder additional(Map<String, String> additional) {
this.additional = additional;
return this;
}
public String builder() {
payload.put("iss", this.iss);
payload.put("sub", this.sub);
payload.put("aud", this.aud);
payload.put("exp", this.exp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
payload.put("iat", this.iat.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
payload.put("jti", this.jti);
if (!CollectionUtils.isEmpty(additional)) {
payload.putAll(additional);
}
payload.put("roles", JSONUtil.toJsonStr(this.roles));
return JSONUtil.toJsonStr(JSONUtil.parse(payload));
}
}
по классу зданияJwtClaimsBuilder
Мы можем легко построитьJWT
нужныйpayload json
строка переданаencode(CharSequence content, Signer signer)
серединаcontent
.
3.2 Генерация ключа RSA и подписи
чтобы генерироватьJWT TokenНам также нужно использоватьRSAАлгоритм подписи. Здесь мы используемJDKПредоставляемые инструменты управления сертификатамиKeytoolгенерироватьсертификат RSA, в форматеjks
Формат.
Ссылка на команду создания сертификата:
keytool -genkey -alias felordcn -keypass felordcn -keyalg RSA -storetype PKCS12 -keysize 1024 -validity 365 -keystore d:/keystores/felordcn.jks -storepass 123456 -dname "CN=(Felord), OU=(felordcn), O=(felordcn), L=(zz), ST=(hn), C=(cn)"
в-alias felordcn -storepass 123456
Мы хотим использовать его как конфигурацию для записи. Мы собираемся использовать этот класс, определенный ниже, для чтения сертификата.
package cn.felord.spring.security.jwt;
import org.springframework.core.io.ClassPathResource;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.spec.RSAPublicKeySpec;
/**
* KeyPairFactory
*
* @author Felordcn
* @since 13:41 2019/10/25
**/
class KeyPairFactory {
private KeyStore store;
private final Object lock = new Object();
/**
* 获取公私钥.
*
* @param keyPath jks 文件在 resources 下的classpath
* @param keyAlias keytool 生成的 -alias 值 felordcn
* @param keyPass keytool 生成的 -keypass 值 felordcn
* @return the key pair 公私钥对
*/
KeyPair create(String keyPath, String keyAlias, String keyPass) {
ClassPathResource resource = new ClassPathResource(keyPath);
char[] pem = keyPass.toCharArray();
try {
synchronized (lock) {
if (store == null) {
synchronized (lock) {
store = KeyStore.getInstance("jks");
store.load(resource.getInputStream(), pem);
}
}
}
RSAPrivateCrtKey key = (RSAPrivateCrtKey) store.getKey(keyAlias, pem);
RSAPublicKeySpec spec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(spec);
return new KeyPair(publicKey, key);
} catch (Exception e) {
throw new IllegalStateException("Cannot load keys from store: " + resource, e);
}
}
}
приобретенныйKeyPair
Вы можете получить открытый и закрытый ключи для созданияJwt
Два элемента готовы. Мы можем использовать ранее определенныйJwtPayloadBuilder
инкапсулировать и генерировать вместеJwt TokenМетоды:
private String jwtToken(String aud, int exp, Set<String> roles, Map<String, String> additional) {
String payload = jwtPayloadBuilder
.iss(jwtProperties.getIss())
.sub(jwtProperties.getSub())
.aud(aud)
.additional(additional)
.roles(roles)
.expDays(exp)
.builder();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RsaSigner signer = new RsaSigner(privateKey);
return JwtHelper.encode(payload, signer).getEncoded();
}
Обычно токены Jwt появляются парами, один переносится обычными запросамиaccessToken
, другой просто как обновлениеaccessToken
заrefreshToken
. а такжеrefreshToken
Срок годности относительно большой. когдаaccessToken
неэффективныйrefreshToken
Когда действительно, мы можем пройтиrefreshToken
получить новыйПара токенов JWT; когда оба терпят неудачу, пользователь должен снова войти в систему.
генерироватьПара токенов JWTМетод заключается в следующем:
public JwtTokenPair jwtTokenPair(String aud, Set<String> roles, Map<String, String> additional) {
String accessToken = jwtToken(aud, jwtProperties.getAccessExpDays(), roles, additional);
String refreshToken = jwtToken(aud, jwtProperties.getRefreshExpDays(), roles, additional);
JwtTokenPair jwtTokenPair = new JwtTokenPair();
jwtTokenPair.setAccessToken(accessToken);
jwtTokenPair.setRefreshToken(refreshToken);
// 放入缓存
jwtTokenStorage.put(jwtTokenPair, aud);
return jwtTokenPair;
}
как правилоПара токенов JWTОн будет помещен в кеш при возвращении на передний план. Вы можете выбрать обработку политики истечения срока действия отдельно илиrefreshToken
Срок годности имеет преимущественную силу.
4. Декодирование и проверка JWT
JwtHelper
Второй статический метод:Jwt decodeAndVerify(String token, SignatureVerifier verifier)
используется для проверки и расшифровкиJwt Token. После того, как мы получим токен в запросе, мы проанализируем некоторую информацию о пользователе. Используйте эту информацию, чтобы перейти к соответствующему токену в кэше, затем сравните и проверьте, является ли он действительным (включая срок его действия).
/**
* 解码 并校验签名 过期不予解析
*
* @param jwtToken the jwt token
* @return the jwt claims
*/
public JSONObject decodeAndVerify(String jwtToken) {
Assert.hasText(jwtToken, "jwt token must not be bank");
RSAPublicKey rsaPublicKey = (RSAPublicKey) this.keyPair.getPublic();
SignatureVerifier rsaVerifier = new RsaVerifier(rsaPublicKey);
Jwt jwt = JwtHelper.decodeAndVerify(jwtToken, rsaVerifier);
String claims = jwt.getClaims();
JSONObject jsonObject = JSONUtil.parseObj(claims);
String exp = jsonObject.getStr(JWT_EXP_KEY);
// 是否过期
if (isExpired(exp)) {
throw new IllegalStateException("jwt token is expired");
}
return jsonObject;
}
Выше мы будем эффективноJwt Tokenсерединаpayload
решаетJSON-объект, что удобно для последующих операций.
5. Конфигурация
мы будемJWTНастраиваемые элементы извлекаются и вставляютсяJwtProperties
следующее:
/**
* Jwt 在 springboot application.yml 中的配置文件
*
* @author Felordcn
* @since 15 :06 2019/10/25
*/
@Data
@ConfigurationProperties(prefix=JWT_PREFIX)
public class JwtProperties {
static final String JWT_PREFIX= "jwt.config";
/**
* 是否可用
*/
private boolean enabled;
/**
* jks 路径
*/
private String keyLocation;
/**
* key alias
*/
private String keyAlias;
/**
* key store pass
*/
private String keyPass;
/**
* jwt签发者
**/
private String iss;
/**
* jwt所面向的用户
**/
private String sub;
/**
* access jwt token 有效天数
*/
private int accessExpDays;
/**
* refresh jwt token 有效天数
*/
private int refreshExpDays;
}
Затем мы можем настроитьJWTизjavaConfig
следующее:
/**
* JwtConfiguration
*
* @author Felordcn
* @since 16 :54 2019/10/25
*/
@EnableConfigurationProperties(JwtProperties.class)
@ConditionalOnProperty(prefix = "jwt.config",name = "enabled")
@Configuration
public class JwtConfiguration {
/**
* Jwt token storage .
*
* @return the jwt token storage
*/
@Bean
public JwtTokenStorage jwtTokenStorage() {
return new JwtTokenCacheStorage();
}
/**
* Jwt token generator.
*
* @param jwtTokenStorage the jwt token storage
* @param jwtProperties the jwt properties
* @return the jwt token generator
*/
@Bean
public JwtTokenGenerator jwtTokenGenerator(JwtTokenStorage jwtTokenStorage, JwtProperties jwtProperties) {
return new JwtTokenGenerator(jwtTokenStorage, jwtProperties);
}
}
Тогда вы можете пройтиJwtTokenGenerator
Проверка кодирования/декодированияПара токенов JWT,пройти черезJwtTokenStorage
иметь дело сJwt Tokenкеш. Я использую кеш здесьSpring Cache Ehcacheдля достижения, вы также можете переключиться наRedis. Связанные модульные тесты см.DEMO
6. Резюме
Сегодня мы используемspring-security-jwt
рукописный наборJWTлогика. каким бы ни был ваш последующий союзSpring Securityвсе ещеShiroВсе очень поучительно. Далее мы объяснимJWTкомбинироватьSpring Security, пожалуйста, обратите внимание на общедоступный номер:Felordcnдля своевременного получения информации.
На этот DEMO также можно ответить, подписавшись на официальный аккаунт.day05Получать.
关注公众号:Felordcn获取更多资讯