Реализация JWT в Spring Boot

В этой статье мы реализуем JWT авторизацию с использованием Spting-Boot приложений.

· 14 минуты на чтение
Реализация JWT в Spring Boot

Теперь, когда мы знаем что такое JWT токен и как он устроен, пришло время использовать наши теоретические знания на практике.

Одно из преимуществ аутентификации с использованием JWT – это возможность выделить выдачу токенов и работу с данными пользователей в отдельное приложение, переложив механизм валидации токенов на клиентские приложения. Этот механизм отлично подходит микросервисной архитекутуре.

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

Сначала мы сделаем приложение, которое будет совмещать в себе бизнес-логику и выдачу токенов. А чуть позже мы сделаем два приложения: одно будет отвечать за выдачу токенов, а другое содержать бизнес логику. Сервис с бизнес логикой не сможет выдавать токены, но сможет их валидировать. Таких приложений может быть много.

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

  • Клиент API, чаще всего это фронт, присылает нам объект с логином и паролем.
  • Если пароль подходит, то мы генерируем access и refresh токены и отправляем их в ответ.
  • Клиент API использует access токен для взаимодействия с остальным нашим эндпойнтами нашего API.
  • Через пять минут, когда access токен протухает, фронт отправляет refresh токен и получает новый access токен. И так по кругу, пока не протухнет refresh токен.
  • Рефреш токен выдается на 30 дней. Примерно на 25-29 день клиент API отправляет валидный refresh токен вместе с валидным access токеном и взамен получает новую пару токенов.

В этой статье будет чистый REST-сервис без фронтенда. Подразумевается, что front написан отдельно: например, на каком-нибудь JavaScript-фреймворке.

Спонсор поста

Используемые версии

Java: 17
SpringBoot: 2.7.0
jsonwebtoken: 0.11.5
jaxb-api: 2.3.1

История изменений

21.06.2022: Актуализировал версию спрингбута и перевел проект на 17 Java. Удалил все устаревшие классы и методы, заменив их на аналогичные современные варианты. Детализировал некоторые моменты в статье.

Создание сервера аутентификации

😺
Проект на GitHub: jwt-server-spring

Если вы хотите повторять все действия из статьи, чтобы лучше понять что происходит. То для вас я собрал начальную конфигурацию приложения на сайте start.spring.io, вам остается только скачать ее, распаковать, и открыть с помощью Idea.

После этого в pom.xml добавим зависимости. Они понадобятся нам для генерации JWT токенов.

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
Актуальные версии в Maven Central: jjwt-api, jjwt-impl, jjwt-jackson, jaxb-api

Сперва создадим наш незамысловатый API. У нас будет 2 роли для пользователей и два эндпойнта:

  • /api/hello/user – будет выводить приветственное сообщение для пользователей с ролью "USER"
  • /api/hello/admin – будет выводить приветственное сообщение для пользователей с ролью "ADMIN".
package dev.struchkov.example.jwt.server.controller; import lombok.RequiredArgsConstructor; import org.sadtech.example.jwt.server.domain.JwtAuthentication; import org.sadtech.example.jwt.server.service.AuthService; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@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;

}
package dev.struchkov.example.jwt.server.domain; import lombok.RequiredArgsConstructor; import org.springframework.security.core.GrantedAuthority;
@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;
import lombok.NonNull; import lombok.RequiredArgsConstructor; import dev.struchkov.example.jwt.server.domain.Role; import dev.struchkov.example.jwt.server.domain.User; import org.springframework.stereotype.Service; import java.util.Collections; import java.util.List; import java.util.Optional;
@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 токен. Он содержит поля: логин пользователя и его пароль.

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. Он будет генерировать и валидировать access и refresh токены.

package dev.struchkov.example.jwt.server.service;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import dev.struchkov.example.jwt.server.domain.User; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.security.Key; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date;
@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==

Как получить ключи?

Самым надежным способом будет воспользоваться методом Keys.secretKeyFor(), который генерирует надежные ключи, но он возвращает объект SecretKey. Нам же нужно как-то получить текстовую строку, чтобы использовать ее в application.properties.

