В настоящее время я реализую аутентификацию 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.
Эффективная аутентификация JWT в Spring Security
Ответ - вообще не использовать Jwts.
Jwts никогда не создавались для использования в качестве замены сеанса, и большинство официальных источников не рекомендуют использовать JWT в браузерах. Но печальная правда заключается в том, что авторы учебных пособий — это эхо-камеры. Они читают другие руководства, а затем создают свои собственные, со своим вкусом.
И многие разработчики не учатся, читая официальную документацию, а просто ищут руководство в Google.
Есть причина, по которой в Spring Security нет встроенного JWTFilter, потому что он не является официальным стандартом аутентификации. На самом деле, многие официальные источники прямо рекомендуют это делать.
Окта Редис
А последняя рекомендация IATF (организации, которая занимается oauth2) строго рекомендует против потока предоставления пароля (потока, который выдает токены непосредственно после имени пользователя и пароля), и он будет полностью удален в oauth2.1.
Итак, печальная правда заключается в том, что многие создатели учебных пособий больше заботятся о кликах, чем о точности, и не понимают, что рекомендации меняются со временем, но они не обновляют свой контент соответствующим образом.
JWT следует использовать только для связи между серверами при однократной авторизации.
Если вы не создаете что-то, обрабатывающее 10 000 запросов в секунду в корпоративной среде, вам не понадобится микроуправление запросами. Просто используйте сеансы cookie, а если вам нужно поделиться сеансами с еще парой микросервисов, реализуйте весенний сеанс и используйте что-то вроде Redis для совместного использования сеансов.
Это уже реализовано в Spring Security и называется FormLogin
, это проще и намного безопаснее.
Просто прочитайте Главу «Архитектура безопасности Spring» , а затем внедрите FormLogin.
Пожалуйста, изучайте безопасность из официальной документации, а не случайных блоггеров.
настолько верно, что это не дает никаких шансов не проголосовать за это.
@Toerktumlare Спасибо за ответ и обмен опытом. Это было немного неожиданно, но познавательно. Я все еще нахожусь на ранней стадии изучения этой темы и еще не углублялся в документацию, поэтому, возможно, забегу немного вперед. Мне искренне любопытно понять, не противоречит ли это предложение потенциальному принципу безгражданства REST?
Какое отношение безопасность имеет к REST? Отдых есть отдых, безопасность есть безопасность. У них нет ничего общего.
Вы имеете в виду, что безгражданство в REST связано с ресурсами, а не с безопасностью?
Я говорю, что ОТДЫХ. это одно, а безопасность - это другое. Тот факт, что REST не имеет состояния, не означает, что безопасность тоже.
Создание Authentication
из токена доступа Bearer
происходит на серверах ресурсов OAuth2: Security(Web)FilterChain
настроено с помощью oauth2ResourceServers
, который адаптирован для REST API без сохранения состояния (без сеанса). Это делает микросервис супермасштабируемым и отказоустойчивым. Но обратите внимание, что сервер ресурсов не заботится о том, как был получен токен Bearer
(какой поток использовался). Это означает, что сервер ресурсов OAuth2 никогда не будет отвечать за вход пользователей (это работа клиентов OAuth2).
Имейте в виду, что серверы ресурсов OAuth2 могут запрашиваться только клиентами OAuth2, а одностраничные или мобильные приложения делают небезопасных клиентов (публичными). Лучше использовать OAuth2 BFF.
Короче говоря: используйте код авторизации BFF и сохраняйте токены в сеансе, а затем создайте аутентификацию из JWT на сервере ресурсов Spring без сохранения состояния OAuth2.
Нет необходимости обращаться к базе данных, если срок действия токена не истек и подпись действительна. Обращение к базе данных при каждом запросе повлияет на производительность.
If the token is issued and verified, doesn't it mean the user is already authenticated
Да, попробуйте другой урок. В этом уроке, возможно, основное внимание уделяетсяroles
тому, с чем Spring должен справиться, декодируя JWT. Роли могут передаваться в JWT, или Spring может искать пользователя внутри и использовать то, что там сохранено, как в вашем случае.