Реальные боевые галантерейные товары Spring Security: научим вас внедрять JWT Token

задняя часть Spring

jwt.png

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获取更多资讯

Личный блог: https://felord.cn