Фактический бой Spring Cloud Alibaba — как сделать токен jwt недействительным?

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

Всем привет, я Мисти.

Поклонник, прочитавший мою серию статей о SpringCloud Alibaba, спросил меня в частном порядке: как решить проблему сбоя jwt? После смены пароля или выхода из системы предыдущий токен jwt необходимо аннулировать, а старый токен не может войти в систему.

Я сказал: это очень просто, давайте прямоНичего не делать, когда пользователь выходит из системы или меняет пароль, разве это не конец внешнего интерфейса, чтобы напрямую удалить токен? Бэкенд не должен ни о чем беспокоиться и ничего не должен делать.

Он сказал: Не создавайте проблем, я дам вам три ссылки подряд на каждую из ваших статей.

В то время я был тронут.Так как я такой хороший читатель, я решительно согласился написать для него статью, чтобы поделиться своей незрелой практикой и преобразовать этот проект SpringCloud Alibaba.

Перед официальным стартом давайте рассмотрим соответствующие знания о токенах в oauth2.

Обзор знаний

Как мы все знаем, информация о токенах, возвращаемая после аутентификации в системе OAuth2, делится на две категории:непрозрачные токеныа такжеПрозрачные токены (не непрозрачные токены).

непрозрачный токенявляется нечитаемым токеном, как правило, простой строкой UUID. При использовании непрозрачного токена служба ресурсов не знает, что это за токен и кого он представляет, и ей необходимо вызвать сервер проверки подлинности для проверки и получения информации о пользователе. Использование непрозрачных токенов требуетцентрализованныйархитектура.

прозрачный токенОбычно относится к токену JWT, о котором мы часто говорим, информация о пользователе хранится в строке JWT, и сервер ресурсов может сам анализировать токен, и ему больше не нужно обращаться к серверу аутентификации для проверки токена. Использование JWT является частьюБез гражданства и децентрализованоархитектура.

Как только мы решили использовать JWT, мы должны четко понимать: единственный способ аннулировать JWT без внешней силы — это дождаться истечения срока действия токена, а активно аннулировать JWT невозможно. Если JWT должен иметь функцию активного отказа, можно использовать только внешнюю силу, то есть состояние JWT сохраняется на стороне сервера, а логика суждения добавляется при выполнении запроса.Безотказные и децентрализованные функции JWTпротиворечиво. Однако, поскольку вы выбрали путь JWT, вы можете только принять эту реальность.

Советы: наш текущий проект использует децентрализованную архитектуру токена JWT и создал модуль конфигурации сервера унифицированных ресурсов.Подробнее см.:SpringCloud Микросервисы Alibaba на практике Тридцать | Модуль конфигурации сервера унифицированных ресурсов.

Решения

Как упоминалось выше, для реализации активного аннулирования JWT необходимо использовать внешнюю силу для сохранения состояния JWT на стороне сервера, как правило, с использованием кеша, такого как Redis. Существует два варианта сохранения состояния JWT:

  1. Механизм белого списка

    При сертификации, сохраните JWT в Redis. При выходе удалите JWT из кеша. Ресурс запроса добавляется, чтобы определить, существует ли JWT в кеше и нет ли отказа в доступе. Этот метод в основном такой же, как сеанс удаления аннулирования сеанса в механизме cookie/сеанса.

  2. механизм черного списка

    При выходе из системы, кешировать JWT в Redis, а срок действия кеша устанавливается равным сроку действия JWT.При запросе ресурсов оценивается, существует ли он в черном списке кеша, и доступ запрещается, если он существует.

Логика реализации белого и черного списков аналогична: черному списку не нужно кэшировать JWT каждый раз, когда вы входите в систему.

Я предпочитаю использовать механизм черного списка по двум причинам:

Во-первых, это значительно сэкономит место для хранения Redis, нам даже не нужно хранить весь jwt, а только уникальный id jti в jwt.

image-20210909195008939

