Это не замена 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. Но, как уже было сказано в самом начале, этот метод проверен, удобен в использовании и очень хорошо работает.
ссылка на оригинал статьи https://habr.com/ru/post/516638/
Добавить комментарий