Это не замена 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 проект и добавим следующие зависимости
<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
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 файл
<?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) и время истечения токена. Для этого создадим интерфейсы.
Для передачи времени жизни токена.
package org.website.jwt;
import java.time.LocalDateTime;
import java.util.Optional;
public interface Expiration {
    Optional<LocalDateTime> getAuthTokenExpire();
}
И для передачи ID. Его будет имплементировать сущность юзера
package org.website.jwt;
public interface CreateBy {
    Long getId();
}
Так же создадим дефолтную имплементацию для интерфейса Expiration. По умолчанию токен будет жить 24 часа.
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 — для информации о пришедшем к нам токене.
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();
    }
}
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. Он будет генерировать токен при авторизации юзера, а так же извлекать информацию о токене с которым пришел ранее уже авторизованный юзер.
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. Аннотация и обработчик
Итак, после всех подготовительных работ, перейдем к самому интересному. Как уже было сказано ранее, нам нужна аннотация, которая будет инжектиться в методы контроллера, где нужен авторизованный пользователь.
Создаем аннотацию со следующим кодом
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, который имплементит указанный выше интерфейс.
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 — основной метод, внутри которого и происходит вся «магия».
Итак, что тут происходит:
- Достаем из нашей аннотации значение поля required
- Получаем HttpServletRequest из пришедшего запроса
- Пробуем достать токен из куки или из хедеров
- Парсим его, в случае если он есть
- Достаем из базы пользователя по токену
- Далее смотрим на значение поля required, и если оно обязательное, то проверяем наличие полученного пользователя. 
 В случае, если мы не достали пользователя, то бросаем исключение(для чего это надо, объясню в следующем разделе).
 Если же удалось найти пользователя по токену, то возвращаем его, и тем самым, он будет заинжекчен в наш метод.
- В случае если авторизация необязательная, о чем свидетельствует поле required, возвращаем либо полученного юзера, либо null
Обработчик аннотации создан. Но это еще не все. Его надо зарегистрировать, чтобы Spring о нем узнал. Тут все просто. Создаем конфигурационный файл, который имплементирует интерфейс Spring'а WebMvcConfigurer и переопределяем метод addArgumentResolvers
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 с нужной нам информацией.
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.
Добавим класс обработки нашего эксепшена.
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 метода:
- С обязательной авторизацией
- С необязательной авторизацией
- Регистрации нового пользователя. Минимальный код. Просто сохраняет пользователя в базу, без паролей. Который, так же, будет возвращать токен нового пользователя
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!, а если авторизован, то вернется его имя.
Проверим, что все действительно работает.
Для начала проверим оба метода в качестве неавторизованного пользователя.
Все как и ожидалось. Там где авторизация была обязательной — вернулась ошибка, а во втором случае — сообщение Hello guest!.
Теперь зарегистрируемся и попробуем вызвать эти же методы, но уже с передачей токена в заголовках запроса.
В ответе вернулся токен, который можно использовать для тех запросов, где нужна авторизация.
Проверим это:
В первом случае возвращается просто информацию о пользователе. Во втором случае возвращается приветственное сообщение.
Работает!
7. Заключение
Данный метод не претендует на единственное правильное решение. Возможно, кому-то больше по душе использование Spring Security. Но, как уже было сказано в самом начале, этот метод проверен, удобен в использовании и очень хорошо работает.
 
           
 






Romertosdf
Дальше не читал
oxff
Странно что чувак не выкинул весь Spring. Всё стальное показалось простым? :)
VarLegovar
Зря, довольно интересно, да и автор старался что-то по-своему сделать, а это всегда занимательно