Во-вторых, у нас есть независимый модуль конфигурации сервера ресурсов.Если мы используем белый список, все зависимые бизнес-модули должны быть добавлены в зависимости Redis, и добавляется конфигурация Redis, что не очень удобно. Если используется черный список, нам нужно проверить только на уровне шлюза, то есть до тех пор, пока шлюз и сервер аутентификации добавляют зависимости redis.

Хорошо, теперь, когда механизм черного списка определен, давайте, наконец, разберемся с полным процессом реализации:

  1. Сервер аутентификации должен добавить интерфейс выхода из системы, добавить уникальный идентификатор jwt jti в redis при выходе из системы и установить допустимое время как оставшееся время токена.
  2. Все запросы должны проходить через шлюз, уровень шлюза добавляет логику проверки через фильтр для анализа и определения того, находится ли токен jti, переносимый пользователем, в Redis. Если он существует, это означает, что токен недействителен и доступ запрещен, в противном случае он освобождается.

Разобравшись с процессом реализации, приступаем к преобразованию кода.

модификация кода

Обратите внимание, чтобы не мешать чтению, код, показанный в этой статье, является только кодом, относящимся к теме этой статьи, а другие нерелевантные коды заменены на ....

Преобразование сервера аутентификации

  1. Поскольку для хранения черных списков необходимо использовать redis, необходимо ввести зависимости redis
<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-redis</artifactId>
</dependency>
  1. Изменить центр конфигурации nacosauth-service.yml, настроить свойства, связанные с Redis
spring:
  redis:
    host: localhost
    password: xxxxxx
    port: 6379
    timeout: 3000
  1. Напишите интерфейс выхода и вставьте токен в черный список
@RestController
@RequestMapping("/token")
@Slf4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AuthController {

    private final TokenStore tokenStore;

    private final RedisTemplate<String,String> redisTemplate;
    
    /**
     * 用户退出登录
     * @param authHeader 从请求头获取token
     */
    @DeleteMapping("/logout")
    public ResultData<String> logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION) String authHeader){

        //获取token,去除前缀
        String token = authHeader.replace(OAuth2AccessToken.BEARER_TYPE,"").trim();

        // 解析Token
        OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token);

        //token 已过期
        if(oAuth2AccessToken.isExpired()){
           return ResultData.fail(ReturnCode.INVALID_TOKEN_OR_EXPIRED.getCode(),ReturnCode.INVALID_TOKEN_OR_EXPIRED.getMessage());
        }

        if(StringUtils.isBlank(oAuth2AccessToken.getValue())){
            //访问令牌不合法
            return ResultData.fail(ReturnCode.INVALID_TOKEN.getCode(),ReturnCode.INVALID_TOKEN.getMessage());
        }

        OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(oAuth2AccessToken);

        String userName = oAuth2Authentication.getName();

        //获取token唯一标识
        String jti = (String) oAuth2AccessToken.getAdditionalInformation().get("jti");

        //获取过期时间
        Date expiration = oAuth2AccessToken.getExpiration();
        long exp = expiration.getTime() / 1000;

        long currentTimeSeconds = System.currentTimeMillis() / 1000;

        //设置token过期时间
        redisTemplate.opsForValue().set(CloudConstant.TOKEN_BLACKLIST_PREFIX + jti, userName, (exp - currentTimeSeconds), TimeUnit.SECONDS);

        return ResultData.success("退出成功");
    }

}

Сервер аутентификации может пройтиOAuth2AccessTokenНепосредственно проанализируйте токен, выполните некоторые базовые проверки действительности токена, рассчитайте оставшееся время действия токена и передайтеredisTemplateПрисоединяйтесь к редису.

