Теперь, когда мы разобрались с тем, что такое JWT-токен и как он работает, пришло время применить теоретические знания на практике.
Одним из преимуществ аутентификации с использованием JWT является возможность выделить процессы генерации токенов и обработки данных пользователей в отдельное приложение, переложив проверку валидности токенов на клиентские приложения. Такой подход идеально подходит для микросервисной архитектуры.
Для начала мы создадим приложение, которое будет совмещать бизнес-логику и функцию выдачи токенов. Позже мы разделим эти функции на два отдельных приложения: одно будет отвечать за генерацию токенов, а другое — за бизнес-логику. Приложение с бизнес-логикой не сможет выдавать токены, но будет иметь возможность их валидировать. В будущем подобных приложений может быть несколько.
Рассмотрим процесс аутентификации пошагово. В большинстве обучающих материалов часто не упоминается наличие refresh-токена, хотя он является важной частью системы аутентификации на основе JWT. Поэтому мы также включим его в рассмотрение. Вот как будет выглядеть процесс аутентификации:
- Запрос с логином и паролем. Клиент (чаще всего это фронтенд) отправляет запрос с объектом, содержащим логин и пароль.
- Генерация токенов. Если введённый пароль корректен, сервер генерирует access- и refresh-токены и возвращает их клиенту.
- Использование access-токена. Клиент использует access-токен для взаимодействия с API.
- Обновление access-токена. Через пять минут, когда срок действия access-токена истекает, клиент отправляет refresh-токен и получает новый access-токен. Этот процесс повторяется до тех пор, пока не истечёт срок действия refresh-токена.
- Продление refresh-токена. Refresh-токен выдаётся на 30 дней. Примерно на 25-29 день клиент отправляет запрос с действительными access- и refresh-токенами и получает новую пару токенов.
В данной статье будет представлен REST-сервис без фронтенда, подразумевается, что фронтенд будет разработан отдельно.
Используемые версии
Java: 17
SpringBoot: 2.7.0
jsonwebtoken: 0.11.5
jaxb-api: 2.3.1
История изменений
21.06.2022: Актуализировал версию спрингбута и перевел проект на 17 Java. Удалил все устаревшие классы и методы, заменив их на аналогичные современные варианты. Детализировал некоторые моменты в статье.
Создание сервера аутентификации
Добавим в pom.xml
необходимые зависимости для генерации JWT-токенов.
Создание API и ролей
Сначала создадим простой API с двумя ролями пользователей и двумя эндпойнтами.
/api/hello/user
– будет выводить приветственное сообщение для пользователей с ролью "USER"/api/hello/admin
– будет выводить приветственное сообщение для пользователей с ролью "ADMIN".
@RestController
@RequestMapping("api")
@RequiredArgsConstructor
public class Controller {
private final AuthService authService;
@PreAuthorize("hasAuthority('USER')")
@GetMapping("hello/user")
public ResponseEntity<String> helloUser() {
final JwtAuthentication authInfo = authService.getAuthInfo();
return ResponseEntity.ok("Hello user " + authInfo.getPrincipal() + "!");
}
@PreAuthorize("hasAuthority('ADMIN')")
@GetMapping("hello/admin")
public ResponseEntity<String> helloAdmin() {
final JwtAuthentication authInfo = authService.getAuthInfo();
return ResponseEntity.ok("Hello admin " + authInfo.getPrincipal() + "!");
}
}
Теперь создадим класс пользователя системы User
и enum отвечающий за роль пользователя:
package dev.struchkov.example.jwt.server.domain;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String login;
private String password;
private String firstName;
private String lastName;
private Set<Role> roles;
}
@RequiredArgsConstructor
public enum Role implements GrantedAuthority {
ADMIN("ADMIN"),
USER("USER");
private final String vale;
@Override
public String getAuthority() {
return vale;
}
}
Далее создадим UserService
с единственным методом, возвращающим пользователя по логину. Пользователи будут заранее созданы в конструкторе. Для упрощения мы не будем рассматривать добавление новых пользователей и шифрование паролей, так как основная цель — реализация работы с JWT.
package dev.struchkov.example.jwt.server.service;
@Service
@RequiredArgsConstructor
public class UserService {
private final List<User> users;
public UserService() {
this.users = List.of(
new User("anton", "1234", "Антон", "Иванов", Collections.singleton(Role.USER)),
new User("ivan", "12345", "Сергей", "Петров", Collections.singleton(Role.ADMIN))
);
}
public Optional<User> getByLogin(@NonNull String login) {
return users.stream()
.filter(user -> login.equals(user.getLogin()))
.findFirst();
}
}
Объекты для передачи данных
Создадим класс JwtRequest
. Этот класс представляет запрос, который пользователь отправляет для получения JWT-токена. Он содержит поля login
и password
.
package dev.struchkov.example.jwt.server.domain;
@Setter
@Getter
public class JwtRequest {
private String login;
private String password;
}
Создадим еще один объект JwtResponse
. Этот класс представляет ответ, возвращаемый пользователю, и содержит access
- и refresh
-токены:
package dev.struchkov.example.jwt.server.domain;
@Getter
@AllArgsConstructor
public class JwtResponse {
private final String type = "Bearer";
private String accessToken;
private String refreshToken;
}
Компонент JwtProvider
Создадим компонент JwtProvider
, отвечающий за генерацию и валидацию access
- и refresh
-токенов.
package dev.struchkov.example.jwt.server.service;
@Slf4j
@Component
public class JwtProvider {
private final SecretKey jwtAccessSecret;
private final SecretKey jwtRefreshSecret;
public JwtProvider(
@Value("${jwt.secret.access}") String jwtAccessSecret,
@Value("${jwt.secret.refresh}") String jwtRefreshSecret
) {
this.jwtAccessSecret = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtAccessSecret));
this.jwtRefreshSecret = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtRefreshSecret));
}
public String generateAccessToken(@NonNull User user) {
final LocalDateTime now = LocalDateTime.now();
final Instant accessExpirationInstant = now.plusMinutes(5).atZone(ZoneId.systemDefault()).toInstant();
final Date accessExpiration = Date.from(accessExpirationInstant);
return Jwts.builder()
.setSubject(user.getLogin())
.setExpiration(accessExpiration)
.signWith(jwtAccessSecret)
.claim("roles", user.getRoles())
.claim("firstName", user.getFirstName())
.compact();
}
public String generateRefreshToken(@NonNull User user) {
final LocalDateTime now = LocalDateTime.now();
final Instant refreshExpirationInstant = now.plusDays(30).atZone(ZoneId.systemDefault()).toInstant();
final Date refreshExpiration = Date.from(refreshExpirationInstant);
return Jwts.builder()
.setSubject(user.getLogin())
.setExpiration(refreshExpiration)
.signWith(jwtRefreshSecret)
.compact();
}
public boolean validateAccessToken(@NonNull String accessToken) {
return validateToken(accessToken, jwtAccessSecret);
}
public boolean validateRefreshToken(@NonNull String refreshToken) {
return validateToken(refreshToken, jwtRefreshSecret);
}
private boolean validateToken(@NonNull String token, @NonNull Key secret) {
try {
Jwts.parserBuilder()
.setSigningKey(secret)
.build()
.parseClaimsJws(token);
return true;
} catch (ExpiredJwtException expEx) {
log.error("Token expired", expEx);
} catch (UnsupportedJwtException unsEx) {
log.error("Unsupported jwt", unsEx);
} catch (MalformedJwtException mjEx) {
log.error("Malformed jwt", mjEx);
} catch (SignatureException sEx) {
log.error("Invalid signature", sEx);
} catch (Exception e) {
log.error("invalid token", e);
}
return false;
}
public Claims getAccessClaims(@NonNull String token) {
return getClaims(token, jwtAccessSecret);
}
public Claims getRefreshClaims(@NonNull String token) {
return getClaims(token, jwtRefreshSecret);
}
private Claims getClaims(@NonNull String token, @NonNull Key secret) {
return Jwts.parserBuilder()
.setSigningKey(secret)
.build()
.parseClaimsJws(token)
.getBody();
}
}
В конструктор мы передаем секретные ключи, для подписи и валидации токенов.
Один ключ используется для генерации access-токенов, а второй — для refresh-токенов. Это позволяет отделить сервисы с бизнес-логикой, которые смогут валидировать access-токены, но не будут иметь доступа к refresh-токенам. Если такой сервис будет скомпрометирован, можно будет просто заменить ключ access-токена, не затрагивая refresh-токены и не разлогинивая всех пользователей.
Используя аннотацию @Value
, Spring автоматически подставляет значения из файла application.properties
. Укажите ключи в формате Base64 в этом файле:
jwt.secret.access=qBTmv4oXFFR2GwjexDJ4t6fsIUIUhhXqlktXjXdkcyygs8nPVEwMfo29VDRRepYDVV5IkIxBMzr7OEHXEHd37w==
jwt.secret.refresh=zL1HB3Pch05Avfynovxrf/kpF9O2m4NCWKJUjEp27s9J2jEG3ifiKCGylaZ8fDeoONSTJP/wAzKawB8F9rOMNg==
Как получить ключи?
Cамым надежным способом является использование метода Keys.secretKeyFor()
, который генерирует безопасные ключи. Однако метод возвращает объект SecretKey
, который нужно преобразовать в текстовую строку для записи в application.properties
. Это можно сделать, вызвав SecretKey.getEncoded()
и преобразовав массив байт в Base64. Класс GenerateKeys
выполнит эту операцию, и, запустив его, вы получите два ключа.
В конструкторе JwtProvider
происходит обратная операция: Base64 преобразуется обратно в массив байт, после чего с помощью Keys.hmacShaKeyFor()
создается объект SecretKey
.
Генерация и валидация токенов
Метод generateAccessToken
принимает объект пользователя и генерирует для него access
-токен. Строки 17–19 задают время жизни токена (в данном случае — 5 минут). Библиотека для генерации токенов работает с Date
, поэтому время нужно конвертировать в этот формат из LocalDateTime
.
Строки 20–26 отвечают за создание access-токена. В него включаются логин пользователя, срок действия токена, алгоритм шифрования и произвольные claims: роль и имя пользователя. Например, имя пользователя можно добавить в токен, чтобы избежать дополнительных запросов к бэкенду для его получения.
Метод generateRefreshToken
работает аналогично, но без claims и с более длительным сроком действия.
Методы validateAccessToken
и validateRefreshToken
проверяют валидность токенов. В случае истечения срока действия или некорректной подписи в лог будет записано сообщение, а метод вернет false
.
Валидация пароля и создание AuthService
Обратите внимание, что в текущей конфигурации пароль пользователя не проходит проверку. Для выполнения этой задачи, а также для обновления access- и refresh-токенов, создадим сервис AuthService
.
package dev.struchkov.example.jwt.server.service;
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserService userService;
private final Map<String, String> refreshStorage = new HashMap<>();
private final JwtProvider jwtProvider;
public JwtResponse login(@NonNull JwtRequest authRequest) {
final User user = userService.getByLogin(authRequest.getLogin())
.orElseThrow(() -> new AuthException("Пользователь не найден"));
if (user.getPassword().equals(authRequest.getPassword())) {
final String accessToken = jwtProvider.generateAccessToken(user);
final String refreshToken = jwtProvider.generateRefreshToken(user);
refreshStorage.put(user.getLogin(), refreshToken);
return new JwtResponse(accessToken, refreshToken);
} else {
throw new AuthException("Неправильный пароль");
}
}
public JwtResponse getAccessToken(@NonNull String refreshToken) {
if (jwtProvider.validateRefreshToken(refreshToken)) {
final Claims claims = jwtProvider.getRefreshClaims(refreshToken);
final String login = claims.getSubject();
final String saveRefreshToken = refreshStorage.get(login);
if (saveRefreshToken != null && saveRefreshToken.equals(refreshToken)) {
final User user = userService.getByLogin(login)
.orElseThrow(() -> new AuthException("Пользователь не найден"));
final String accessToken = jwtProvider.generateAccessToken(user);
return new JwtResponse(accessToken, null);
}
}
return new JwtResponse(null, null);
}
public JwtResponse refresh(@NonNull String refreshToken) {
if (jwtProvider.validateRefreshToken(refreshToken)) {
final Claims claims = jwtProvider.getRefreshClaims(refreshToken);
final String login = claims.getSubject();
final String saveRefreshToken = refreshStorage.get(login);
if (saveRefreshToken != null && saveRefreshToken.equals(refreshToken)) {
final User user = userService.getByLogin(login)
.orElseThrow(() -> new AuthException("Пользователь не найден"));
final String accessToken = jwtProvider.generateAccessToken(user);
final String newRefreshToken = jwtProvider.generateRefreshToken(user);
refreshStorage.put(user.getLogin(), newRefreshToken);
return new JwtResponse(accessToken, newRefreshToken);
}
}
throw new AuthException("Невалидный JWT токен");
}
public JwtAuthentication getAuthInfo() {
return (JwtAuthentication) SecurityContextHolder.getContext().getAuthentication();
}
}
Метод login
сначала ищет пользователя по логину. Если пользователь найден и присланный пароль совпадает с сохранённым паролем, объект пользователя передается в JwtProvider
для генерации токенов. Полученный refresh
-токен сохраняется в хранилище refreshStorage
, после чего возвращается объект JwtResponse
с токенами.
Метод getAccessToken
принимает refresh-токен и возвращает новый access
-токен. Он выполняет следующие шаги:
- Проверяет, что присланный refresh-токен является валидным.
- Получает из claims логин пользователя.
- Находит сохранённый refresh-токен в
refreshStorage
и сверяет его с токеном, присланным пользователем. - Если токены совпадают, находит объект
User
, передаёт его вJwtProvider
и генерирует новый access-токен. Refresh-токен при этом не обновляется.
Хотя сохранение refresh-токенов не является обязательным, оно повышает безопасность системы:
- В случае компрометации секретного ключа для генерации
refresh
-токенов злоумышленник не сможет создать валидный токен, так как ему нужно будет знать точное время его создания. Без хранилища refresh-токенов злоумышленник мог бы сгенерировать токен для любого пользователя и получить по нему access-токены. - Если у пользователя есть несколько клиентов API (например, сайт и мобильное приложение), для каждого нужно будет хранить отдельный refresh-токен.
- Хранение токенов позволяет отзывать доступ у забаненных пользователей. Если пользователь заблокирован, можно удалить его refresh-токен из хранилища, что заблокирует возможность генерировать новые access-токены до истечения срока действия refresh-токена.
Создание контроллера AuthController
Теперь создадим контроллер AuthController
.
package dev.struchkov.example.jwt.server.controller;
@RestController
@RequestMapping("api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("login")
public ResponseEntity<JwtResponse> login(@RequestBody JwtRequest authRequest) {
final JwtResponse token = authService.login(authRequest);
return ResponseEntity.ok(token);
}
@PostMapping("token")
public ResponseEntity<JwtResponse> getNewAccessToken(@RequestBody RefreshJwtRequest request) {
final JwtResponse token = authService.getAccessToken(request.getRefreshToken());
return ResponseEntity.ok(token);
}
@PostMapping("refresh")
public ResponseEntity<JwtResponse> getNewRefreshToken(@RequestBody RefreshJwtRequest request) {
final JwtResponse token = authService.refresh(request.getRefreshToken());
return ResponseEntity.ok(token);
}
}
Контроллер AuthController
обрабатывает три основных эндпойнта:
POST /api/auth/login
: принимаетJwtRequest
и возвращаетJwtResponse
с токенами access и refresh.POST /api/auth/token
: принимаетRefreshJwtRequest
с полемrefreshToken
и возвращаетJwtResponse
с новым access-токеном.POST /api/auth/refresh
: принимаетRefreshJwtRequest
и возвращаетJwtResponse
с новыми access и refresh токенами.
@Getter
@Setter
public class RefreshJwtRequest {
public String refreshToken;
}
Для повышения безопасности эндпойнт /api/auth/refresh
принимает запросы только с валидным access-токеном. Для настройки защиты создадим конфигурационный класс Spring Security.
package dev.struchkov.example.jwt.server.config;
import dev.struchkov.example.jwt.server.filter.JwtFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final JwtFilter jwtFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests(
authz -> authz
.antMatchers("/api/auth/login", "/api/auth/token").permitAll()
.anyRequest().authenticated()
.and()
.addFilterAfter(jwtFilter, UsernamePasswordAuthenticationFilter.class)
).build();
}
}
Эндпойнты для получения токенов (/api/auth/login
и /api/auth/token
) останутся общедоступными. Остальные эндпойнты будут доступны только аутентифицированным пользователям (см. строки 31–32).
Кроме того, подключим фильтр JwtFilter
, который будет проверять и аутентифицировать пользователей по токенам (строка 34). Ниже приведена реализация JwtFilter
.
package dev.struchkov.example.jwt.server.filter;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtFilter extends GenericFilterBean {
private static final String AUTHORIZATION = "Authorization";
private final JwtProvider jwtProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain fc)
throws IOException, ServletException {
final String token = getTokenFromRequest((HttpServletRequest) request);
if (token != null && jwtProvider.validateAccessToken(token)) {
final Claims claims = jwtProvider.getAccessClaims(token);
final JwtAuthentication jwtInfoToken = JwtUtils.generate(claims);
jwtInfoToken.setAuthenticated(true);
SecurityContextHolder.getContext().setAuthentication(jwtInfoToken);
}
fc.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
final String bearer = request.getHeader(AUTHORIZATION);
if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}
В JwtFilter
происходит следующее:
- Извлекается значение заголовка
Authorization
из HTTP-запроса. - Проверяется, что токен не равен
null
и что он является валидным. - Если токен прошел проверку, извлекаются его
claims
, и на их основе создается объект аутентификацииJwtAuthentication
с помощью методаJwtUtils.generate
. - Сформированный объект
JwtAuthentication
помещается вSecurityContext
для дальнейшего использования в приложении.
Метод JwtUtils.generate
используется для создания объекта JwtAuthentication
из токена. Этот объект содержит информацию об аутентификации пользователя, включая его роли и имя.
package dev.struchkov.example.jwt.server.service;
import io.jsonwebtoken.Claims;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import dev.struchkov.example.jwt.server.domain.JwtAuthentication;
import dev.struchkov.example.jwt.server.domain.Role;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class JwtUtils {
public static JwtAuthentication generate(Claims claims) {
final JwtAuthentication jwtInfoToken = new JwtAuthentication();
jwtInfoToken.setRoles(getRoles(claims));
jwtInfoToken.setFirstName(claims.get("firstName", String.class));
jwtInfoToken.setUsername(claims.getSubject());
return jwtInfoToken;
}
private static Set<Role> getRoles(Claims claims) {
final List<String> roles = claims.get("roles", List.class);
return roles.stream()
.map(Role::valueOf)
.collect(Collectors.toSet());
}
}
Метод generate
выполняет следующие действия:
- Создает новый объект
JwtAuthentication
. - Заполняет его ролями пользователя с помощью метода
getRoles
, который извлекает список ролей изclaims
, преобразует их в перечисленияRole
и сохраняет в объектеJwtAuthentication
. - Извлекает из
claims
имя пользователя (firstName
) и добавляет его вJwtAuthentication
. - Устанавливает
username
, используяclaims.getSubject()
.
Метод getRoles
извлекает роли из claims
и преобразует их в множество объектов Role
.
Класс JwtAuthentication
и его назначение
Класс JwtAuthentication
— это специальный объект, реализующий интерфейс Authentication
из Spring Security. Он используется для хранения информации о текущей аутентификации пользователя и передачи этих данных в контекст безопасности приложения SecurityContext
.
Реализуя интерфейс Authentication
, JwtAuthentication
становится совместимым с SecurityContext
, что позволяет Spring Security автоматически проверять, аутентифицирован ли пользователь и имеет ли он соответствующие права для доступа к защищенным ресурсам.
package dev.struchkov.example.jwt.server.domain;
@Getter
@Setter
public class JwtAuthentication implements Authentication {
private boolean authenticated;
private String username;
private String firstName;
private Set<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { return roles; }
@Override
public Object getCredentials() { return null; }
@Override
public Object getDetails() { return null; }
@Override
public Object getPrincipal() { return username; }
@Override
public boolean isAuthenticated() { return authenticated; }
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
this.authenticated = isAuthenticated;
}
@Override
public String getName() { return firstName; }
}
- Свойства
JwtAuthentication
:authenticated
: флаг, указывающий, аутентифицирован ли пользователь.username
: имя пользователя (обычно логин), которое служит уникальным идентификатором.firstName
: имя пользователя, которое может использоваться в интерфейсе для персонализации.roles
: набор ролей пользователя (в данном примере —Set<Role>
), который определяет права доступа пользователя в системе.
- Методы интерфейса
Authentication
:getAuthorities()
: возвращает коллекцию прав доступа (GrantedAuthority
), которые соответствуют ролям пользователя. Эти права можно использовать для ограничения доступа к защищенным ресурсам.getPrincipal()
: возвращает идентификатор пользователя, в данном случае —username
.isAuthenticated()
иsetAuthenticated(boolean)
: проверяют и задают статус аутентификации пользователя. МетодsetAuthenticated
применяется при установке объекта аутентификации в контексте безопасности.getName()
: возвращает отображаемое имя пользователя, которое в данном случае соответствуетfirstName
.
- Неиспользуемые методы:
getCredentials()
иgetDetails()
возвращаютnull
, так как они не нужны для работы с JWT. В JWT-аутентификации нет пароля на каждом запросе, а используется токен, который уже содержит всю необходимую информацию.
С сервером аутентификации все, он полностью готов.
Проверка работы аутентификационного сервера
Для проверки работы сервера аутентификации будем использовать Postman.
resources
.Выполните запрос на получение токенов, отправив логин и пароль. В ответе вы получите access- и refresh-токены.
Скопируйте access-токен из полученного ответа и вставьте его во вкладке Authorization
в Postman, выбрав тип "Bearer token".
Теперь выполните запрос к защищенному эндпойнту. Если токен действителен и у пользователя есть соответствующие права, сервер вернет запрашиваемые данные.
Для проверки прав доступа отправьте запрос к эндпойнту, предназначенному только для администраторов. При отсутствии прав доступа сервер вернет ошибку 403.
Через 5 минут access-токен истечет. Если отправить запрос к обычному защищенному эндпойнту после истечения срока, сервер также вернет ошибку 403.
Чтобы получить новый access-токен, отправьте запрос на /api/auth/token
, указав в теле запроса refresh-токен. Сервер вернет новый access-токен.
Отправьте запрос на /api/auth/refresh
, указав текущий refresh-токен в теле запроса. Так как этот эндпойнт защищен, не забудьте вставить access-токен во вкладке Authorization
. Сервер вернет новую пару токенов.
Если отправить повторный запрос на /api/auth/refresh
с уже использованным refresh-токеном, сервер вернет ошибку, так как этот токен больше не числится среди действующих.
На этом настройка и проверка завершены. Мы реализовали JWT-аутентификацию, где токены выдаются и обновляются аутентификационным сервером.
Теперь, если потребуется, вы сможете легко реализовать клиентское приложение, которое будет проверять и использовать токены, но не выдавать их. Я подготовил небольшое клиентское приложение для демонстрации. Склонируйте его, запустите и подключите к серверу аутентификации. Сервер аутентификации будет выдавать токены, а клиентское приложение — использовать их для доступа к API.
Резюмирую
В этой статье мы реализовали аутентификацию в Spring Boot-сервисе с использованием JWT. Вы узнали, как настраивать сервер для выдачи токенов, а также как валидировать токены в других сервисах. Такой подход, позволяющий разграничить функции выдачи и валидации токенов, полезен в микросервисной архитектуре, так как уменьшает количество запросов к серверу аутентификации.