Почему нам нужно загружать данные пользователя из БД для каждого запроса при аутентификации JWT с помощью Spring Security?

В настоящее время я реализую аутентификацию JWT в приложении Spring Boot. В большинстве руководств и примеров я вижу, что метод UserDetailsService.loadUserByUsername вызывается для каждого запроса для проверки токена. Вот фрагмент моего фильтра:

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
    private static final String BEARER_PREFIX = "Bearer ";
    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final int TOKEN_START_INDEX = 7;


    private final JwtService jwtService;
    private final UserDetailsService jpaUserDetailsService;

    @Override
    protected void doFilterInternal(final HttpServletRequest request,
                                    final HttpServletResponse response,
                                    final FilterChain filterChain) throws ServletException, IOException {
        String authHeader = request.getHeader(AUTHORIZATION_HEADER);
        if (hasBearerHeader(authHeader)) {
            String token = extractToken(authHeader);
            String username = jwtService.extractUsername(token);

            if (username != null && !isUserAuthenticated()) {
                UserDetails userDetails = jpaUserDetailsService.loadUserByUsername(username);
                if (jwtService.isTokenValid(token, userDetails)) {
                    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                            userDetails,
                            null,
                            userDetails.getAuthorities()
                    );
                    authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                }
            }
        }

        filterChain.doFilter(request, response);
    }

    private boolean hasBearerHeader(final String authHeader) {
        return StringUtils.hasText(authHeader) && authHeader.startsWith(BEARER_PREFIX);
    }

    private String extractToken(String authHeader) {
        return Optional.of(authHeader)
                .filter(s -> s.length() > TOKEN_START_INDEX)
                .map(s -> s.substring(TOKEN_START_INDEX))
                .orElseThrow(() -> new BadCredentialsException("Invalid Authorization header: Bearer token is missing or invalid."));
    }

    private boolean isUserAuthenticated() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication != null && authentication.isAuthenticated();
    }
}

JwtService:

@Service
public class JwtService {
    private static final int TOKEN_VALIDITY_HOURS = 1;
    private static final int SECONDS_PER_HOUR = 3600;
    private static final int TOKEN_VALIDITY_SECONDS = TOKEN_VALIDITY_HOURS * SECONDS_PER_HOUR;
    private static final String ALGORITHM = "HmacSHA256";

    @Value("${secret.key}")
    private String secretKey;

    public String generateToken(final UserDetails userDetails) {
        return generateToken(new HashMap<>(), userDetails);
    }

    public String generateToken(final Map<String, ?> claims, final UserDetails userDetails) {
        Instant currentTime = Instant.now();
        Instant expirationTime = currentTime.plusSeconds(TOKEN_VALIDITY_SECONDS);

        return Jwts
                .builder()
                .claims(claims)
                .subject(userDetails.getUsername())
                .issuedAt(Date.from(currentTime))
                .expiration(Date.from(expirationTime))
                .signWith(getSecretKey(), Jwts.SIG.HS256)
                .compact();
    }

