Primavera. Autenticación personalizada mediante JWT

En este artículo, me gustaría compartir, en mi opinión, la exitosa experiencia de escribir mi bicicleta para la autenticación de usuarios en la API REST usando JWT.

No es un reemplazo de Spring Security, pero ha estado funcionando bien en producción durante más de dos años.



Intentaré describir todo el proceso con el mayor detalle posible, desde la generación de una clave para un JWT hasta un controlador, de modo que incluso alguien que no esté familiarizado con JWT lo entienda todo.







Contenido



  • Antecedentes
  • Generación de claves
  • Creación de proyectos de primavera
  • TokenHandler
  • Anotación y controlador
  • Manejo de AuthenticationException
  • Controlador


0. Antecedentes



Para empezar, quiero decirle qué me impulsó exactamente a implementar este método de autenticación de cliente y por qué no usé Spring Security. Si no está interesado, puede pasar al siguiente capítulo.



En ese momento, estaba trabajando en una pequeña empresa que desarrolla sitios web. Este fue mi primer trabajo en esta área, así que realmente no sabía nada. Después de aproximadamente un mes de trabajo, dijeron que habría un nuevo proyecto y que era necesario preparar una funcionalidad básica para él. Decidí ver con más detalle cómo se implementó este proceso en proyectos existentes. A mi pesar, no todo fue tan feliz allí.



En cada método del controlador, donde era necesario sacar al usuario autorizado, había algo como lo siguiente



@RequestMapping(value = "/endpoint", method = RequestMethod.GET)
 public Response endpoint() {
     User user = getUser(); //   
     if (null == user)
         return new ErrorResponse.Builder(Error.AUTHENTICATION_ERROR).build();

     //  
 }


Y así fue en todas partes ... La adición de un nuevo punto final comenzó con el hecho de que se copió este fragmento de código. Lo encontré un poco extraño y completamente incómodo de usar.



Para solucionar este problema, fui a google. Quizás estaba buscando algo mal, pero no pude encontrar una solución adecuada. Las instrucciones para configurar Spring Security estaban en todas partes.



Déjame explicarte por qué no quería usar Spring Security. Me pareció demasiado complicado y de alguna manera no muy conveniente usarlo en REST. Y en los métodos de procesamiento de puntos finales, probablemente aún tendrá que sacar al usuario del contexto. Quizás me equivoque, ya que no sabía mucho al respecto, pero el artículo no trata de eso de todos modos.



Necesitaba algo simple y fácil de usar. La idea surgió de hacerlo mediante anotación.



La idea es que inyectemos a nuestro usuario en cada método del controlador, donde se necesita autorización. Y eso es todo. Resulta que dentro del método del controlador ya habrá un usuario autorizado y será ! = Null (excepto en los casos en que no se requiere autorización).



Descubrimos las razones para crear esta bicicleta. Ahora pongámonos a practicar.



1. Generación de claves



Primero, necesitamos generar una clave que cifrará la información mínima requerida sobre el usuario.



Existe una biblioteca muy conveniente para trabajar en java con jwt .



El github tiene todas las instrucciones sobre cómo trabajar con jwt, pero para simplificar el proceso, daré un ejemplo a continuación.



Para generar la clave, cree un proyecto maven regular y agregue las siguientes dependencias



dependencias
<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>




Y la clase que generará el secreto



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);
    }
}




Como resultado, obtenemos una clave secreta, que usaremos en el futuro.



2. Creando un proyecto Spring



No describiré el proceso de creación, ya que hay muchos artículos y tutoriales sobre este tema. Y en el sitio web oficial de Spring hay un inicializador , donde puede crear un proyecto mínimo en dos clics.



Dejaré solo el archivo pom final



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>




Después de crear el proyecto, copie la clave creada anteriormente en application.properties



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


3. TokenHandler



Necesitaremos un servicio para generar y descifrar tokens.



El token contendrá un mínimo de información sobre el usuario (solo su identificación) y el tiempo de vencimiento del token. Para hacer esto, crearemos interfaces.



Para transferir la vida útil del token.



Expiration.java
package org.website.jwt;

import java.time.LocalDateTime;
import java.util.Optional;

public interface Expiration {

    Optional<LocalDateTime> getAuthTokenExpire();
}




Y para transferir ID. Será implementado por la entidad usuaria



CreateBy.java
package org.website.jwt;

public interface CreateBy {

    Long getId();
}




También crearemos una implementación predeterminada para la interfaz Expiration . De forma predeterminada, el token estará activo durante 24 horas.



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));
    }
}




Agreguemos un par de clases de ayuda.



GeneratedTokenInfo : para obtener información sobre el token generado.

TokenInfo : para obtener información sobre el token que nos llegó.



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;
    }
}




