Spring. Кастомная аутентификация с применением JWT

от автора

В данной статье я хотел бы поделиться, на мой взгляд, удачным опытом написания своего велосипеда для аутентификации пользователя в REST API с использованием JWT.
Это не замена Spring Security, но этот способ хорошо себя показывает в продакшене на протяжении вот уже более двух лет.

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


Содержание

  • Предыстория
  • Генерацию ключа
  • Создание Spring проекта
  • TokenHandler
  • Аннотация и обработчик
  • Обработка AuthenticationException
  • Контроллер

0. Предыстория

Для начала, хочу рассказать что именно меня побудило реализовать данный способ аутентификации клиента и почему не использовал Spring Security. Кому не интересно, может сразу перейти к следующей главе.

К тому моменту я работал в небольшой фирме, которая занимается разработкой сайтов. Это было мое первое рабочее место в данной сфере, поэтому толком ничего и не знал. Где-то через месяц работы сказали, что будет новый проект и что нужно подготовить базовый функционал для него. Решил посмотреть подробнее как этот процесс был реализован в уже существующих проектах. К моему сожалению, там было все не так уж и радостно.

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

@RequestMapping(value = "/endpoint", method = RequestMethod.GET)  public Response endpoint() {      User user = getUser(); // Метод базового класса      if (null == user)          return new ErrorResponse.Builder(Error.AUTHENTICATION_ERROR).build();       // Логика контроллера  } 

И так было везде… Добавление нового эндпоинта начиналось с того, что копировался этот кусок кода. Мне показалось это немного странным и совершенно неудобным в использовании.

Для решения этой проблемы я отправился в гугл. Возможно я как-то не так искал, но подходящего решения я не нашел. Везде были инструкции по настройке Spring Security.

Объясню почему я не хотел использовать Spring Security. Он мне показался слишком сложным и его как-то не очень удобно использовать в REST. Да и в методах обработки endpoint’а все равно, наверное, придется доставать юзера из контекста. Возможно я не прав, так как не сильно в нем разбирался, но статья в любом случае не об этом.

Мне нужно было что-то простое и удобное в использовании. Пришла идея сделать это через аннотацию.

Идея заключается в том, что в каждый метод контроллера, где нужна авторизация, мы инжектим нашего юзера. И все. Получается, что внутри метода контроллера уже будет авторизованный юзер и он будет != null (за исключением случаев, когда авторизация не обязательная).

С причинами создания данного велосипеда разобрались. Теперь перейдем к практике.

1. Генерацию ключа

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

Для работы на java с jwt есть очень удобная библиотека.

На гитхабе есть все инструкции как работать с jwt, но что бы упростить процесс, приведу пример ниже.

Для генерации ключа создадим обычный maven проект и добавим следующие зависимости

dependencies

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

И класс, который будет генерировать secret

SecretGenerator.java

package jwt;  import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.security.Keys;  import javax.crypto.SecretKey;  public class SecretGenerator {      public static void main(String[] args) {         SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);         String secretString = Encoders.BASE64.encode(secretKey.getEncoded());         System.out.println(secretString);     } } 

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

2. Создание Spring проекта

Процесс создания описывать не буду, так как на эту тему существует множество статей и туториалов. Да и на официальном сайте Spring’а есть initializer, где в два клика можно создать минимальный проект.

Оставлю только итоговый pom файл