Для этого можно получить массив байт ключа, используя метод  SecretKey.getEncoded(), и преобразовать их в Base64. Этот механизм я описал в классе GenerateKeys. Можно просто запустить этот класс и получить два ключа.

Обратите внимание на конструктор JwtProvider, там происходит обратный процесс. Мы преобразуем Base64 обратно в массив байт, после чего используем Keys.hmacShaKeyFor(), чтобы восстановить из этих байтов объект ключа SecretKey.

Метод generateAccessToken принимает объект пользователя и генерирует access токен для него. Строки 17-19 отвечают за определение времени жизни токена. В данном случае это пять минут. Библиотека, которая генерирует токены, не работает с LocalDateTime, поэтому приходится конвертировать все в старый формат Date.

Строки 20-26 отвечают за непосредственное создание access токена. В токен мы указываем логин пользователя, дату до которой токен валиден, алгоритм шифрования и наши произвольные claims: роли и имя пользователя. Для примера мы добавили в токен информацию об имени пользователя, чтобы на фронте не запрашивать бэкенд каждый раз, когда его нужно вывести.

Метод generateRefreshToken делает все тоже самое, только мы не передаем туда claims и указываем большее время жизни.

Методы validateAccessToken и validateRefreshToken отвечают за проверку валидности токена. Если токен протух, или подписан неправильно, то в лог запишется соотсветсвующее сообщение, а метод вернет false.

Вы могли заметить, что мы не валидируем пароль пользователя. Для проверки пароля мы создадим отдельный сервис AuthService, который также будет отвечать за получение новых access и refresh токенов взамен протухающим.

package dev.struchkov.example.jwt.server.service;
import io.jsonwebtoken.Claims; import lombok.NonNull; import lombok.RequiredArgsConstructor; import dev.struchkov.example.jwt.server.domain.JwtAuthentication; import dev.struchkov.example.jwt.server.domain.JwtRequest; import dev.struchkov.example.jwt.server.domain.JwtResponse; import dev.struchkov.example.jwt.server.domain.User; import dev.struchkov.example.jwt.server.exception.AuthException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map;
@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 и получаем от него токены. Далее сохраняем выданный рефреш токен в мапу refreshStorage и возвращаем объект JwtResponse с токенами. Зачем сохранять рефреш токены, я объясню позже.

Для хранения рефреш токена используется HashMap лишь для упрощения примера. Лучше использовать какое-нибудь постоянное хранилище, например Redis.

Переходим к методу getAccessToken, который принимает refresh токен, а возвращает новый access токен. Сначала мы проверяем, что присланный rehresh токен валиден. Если валиден, то получаем claims и оттуда получаем логин пользователя. Далее по логину находим выданный пользователю refresh токен в мапе refreshStorage, и сверяем его с присланным пользователем. Если токены одинаковые, то получаем объект User, который отправляем в JwtProvider и получаем новый access токен, без обновления refresh токена.

Зачем сохранять токены?

Сохранять их не обязательно, но это дает некоторые преимущества. Если каким-то образом злоумышленник заполучит секретный ключ для генерации refresh токенов, он не сможет создавать токены. Потому что ему нужно будет знать время создания токена конкретным пользователем.

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

Но учтите, если у вас есть сайт и мобильное приложение, то вам нужно будет сохранять два refresh токена для одного пользователя. По одному на каждого клиента API.

Еще один плюс, вы можете забанить пользователя в системе и отозвать его refresh токен, реализовав удаление токена из хранилища сохраненных и запрет на выдачу новых забаненым пользователям. В другом случае пользователь смог бы выпускать себе новые access токены, пока не протух бы refresh токен.

Теперь создадим контроллер AuthController.

package dev.struchkov.example.jwt.server.controller;
import dev.struchkov.example.jwt.server.domain.JwtResponse; import dev.struchkov.example.jwt.server.domain.RefreshJwtRequest; import dev.struchkov.example.jwt.server.service.AuthService; import lombok.RequiredArgsConstructor; import dev.struchkov.example.jwt.server.domain.JwtRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@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); } }