Ahora el TokenHandler en. Generará un token con la autorización del usuario, así como también recuperará información sobre el token con el que vino el usuario previamente autorizado.



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();
    }

}




No llamaré su atención, ya que todo debe quedar claro con esto.



4. Anotación y controlador



Entonces, después de todo el trabajo preparatorio, pasemos a lo más interesante. Como se mencionó anteriormente, necesitamos una anotación que se inyectará en los métodos del controlador, donde se necesita un usuario autorizado.



Crea una anotación con el siguiente código



AuthUser.java
package org.website.annotation;

import java.lang.annotation.*;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthUser {
    boolean required() default true;
}




Se dijo anteriormente que la autorización puede ser opcional. Solo para esto y necesitamos un método requerido en el resumen. Si la autorización para un método específico es opcional y si el usuario entrante realmente no está autorizado, se inyectará null en el método . Pero estaremos preparados para esto.



Se ha creado la anotación, pero aún se necesita un controlador , que recuperará un token de la solicitud, lo recibirá de la base de usuarios y lo pasará al método del controlador. Spring tiene una interfaz HandlerMethodArgumentResolver para tales casos . Lo implementaremos.



Cree la clase AuthUserHandlerMethodArgumentResolver que implementa la interfaz anterior.



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));
    }
}




En el constructor, aceptamos los nombres de la cookie y el encabezado en el que se puede pasar el token. Los saqué en application.properties



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


El TokenHandler y UserJwtSignatureService creados previamente también se pasan en el constructor .



No consideraremos UserJwtSignatureService, ya que existe una extracción estándar de un usuario de la base de datos por su identificación y firma de token.



Pero analicemos el código del propio controlador con más detalle.



supportsParameter : comprueba si el método cumple los requisitos requeridos.



resolveArgument es el método principal, dentro del cual ocurre toda la "magia".



Entonces, ¿qué está pasando aquí?



  1. Obtenemos el valor del campo requerido de nuestra anotación
  2. HttpServletRequest
  3. ,
  4. required, , .

    , , ( , ).

    , , , .
  5. , required, , null


Se ha creado un procesador de anotaciones. Pero eso no es todo. Debe estar registrado para que Spring lo sepa. Aquí todo es sencillo. Cree un archivo de configuración que implemente la interfaz WebMvcConfigurer de Spring y anule el método 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));
    }
}




Con esto concluye la redacción de la anotación.



5. Manejo de AuthenticationException



En la sección anterior, en el manejador de anotaciones, si se requiere autorización para un método de controlador, pero el usuario no está autorizado, lanzamos una AuthenticationException .



Ahora necesitamos agregar la clase de esta excepción y manejarla para devolver json al usuario con la información que necesitamos.



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));
    }
}




Y ahora el propio controlador de excepciones. Para manejar las excepciones que han surgido y darle al usuario no una página de error estándar de Spring, sino el json que necesitamos, Spring tiene una anotación ControllerAdvice .



Agreguemos una clase para manejar nuestra ejecución.



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();
    }
}




Ahora, si se lanza una AuthenticationException , se detectará y se devolverá al usuario un json con un error AUTHENTICATION_ERROR



6. Controlador



Ahora, de hecho, por el bien de lo cual se inició todo. Creemos un controlador con 3 métodos:



  1. Autorización obligatoria
  2. Con ninguna autorización obligatoria
  3. Registro de un nuevo usuario. Código mínimo. Simplemente guarda al usuario en la base de datos, sin contraseñas. Que también devolverá el token del nuevo usuario


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();
    }
}




En los métodos required y notRequired, insertamos nuestra anotación.

En el primer caso, si el usuario no está autorizado, se debe devolver json con un error y, si está autorizado, se devolverá la información sobre el usuario.



En el segundo caso, si el usuario no ha iniciado sesión, aparecerá el mensaje ¡Hola invitado! , y si está autorizado, se devolverá su nombre.

Comprobemos que todo realmente funciona.



Primero, revisemos ambos métodos como usuario no autorizado.



/ requerido




/ no requerido




Todo es como se esperaba. Cuando se requirió autorización, se devolvió un error y, en el segundo caso, el mensaje ¡Hola invitado! ...



Ahora registremos e intentemos llamar a los mismos métodos, pero con la transferencia del token en los encabezados de la solicitud.



/ Regístrate




La respuesta devolvió un token que se puede usar para aquellas solicitudes donde se necesita autorización.



Revisemos esto:



/ requerido




/ no requerido




En el primer caso, solo se devuelve información sobre el usuario. En el segundo caso, se devuelve un mensaje de bienvenida.



¡Trabajando!



7. Conclusión



Este método no pretende ser la única solución correcta. Alguien podría preferir usar Spring Security. Pero, como se mencionó al principio, este método está probado, es fácil de usar y funciona muy bien.



All Articles