pom.xml

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">     <modelVersion>4.0.0</modelVersion>      <parent>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-starter-parent</artifactId>         <version>2.3.2.RELEASE</version>     </parent>      <groupId>org.website</groupId>     <artifactId>backend</artifactId>     <version>1.0-SNAPSHOT</version>     <packaging>jar</packaging>      <properties>         <java.version>14</java.version>         <start-class>org.website.BackendWebsiteApplication</start-class>     </properties>      <profiles>         <profile>             <id>local</id>             <properties>                 <activatedProperties>local</activatedProperties>             </properties>             <activation>                 <activeByDefault>true</activeByDefault>             </activation>         </profile>     </profiles>      <build>         <plugins>             <plugin>                 <groupId>org.springframework.boot</groupId>                 <artifactId>spring-boot-maven-plugin</artifactId>             </plugin>         </plugins>     </build>      <dependencies>         <!--*******SPRING*******-->         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-web</artifactId>         </dependency>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-logging</artifactId>         </dependency>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-data-jpa</artifactId>         </dependency>          <!--*******JWT*******-->         <dependency>             <groupId>io.jsonwebtoken</groupId>             <artifactId>jjwt-api</artifactId>             <version>0.11.2</version>         </dependency>         <dependency>             <groupId>io.jsonwebtoken</groupId>             <artifactId>jjwt-impl</artifactId>             <version>0.11.2</version>             <scope>runtime</scope>         </dependency>         <dependency>             <groupId>io.jsonwebtoken</groupId>             <artifactId>jjwt-jackson</artifactId>             <version>0.11.2</version>             <scope>runtime</scope>         </dependency>          <!--*******OTHER*******-->         <dependency>             <groupId>org.postgresql</groupId>             <artifactId>postgresql</artifactId>             <version>42.2.14</version>         </dependency>         <dependency>             <groupId>org.liquibase</groupId>             <artifactId>liquibase-core</artifactId>             <version>4.0.0</version>         </dependency>         <dependency>             <groupId>com.google.guava</groupId>             <artifactId>guava</artifactId>             <version>29.0-jre</version>         </dependency>         <dependency>             <groupId>com.google.code.gson</groupId>             <artifactId>gson</artifactId>             <version>2.8.6</version>         </dependency>         <dependency>             <groupId>org.apache.commons</groupId>             <artifactId>commons-lang3</artifactId>             <version>3.11</version>         </dependency>         <dependency>             <groupId>org.projectlombok</groupId>             <artifactId>lombok</artifactId>             <version>1.18.12</version>             <scope>provided</scope>         </dependency>          <!--*******TEST*******-->         <dependency>             <groupId>org.junit.jupiter</groupId>             <artifactId>junit-jupiter</artifactId>             <scope>test</scope>         </dependency>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-test</artifactId>             <scope>test</scope>         </dependency>         <dependency>             <groupId>org.hamcrest</groupId>             <artifactId>hamcrest</artifactId>             <version>2.2</version>             <scope>test</scope>         </dependency>     </dependencies>  </project> 

После создания проекта копируем ранее созданный ключ в application.properties

app.api.jwtEncodedSecretKey=teTN1EmB5XADI5iV4daGVAQhBlTwLMAE+LlXZp1JPI2PoQOpgVksRqe79EGOc5opg+AmxOOmyk8q1RbfSWcOyg== 

3. TokenHandler

Нам понадобится сервис для генерации и расшифровки токенов.

В токене будет минимум информации о юзере(только его id) и время истечения токена. Для этого создадим интерфейсы.

Для передачи времени жизни токена.

Expiration.java

package org.website.jwt;  import java.time.LocalDateTime; import java.util.Optional;  public interface Expiration {      Optional<LocalDateTime> getAuthTokenExpire(); } 

И для передачи ID. Его будет имплементировать сущность юзера

CreateBy.java

package org.website.jwt;  public interface CreateBy {      Long getId(); } 

Так же создадим дефолтную имплементацию для интерфейса Expiration. По умолчанию токен будет жить 24 часа.

DefaultExpiration.java

package org.website.jwt;  import org.springframework.stereotype.Component;  import java.time.LocalDateTime; import java.util.Optional;  @Component public class DefaultExpiration implements Expiration {      @Override     public Optional<LocalDateTime> getAuthTokenExpire() {         return Optional.of(LocalDateTime.now().plusHours(24));     } } 

Добавим пару вспомогательных классов.

GeneratedTokenInfo — для информации о сгенерированном токене.
TokenInfo — для информации о пришедшем к нам токене.

GeneratedTokenInfo.java

package org.website.jwt;  import java.time.LocalDateTime; import java.util.Optional;  public class GeneratedTokenInfo {      private final String token;     private final LocalDateTime expiration;      public GeneratedTokenInfo(String token, LocalDateTime expiration) {         this.token = token;         this.expiration = expiration;     }      public String getToken() {         return token;     }      public LocalDateTime getExpiration() {         return expiration;     }      public Optional<String> getSignature() {         if (null != this.token && this.token.length() >= 3)             return Optional.of(this.token.split("\\.")[2]);          return Optional.empty();     } }  

