Всем привет, я Мисти.
Поклонник, прочитавший мою серию статей о 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:
-
Механизм белого списка
При сертификации, сохраните JWT в Redis. При выходе удалите JWT из кеша. Ресурс запроса добавляется, чтобы определить, существует ли JWT в кеше и нет ли отказа в доступе. Этот метод в основном такой же, как сеанс удаления аннулирования сеанса в механизме cookie/сеанса.
-
механизм черного списка
При выходе из системы, кешировать JWT в Redis, а срок действия кеша устанавливается равным сроку действия JWT.При запросе ресурсов оценивается, существует ли он в черном списке кеша, и доступ запрещается, если он существует.
Логика реализации белого и черного списков аналогична: черному списку не нужно кэшировать JWT каждый раз, когда вы входите в систему.
Я предпочитаю использовать механизм черного списка по двум причинам:
Во-первых, это значительно сэкономит место для хранения Redis, нам даже не нужно хранить весь jwt, а только уникальный id jti в jwt.
Во-вторых, у нас есть независимый модуль конфигурации сервера ресурсов.Если мы используем белый список, все зависимые бизнес-модули должны быть добавлены в зависимости Redis, и добавляется конфигурация Redis, что не очень удобно. Если используется черный список, нам нужно проверить только на уровне шлюза, то есть до тех пор, пока шлюз и сервер аутентификации добавляют зависимости redis.
Хорошо, теперь, когда механизм черного списка определен, давайте, наконец, разберемся с полным процессом реализации:
- Сервер аутентификации должен добавить интерфейс выхода из системы, добавить уникальный идентификатор jwt jti в redis при выходе из системы и установить допустимое время как оставшееся время токена.
- Все запросы должны проходить через шлюз, уровень шлюза добавляет логику проверки через фильтр для анализа и определения того, находится ли токен jti, переносимый пользователем, в Redis. Если он существует, это означает, что токен недействителен и доступ запрещен, в противном случае он освобождается.
Разобравшись с процессом реализации, приступаем к преобразованию кода.
модификация кода
Обратите внимание, чтобы не мешать чтению, код, показанный в этой статье, является только кодом, относящимся к теме этой статьи, а другие нерелевантные коды заменены на ....
Преобразование сервера аутентификации
- Поскольку для хранения черных списков необходимо использовать redis, необходимо ввести зависимости redis
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
- Изменить центр конфигурации nacos
auth-service.yml
, настроить свойства, связанные с Redis
spring:
redis:
host: localhost
password: xxxxxx
port: 6379
timeout: 3000
- Напишите интерфейс выхода и вставьте токен в черный список
@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.
- Измените файл конфигурации сервера аутентификации.
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
этот класс инструментов.
- вводить
nimbus-jose-jwt
полагаться
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.13</version>
</dependency>
- Внедрение зависимостей Redis и настройка свойств, связанных с Redis.
Это было настроено при преобразовании сервера аутентификации, поэтому оно не будет повторяться.
- Изменить фильтр шлюза
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);
}
...
}
До сих пор слой шлюза был преобразован, теперь давайте проверим весь процесс.
контрольная работа
- выполнить выход из системы
После выхода из системы вы можете увидеть черный список в Redis.Ключ — jti, а значение — вошедший в систему пользователь.
- Доступ к другим интерфейсам с просроченными токенами будет перехвачен шлюзом
резюме
Вообще говоря, поскольку вы выбираете jwt, вы должны принять соглашение jwt, которое не допускает активного сбоя.Если вы считаете безопасность, вы можете сократить время действия jwt и использовать https и другие средства для защиты. Хотя мы реализовали в нем функцию активного сбоя посредством преобразования кода, в конечном итоге он утратил характеристики jwt** без сохранения состояния и децентрализации. **Конечно, все решения не могут быть идеальными, в основном нужно посмотреть, какое из них больше подходит для вашего бизнес-сценария.
Советы: Исходный код серии микросервисов SpringCloud alibaba был загружен на GitHub.Если вам нужно обратить внимание на эту общедоступную учетную запись JAVA, ежедневную запись и ответьте на ключевые слова2214Получите адрес исходного кода.