Тут ничего не обычного.

  • Эндпойнт /api/auth/login принимает JwtRequest, а возвращает JwtResponse с токенами.
  • Эндпойнт /api/auth/token  принимает RefreshJwtRequest c единственным полем refreshToken и возвращает JwtResponse с новым access токеном.
  • И наконец эндпойнт /api/auth/refresh  принимает RefreshJwtRequest  и возвращает JwtResponse с новыми токенами.
@Getter
@Setter
public class RefreshJwtRequest {

    public String refreshToken;

}

Также для большой безопасности мы защитим /api/auth/refresh и будем принимать на него запросы только с валидным access токеном. Для этого нам нужно создать класс конфигурации для настройки Spting 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();
    }

}

Эндпойнт, который выдает токены по логину и паролю, а также тот, что выдает новый access токен по refresh токену, мы оставляем без защиты. Остальные эндпойнты будут доступны только аутентифицированным пользователям (строки 31-32).

Также мы добавляем наш фильтр JwtFilter, который и будет осуществлять аутентификацию пользователей (строка 34).

Рассмотрим реализацию JwtFilter.

package dev.struchkov.example.jwt.server.filter;
import dev.struchkov.example.jwt.server.service.JwtProvider; import dev.struchkov.example.jwt.server.service.JwtUtils; import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import dev.struchkov.example.jwt.server.domain.JwtAuthentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.GenericFilterBean; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException;
@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; } }

В 13 строке мы достаем из HTTP запроса значение заголовка Authorization. Далее проверяем, что значение этого заголовка не null и токен валидный (строка 14). После этого достаем claims и генерируем JwtAuthentication. Далее сгенерированный объект аутентификации мы помещаем в SecurityContext.

package dev.struchkov.example.jwt.server.domain;
import lombok.Getter; import lombok.Setter; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import java.util.Collection; import java.util.Set;
@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; } }

С сервером аутентификации все, он полностью готов.

Проверка работы

Вернемся к нашему проверочному контроллеру. Проверять работу сервера аутентификации будем с использованием Postman.

⛷️
Postman коллекция лежит в папке проекта - resources.

Первым делом получаем по логину и паролю получаем access и refresh токены.

После этого берем access токен и вcтавляем его на вкладке Authorization, выбрав тип "Bearer token".

Далее выполняем запрос и получаем заветный результат.

Также попробуем получить сообщение предназначенное только для админов.

Видим 403 ошибку доступа, все работает. Если подождать 5 минут, то и по эндпойнту /api/hello/user увидим ту же ошибку, так как access токен протух.

Чтобы получить новый access токен отправляем запрос на /api/auth/token. В теле запроса указываем наш refresh токен.

Все работает. А теперь взамен текущего refresh токена получим новые access и refresh токены. Для этого вызовем /api/auth/refresh и передадим наш текущий refresh токен в теле запроса. Также не стоит забывать, что это защищенный метод, поэтому во вкладке Authorization вставляем наш access токен.

Отлично, мы получили новую пару токенов. А что если попробовать снова вызвать этот же запрос?

Мы получим ошибку, так как этого токена больше нет в сохраненных.

На этом основная часть закончена. Мы реализовали JWT аутентификацию. Теперь вы запросто реализуете приложение клиент, если это необходимо. Нужно только оставить функционал проверки токенов и аутентификации, и убрать функционал по выдаче новых токенов. Для удобства я сделал одно небольшое приложение клиент для демонстрации.

Склонируйте его себе и запустите. Токены выдает наш сервер аутентификации, а приложение клиент может их использовать для доступа к своему API.

Резюмирую

В этой статье мы реализовали аутентифицкаицю в SpringBoot сервисе с использованием JWT.

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

Struchkov Mark
Struchkov Mark
Задавайте вопросы, если что-то осталось не понятным👇