TokenInfo.java

package org.website.jwt;  import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import lombok.NonNull;  import java.time.LocalDateTime; import java.time.ZoneId;  public class TokenInfo {      private final Jws<Claims> claimsJws;      private final String signature;     private final Claims body;     private final Long userId;     private final LocalDateTime expiration;      private TokenInfo() {         throw new UnsupportedOperationException();     }      private TokenInfo(@NonNull final Jws<Claims> claimsJws,                       @NonNull final String signature,                       @NonNull final Claims body,                       @NonNull final Long userId,                       @NonNull final LocalDateTime expiration) {         this.claimsJws = claimsJws;         this.signature = signature;         this.body = body;         this.userId = userId;         this.expiration = expiration;     }      public static TokenInfo fromClaimsJws(@NonNull final Jws<Claims> claimsJws) {         final Claims body = claimsJws.getBody();         return new TokenInfo(                 claimsJws,                 claimsJws.getSignature(),                 body,                 Long.parseLong(body.getId()),                 body.getExpiration().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());     }      public Jws<Claims> getClaimsJws() {         return claimsJws;     }      public String getSignature() {         return signature;     }      public Claims getBody() {         return body;     }      public Long getUserId() {         return userId;     }      public LocalDateTime getExpiration() {         return expiration;     } } 

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

TokenHandler.java

package org.website.jwt;  import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service;  import javax.annotation.PostConstruct; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.sql.Date; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Base64; import java.util.Optional;  @Service @Slf4j public class TokenHandler {      @Value("${app.api.jwtEncodedSecretKey}")     private String jwtEncodedSecretKey;      private final DefaultExpiration defaultExpiration;      private SecretKey secretKey;      @Autowired     public TokenHandler(final DefaultExpiration defaultExpiration) {         this.defaultExpiration = defaultExpiration;     }      @PostConstruct     private void postConstruct() {         byte[] decode = Base64.getDecoder().decode(jwtEncodedSecretKey);         this.secretKey = new SecretKeySpec(decode, 0, decode.length, "HmacSHA512");     }      public Optional<GeneratedTokenInfo> generateToken(CreateBy createBy, Expiration expire) {         if (null == expire || expire.getAuthTokenExpire().isEmpty())             expire = this.defaultExpiration;          try {             final LocalDateTime expireDateTime = expire.getAuthTokenExpire().get().withNano(0);              String compact = Jwts.builder()                     .setId(String.valueOf(createBy.getId()))                     .setExpiration(Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant()))                     .signWith(this.secretKey)                     .compact();              return Optional.of(new GeneratedTokenInfo(compact, expireDateTime));         } catch (Exception e) {             log.error("Error generate new token. CreateByID: {}; Message: {}", createBy.getId(), e.getMessage());         }         return Optional.empty();     }      public Optional<GeneratedTokenInfo> generateToken(CreateBy createBy) {         return this.generateToken(createBy, this.defaultExpiration);     }      public Optional<TokenInfo> extractTokenInfo(final String token) {         try {             Jws<Claims> claimsJws = Jwts.parserBuilder()                     .setSigningKey(this.secretKey)                     .build()                     .parseClaimsJws(token);             return Optional.ofNullable(claimsJws).map(TokenInfo::fromClaimsJws);         } catch (Exception e) {             log.error("Error extract token info. Message: {}", e.getMessage());         }          return Optional.empty();     }  } 

Заострять внимание не буду, так как с этим все должно быть понятно.

4. Аннотация и обработчик

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

Создаем аннотацию со следующим кодом

AuthUser.java

package org.website.annotation;  import java.lang.annotation.*;  @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface AuthUser {     boolean required() default true; } 

Ранее было сказано, что авторизация может быть необязательной. Как раз для этого и нужен метод required в аннотации. В случае если авторизация для конкретного метода необязательна и если пришедший пользователь действительно не авторизован, то в метод заинжектится null. Но к этому мы будем готовы.