При вставке черного списка полный токен jwt не вставляется напрямую, а используется уникальный идентификатор jti jwt для экономии места в хранилище Redis.

  1. Измените файл конфигурации сервера аутентификации.WebSecurityConfig, отпустите выходной интерфейс
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
//@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	...
    
    /**
     * http安全配置
     * @param http http安全对象
     * @throws Exception http安全异常信息
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 加入验证码登陆
        http.apply(smsCodeSecurityConfig);

        http
                .authorizeRequests().requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
                .and()
                .authorizeRequests().antMatchers("/token/**","/sms/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable();
    }

	...
}

На этом преобразование сервера аутентификации завершено, а затем преобразован шлюз.

Трансформация шлюза

Уровень шлюза больше не вводит зависимости, связанные с oauth2, поэтому метод сервера аутентификации нельзя использовать для анализа токена.nimbus-jose-jwtэтот класс инструментов.

  1. вводитьnimbus-jose-jwtполагаться
<dependency>
  <groupId>com.nimbusds</groupId>
  <artifactId>nimbus-jose-jwt</artifactId>
  <version>9.13</version>
</dependency>
  1. Внедрение зависимостей Redis и настройка свойств, связанных с Redis.

Это было настроено при преобразовании сервера аутентификации, поэтому оно не будет повторяться.

  1. Изменить фильтр шлюзаGatewayRequestFilter, чтобы определить, существует ли токен в черном списке
@Component
@Order(0)
@Slf4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class GatewayRequestFilter implements GlobalFilter {

    private final RedisTemplate<String,String> redisTemplate;

    @SneakyThrows
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
			...
        //获取请求头的token
        String headerToken = exchange.getRequest().getHeaders().getFirst(CloudConstant.JWT_HEADER_KEY);

        if (StrUtil.isNotEmpty(headerToken)) {
            // 是否在黑名单
            if(isBlack(headerToken)){
                throw new HttpServerErrorException(HttpStatus.FORBIDDEN,"该令牌已过期,请重新获取令牌");
            }
        }
      ...

        return chain.filter(newExchange);
    }



    /**
     * 通过redis判断token是否为黑名单
     * @param headerToken 请求头
     * @return boolean
     */
    private boolean isBlack(String headerToken) throws ParseException {
        //todo  移除所有oauth2相关代码,暂时使用 OAuth2AccessToken.BEARER_TYPE 代替
        String token  = headerToken.replace(OAuth2AccessToken.BEARER_TYPE, StrUtil.EMPTY).trim();

        //解析token
        JWSObject jwsObject = JWSObject.parse(token);
        String payload = jwsObject.getPayload().toString();
        JSONObject jsonObject = JSONUtil.parseObj(payload);

        // JWT唯一标识
        String jti = jsonObject.getStr("jti");
        return redisTemplate.hasKey(CloudConstant.TOKEN_BLACKLIST_PREFIX + jti);
    }
	...
}

До сих пор слой шлюза был преобразован, теперь давайте проверим весь процесс.

контрольная работа

  1. выполнить выход из системы

image-20210908141930756

image-20210907154748658

После выхода из системы вы можете увидеть черный список в Redis.Ключ — jti, а значение — вошедший в систему пользователь.

  1. Доступ к другим интерфейсам с просроченными токенами будет перехвачен шлюзом

image-20210908103635321

резюме

Вообще говоря, поскольку вы выбираете jwt, вы должны принять соглашение jwt, которое не допускает активного сбоя.Если вы считаете безопасность, вы можете сократить время действия jwt и использовать https и другие средства для защиты. Хотя мы реализовали в нем функцию активного сбоя посредством преобразования кода, в конечном итоге он утратил характеристики jwt** без сохранения состояния и децентрализации. **Конечно, все решения не могут быть идеальными, в основном нужно посмотреть, какое из них больше подходит для вашего бизнес-сценария.

Советы: Исходный код серии микросервисов SpringCloud alibaba был загружен на GitHub.Если вам нужно обратить внимание на эту общедоступную учетную запись JAVA, ежедневную запись и ответьте на ключевые слова2214Получите адрес исходного кода.