    public boolean isTokenValid(final String token, final UserDetails userDetails) {
        String usernameFromToken = extractUsername(token);
        return usernameFromToken.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    private boolean isTokenExpired(final String token) {
        return extractExpiration(token).before(new Date());
    }

    public String extractUsername(final String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public Date extractExpiration(final String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    private <T> T extractClaim(final String token, final Function<Claims, T> extractor) {
        Claims claims = Jwts.parser()
                .verifyWith(getSecretKey())
                .build()
                .parseSignedClaims(token)
                .getPayload();

        return extractor.apply(claims);

    }

    private SecretKey getSecretKey() {
        byte[] decodedKey = Base64.getDecoder().decode(secretKey);
        return new SecretKeySpec(decodedKey, 0, decodedKey.length, ALGORITHM);
    }
}

Меня смущает необходимость загрузки данных пользователя из базы данных при каждом запросе. Если токен выдан и проверен, не означает ли это, что пользователь уже аутентифицирован? Разве мы не можем просто проверить подпись и утверждения токена, не обращаясь каждый раз к базе данных?

Я ценю любые подробные объяснения и рекомендации о том, как эффективно обрабатывать аутентификацию JWT в Spring Security.

If the token is issued and verified, doesn't it mean the user is already authenticated Да, попробуйте другой урок. В этом уроке, возможно, основное внимание уделяется roles тому, с чем Spring должен справиться, декодируя JWT. Роли могут передаваться в JWT, или Spring может искать пользователя внутри и использовать то, что там сохранено, как в вашем случае.
K.Nicholas 09.06.2024 16:22
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
1
119
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Эффективная аутентификация JWT в Spring Security

Ответ - вообще не использовать Jwts.

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

И многие разработчики не учатся, читая официальную документацию, а просто ищут руководство в Google.

Есть причина, по которой в Spring Security нет встроенного JWTFilter, потому что он не является официальным стандартом аутентификации. На самом деле, многие официальные источники прямо рекомендуют это делать.

Окта Редис

А последняя рекомендация IATF (организации, которая занимается oauth2) строго рекомендует против потока предоставления пароля (потока, который выдает токены непосредственно после имени пользователя и пароля), и он будет полностью удален в oauth2.1.

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

JWT следует использовать только для связи между серверами при однократной авторизации.

Если вы не создаете что-то, обрабатывающее 10 000 запросов в секунду в корпоративной среде, вам не понадобится микроуправление запросами. Просто используйте сеансы cookie, а если вам нужно поделиться сеансами с еще парой микросервисов, реализуйте весенний сеанс и используйте что-то вроде Redis для совместного использования сеансов.

Это уже реализовано в Spring Security и называется FormLogin, это проще и намного безопаснее.

Просто прочитайте Главу «Архитектура безопасности Spring» , а затем внедрите FormLogin.

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

настолько верно, что это не дает никаких шансов не проголосовать за это.

Andrey B. Panfilov 10.06.2024 07:58

@Toerktumlare Спасибо за ответ и обмен опытом. Это было немного неожиданно, но познавательно. Я все еще нахожусь на ранней стадии изучения этой темы и еще не углублялся в документацию, поэтому, возможно, забегу немного вперед. Мне искренне любопытно понять, не противоречит ли это предложение потенциальному принципу безгражданства REST?

SorryForAsking 10.06.2024 20:41

Какое отношение безопасность имеет к REST? Отдых есть отдых, безопасность есть безопасность. У них нет ничего общего.

Toerktumlare 10.06.2024 22:51

Вы имеете в виду, что безгражданство в REST связано с ресурсами, а не с безопасностью?

SorryForAsking 10.06.2024 23:44

Я говорю, что ОТДЫХ. это одно, а безопасность - это другое. Тот факт, что REST не имеет состояния, не означает, что безопасность тоже.

Toerktumlare 11.06.2024 07:23

Создание Authentication из токена доступа Bearer происходит на серверах ресурсов OAuth2: Security(Web)FilterChain настроено с помощью oauth2ResourceServers, который адаптирован для REST API без сохранения состояния (без сеанса). Это делает микросервис супермасштабируемым и отказоустойчивым. Но обратите внимание, что сервер ресурсов не заботится о том, как был получен токен Bearer (какой поток использовался). Это означает, что сервер ресурсов OAuth2 никогда не будет отвечать за вход пользователей (это работа клиентов OAuth2).

Имейте в виду, что серверы ресурсов OAuth2 могут запрашиваться только клиентами OAuth2, а одностраничные или мобильные приложения делают небезопасных клиентов (публичными). Лучше использовать OAuth2 BFF.

Короче говоря: используйте код авторизации BFF и сохраняйте токены в сеансе, а затем создайте аутентификацию из JWT на сервере ресурсов Spring без сохранения состояния OAuth2.

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

Другие вопросы по теме