Аннотация создана, но еще нужен handler, который и будет доставать из запроса токен, получать из базы пользователя и пробрасывать его в метод контроллера. Для таких случаев у Spring’а есть интерфейс HandlerMethodArgumentResolver. Его и будем имплементировать.

Создаем класс AuthUserHandlerMethodArgumentResolver, который имплементит указанный выше интерфейс.

AuthUserHandlerMethodArgumentResolver.java

package org.website.annotation.handler;  import org.springframework.core.MethodParameter; import org.springframework.lang.NonNull; import org.springframework.web.bind.support.WebArgumentResolver; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.util.WebUtils; import org.website.annotation.AuthUser; import org.website.annotation.exception.AuthenticationException; import org.website.domain.User; import org.website.domain.UserJwtSignature; import org.website.jwt.TokenHandler; import org.website.service.repository.UserJwtSignatureService;  import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import java.util.Objects; import java.util.Optional;  public class AuthUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {      private final String AUTH_COOKIE_NAME;     private final String AUTH_HEADER_NAME;      private final TokenHandler tokenHandler;      private final UserJwtSignatureService userJwtSignatureService;      public AuthUserHandlerMethodArgumentResolver(final String authTokenCookieName,                                                  final String authTokenHeaderName,                                                   final TokenHandler tokenHandler,                                                   final UserJwtSignatureService userJwtSignatureService) {         this.AUTH_COOKIE_NAME = authTokenCookieName;         this.AUTH_HEADER_NAME = authTokenHeaderName;          this.tokenHandler = tokenHandler;          this.userJwtSignatureService = userJwtSignatureService;     }      @Override     public boolean supportsParameter(@NonNull final MethodParameter methodParameter) {         return methodParameter.getParameterAnnotation(AuthUser.class) != null && methodParameter.getParameterType().equals(User.class);     }      @Override     public Object resolveArgument(@NonNull final MethodParameter methodParameter,                                   final ModelAndViewContainer modelAndViewContainer,                                   @NonNull final NativeWebRequest nativeWebRequest,                                   final WebDataBinderFactory webDataBinderFactory) throws Exception {         if (!this.supportsParameter(methodParameter))             return WebArgumentResolver.UNRESOLVED;          // из аннотации достаем значение поля required         final boolean required = Objects.requireNonNull(methodParameter.getParameterAnnotation(AuthUser.class)).required();          // получаем HttpServletRequest из пришедшего запроса         Optional<HttpServletRequest> httpServletRequestOptional = Optional.ofNullable(nativeWebRequest.getNativeRequest(HttpServletRequest.class));          // пробуем достать токен из куки или из заголовка запроса         Optional<UserJwtSignature> userJwtSignature =                 this.extractAuthTokenFromRequest(nativeWebRequest, httpServletRequestOptional.orElse(null))                         .flatMap(tokenHandler::extractTokenInfo)                         .flatMap(userJwtSignatureService::extractByTokenInfo);                  if (required) {             // если пользователь должен быть обязательно авторизован проверяем авторизацию             if (userJwtSignature.isEmpty() || null == userJwtSignature.get().getUser())                 // в случае если не авторизован выбрасываем исключение                 throw new AuthenticationException(httpServletRequestOptional.map(HttpServletRequest::getMethod).orElse(null),                         httpServletRequestOptional.map(HttpServletRequest::getRequestURI).orElse(null));              final User user = userJwtSignature.get().getUser();              // возвращаем юзера в метод             return this.appendCurrentSignature(user, userJwtSignature.get());         } else {             // если авторизация не обязательна, то либо возвращаем полученного юзера, либо null             return this.appendCurrentSignature(userJwtSignature.map(UserJwtSignature::getUser).orElse(null),                     userJwtSignature.orElse(null));         }     }      private User appendCurrentSignature(User user, UserJwtSignature userJwtSignature) {         Optional.ofNullable(user).ifPresent(u -> u.setCurrentSignature(userJwtSignature));         return user;     }      private Optional<String> extractAuthTokenFromRequest(@NonNull final NativeWebRequest nativeWebRequest,                                                          final HttpServletRequest httpServletRequest) {         return Optional.ofNullable(httpServletRequest)                 .flatMap(this::extractAuthTokenFromRequestByCookie)                 .or(() -> this.extractAuthTokenFromRequestByHeader(nativeWebRequest));     }      private Optional<String> extractAuthTokenFromRequestByCookie(final HttpServletRequest httpServletRequest) {         return Optional                 .ofNullable(httpServletRequest)                 .map(request -> WebUtils.getCookie(httpServletRequest, AUTH_COOKIE_NAME))                 .map(Cookie::getValue);     }      private Optional<String> extractAuthTokenFromRequestByHeader(@NonNull final NativeWebRequest nativeWebRequest) {         return Optional.ofNullable(nativeWebRequest.getHeader(AUTH_HEADER_NAME));     } } 

В конструкторе принимаем названия куки и хедера, в которых может передаваться токен. Я их вынес в application.properties

app.api.tokenKeyName=Auth-Token app.api.tokenHeaderName=Auth-Token 

Так же в конструкторе передается созданный ранее TokenHandler и UserJwtSignatureService.

UserJwtSignatureService рассматривать не будем, так как там стандартное извлечение пользователя из базы по его id и сигнатуре токена.

А вот код самого хендлера разберем подробнее.

supportsParameter — проверяется удовлетворяет ли метод необходимым требованиям.

resolveArgument — основной метод, внутри которого и происходит вся «магия».

Итак, что тут происходит:

  1. Достаем из нашей аннотации значение поля required
  2. Получаем HttpServletRequest из пришедшего запроса
  3. Пробуем достать токен из куки или из хедеров
  4. Парсим его, в случае если он есть
  5. Достаем из базы пользователя по токену
  6. Далее смотрим на значение поля required, и если оно обязательное, то проверяем наличие полученного пользователя.
    В случае, если мы не достали пользователя, то бросаем исключение(для чего это надо, объясню в следующем разделе).
    Если же удалось найти пользователя по токену, то возвращаем его, и тем самым, он будет заинжекчен в наш метод.
  7. В случае если авторизация необязательная, о чем свидетельствует поле required, возвращаем либо полученного юзера, либо null

Обработчик аннотации создан. Но это еще не все. Его надо зарегистрировать, чтобы Spring о нем узнал. Тут все просто. Создаем конфигурационный файл, который имплементирует интерфейс Spring’а WebMvcConfigurer и переопределяем метод addArgumentResolvers

WebMvcConfig.java

package org.website.configuration;  import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.website.annotation.handler.AuthUserHandlerMethodArgumentResolver; import org.website.jwt.TokenHandler; import org.website.service.repository.UserJwtSignatureService;  import java.util.List;  @Configuration @EnableWebMvc public class WebMvcConfig implements WebMvcConfigurer {      @Value("${app.api.tokenKeyName}")     private String tokenKeyName;      @Value("${app.api.tokenHeaderName}")     private String tokenHeaderName;      private final TokenHandler tokenHandler;     private final UserJwtSignatureService userJwtSignatureService;      @Autowired     public WebMvcConfig(final TokenHandler tokenHandler,                         final UserJwtSignatureService userJwtSignatureService) {         this.tokenHandler = tokenHandler;         this.userJwtSignatureService = userJwtSignatureService;     }      @Override     public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {         resolvers.add(new AuthUserHandlerMethodArgumentResolver(                 this.tokenKeyName,                 this.tokenHeaderName,                 this.tokenHandler,                 this.userJwtSignatureService));     } } 

На этом написание аннотации заканчивается.

5. Обработка AuthenticationException

В предыдущем разделе, в обработчике аннотации, в случае если для метода контроллера авторизация обязательна, но пользователь не авторизован мы выбросили исключение AuthenticationException.

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

AuthenticationException.java

package org.website.annotation.exception;  public class AuthenticationException extends Exception {      public AuthenticationException(String requestMethod, String url) {         super(String.format("%s - %s", requestMethod, url));     } } 

И теперь сам обработчик исключения. Для того, чтобы обрабатывать возникшие исключения и отдавать пользователю не какую-то стандартную Spring’овую страницу об ошибке, а нужный нам json, в Spring’е есть аннотация ControllerAdvice.

Добавим класс обработки нашего эксепшена.

AuthenticationExceptionControllerAdvice.java

package org.website.controller.exception.handler;  import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.website.annotation.exception.AuthenticationException; import org.website.http.response.Error; import org.website.http.response.ErrorResponse; import org.website.http.response.Response;  import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse;  @ControllerAdvice @Slf4j public class AuthenticationExceptionControllerAdvice extends AbstractControllerAdvice {      @Value("${app.api.tokenKeyName}")     private String tokenKeyName;      @ExceptionHandler({AuthenticationException.class})     public Response authenticationException(HttpServletResponse response) {         Cookie cookie = new Cookie(tokenKeyName, "");         cookie.setPath("/");         cookie.setMaxAge(0);         response.addCookie(cookie);         return new ErrorResponse.Builder(Error.AUTHENTICATION_ERROR).build();     } } 

Теперь, в случае, если возникнет исключение AuthenticationException, оно будет перехвачено и пользователю вернется json с ошибкой AUTHENTICATION_ERROR

6. Контроллер

Теперь, собственно, ради чего все и затевалось. Создадим контроллер, в котором будет 3 метода:

  1. С обязательной авторизацией
  2. С необязательной авторизацией
  3. Регистрации нового пользователя. Минимальный код. Просто сохраняет пользователя в базу, без паролей. Который, так же, будет возвращать токен нового пользователя

TestAuthController.java

package org.website.controller;  import com.google.gson.JsonObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.website.annotation.AuthUser; import org.website.domain.User; import org.website.http.response.Response; import org.website.http.response.SuccessResponse; import org.website.jwt.GeneratedTokenInfo; import org.website.service.repository.UserJwtSignatureService; import org.website.service.repository.UserService;  import java.util.Optional;  @RestController @RequestMapping("/test-auth") public class TestAuthController {      @Autowired     private UserService userService;      @Autowired     private UserJwtSignatureService userJwtSignatureService;      @RequestMapping(value = "/required", method = RequestMethod.GET)     public Response required(@AuthUser final User user) {         return new SuccessResponse.Builder(user).build();     }      @RequestMapping(value = "/not-required", method = RequestMethod.GET)     public Response notRequired(@AuthUser(required = false) final User user) {         JsonObject response = new JsonObject();          if (null == user) {             response.addProperty("message", "Hello guest!");         } else {             response.addProperty("message", "Hello " + user.getFirstName());         }          return new SuccessResponse.Builder(response).build();     }      @RequestMapping(value = "/sign-up", method = RequestMethod.GET)     public Response signUp(@RequestParam String firstName) {         User user = userService.save(User.builder().firstName(firstName).build());          Optional<GeneratedTokenInfo> generatedTokenInfoOptional =                 userJwtSignatureService.generateNewTokenAndSaveToDb(user);          return new SuccessResponse.Builder(user)                 .addPropertyToPayload("token", generatedTokenInfoOptional.get().getToken())                 .build();     } } 

В методах required и notRequired мы вставляем нашу аннотацию.
В первом случае, если пользователь не авторизован — должен вернуться json с ошибкой, а если авторизован, то вернется информация о пользователе.

Во втором случае, если пользователь не авторизован, то вернется сообщение Hello guest!, а если авторизован, то вернется его имя.
Проверим, что все действительно работает.

Для начала проверим оба метода в качестве неавторизованного пользователя.

/required

/not-required

Все как и ожидалось. Там где авторизация была обязательной — вернулась ошибка, а во втором случае — сообщение Hello guest!.

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

/sign-up

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

Проверим это:

/required

/not-required

В первом случае возвращается просто информацию о пользователе. Во втором случае возвращается приветственное сообщение.

Работает!

7. Заключение

Данный метод не претендует на единственное правильное решение. Возможно, кому-то больше по душе использование Spring Security. Но, как уже было сказано в самом начале, этот метод проверен, удобен в использовании и очень хорошо работает.

ссылка на оригинал статьи https://habr.com/ru/post/516638/