Это не замена 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
Зря, довольно интересно, да и автор старался что-то по-своему сделать, а это всегда занимательно