Spring. Custom authentication using JWT

In this article, I would like to share, in my opinion, the successful experience of writing my bike for user authentication in the REST API using JWT.

It is not a replacement for Spring Security, but it has been doing well in production for over two years now.



I will try to describe the whole process in as much detail as possible, from generating a key for a JWT to a controller, so that even someone unfamiliar with JWT will understand everything.







Content



  • Background
  • Key generation
  • Spring project creation
  • TokenHandler
  • Annotation and handler
  • Handling AuthenticationException
  • Controller


0. Background



First, I want to tell you what exactly prompted me to implement this method of client authentication and why I did not use Spring Security. If you are not interested, you can skip to the next chapter.



By that time, I was working in a small company that develops websites. This was my first job in this area, so I didn't really know anything. After about a month of work, they said that there would be a new project and that it was necessary to prepare the basic functionality for it. I decided to see in more detail how this process was implemented in existing projects. To my regret, everything was not so happy there.



In each method of the controller, where it was necessary to pull out the authorized user, there was something like the following



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

     //  
 }


And so it was everywhere ... Adding a new endpoint began with the fact that this piece of code was copied. I found it a bit strange and completely awkward to use.



To solve this problem, I went to google. Perhaps I was looking for something wrong, but I could not find a suitable solution. Instructions for configuring Spring Security were everywhere.



Let me explain why I didn't want to use Spring Security. It seemed to me too complicated and somehow not very convenient to use it in REST. Yes, and in the methods of endpoint processing, you still probably have to get the user out of the context. Perhaps I'm wrong, since I didn't know much about it, but the article is not about that anyway.



I needed something simple and easy to use. The idea came to do it through annotation.



The idea is that we inject our user into each method of the controller where authorization is needed. And that's all. It turns out that inside the controller method there will already be an authorized user and it will be ! = Null (except for cases when authorization is not required).



We figured out the reasons for creating this bike. Now let's get down to practice.



1. Key generation



First, we need to generate a key that will encrypt the minimum required information about the user.



There is a very convenient library for working in java with jwt .



The github has all the instructions on how to work with jwt, but to simplify the process, I will give an example below.



To generate the key, create a regular maven project and add the following dependencies



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




And the class that will generate the secret



SecretGenerator.java
package jwt;

import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;

import javax.crypto.SecretKey;

public class SecretGenerator {

    public static void main(String[] args) {
        SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);
        String secretString = Encoders.BASE64.encode(secretKey.getEncoded());
        System.out.println(secretString);
    }
}




As a result, we get a secret key, which we will use in the future.



2. Creating a Spring project



I will not describe the creation process, since there are many articles and tutorials on this topic. And on the official Spring website there is an initializer , where you can create a minimal project in two clicks.



I will leave only the final pom file



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>




After creating the project, copy the previously created key to application.properties



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


3. TokenHandler



We will need a service for generating and decrypting tokens.



The token will contain a minimum of information about the user (only his id) and the token expiration time. To do this, we will create interfaces.



To transfer the lifetime of the token.



Expiration.java
package org.website.jwt;

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

public interface Expiration {

    Optional<LocalDateTime> getAuthTokenExpire();
}




And for transferring ID. It will be implemented by the user entity



CreateBy.java
package org.website.jwt;

public interface CreateBy {

    Long getId();
}




We will also create a default implementation for the Expiration interface . By default, the token will live for 24 hours.



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




Let's add a couple of helper classes.



GeneratedTokenInfo - for information about the generated token.

TokenInfo - for information about the token that came to us.



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




Now the TokenHandler itself . It will generate a token upon user authorization, as well as retrieve information about the token with which the previously authorized user came.



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

}




I will not draw your attention, since everything should be clear with this.



4. Annotation and handler



So, after all the preparatory work, let's move on to the most interesting. As mentioned earlier, we need an annotation that will be injected into the controller methods, where an authorized user is needed.



Create an annotation with the following code



AuthUser.java
package org.website.annotation;

import java.lang.annotation.*;

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




It was said earlier that authorization may be optional. Just for this and we need a method required in the summary. If authorization for a specific method is optional and if the incoming user is really not authorized, then null will be injected into the method . But we will be ready for this.



The annotation has been created, but a handler is still needed , which will retrieve a token from the request, receive it from the user base and pass it to the controller method. Spring has a HandlerMethodArgumentResolver interface for such cases . We will implement it.



Create the AuthUserHandlerMethodArgumentResolver class that implements the above interface.



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




In the constructor, we accept the names of the cookie and the header in which the token can be passed. I took them out in application.properties



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


The previously created TokenHandler and UserJwtSignatureService are also passed in the constructor .



We will not consider UserJwtSignatureService, since there is a standard extraction of a user from the database by his id and token signature.



But let's analyze the code of the handler itself in more detail.



supportsParameter - Checks if the method meets the required requirements.



resolveArgument is the main method, inside of which all the "magic" happens.



So what's going on here:



  1. We get the value of the required field from our annotation
  2. HttpServletRequest
  3. ,
  4. required, , .

    , , ( , ).

    , , , .
  5. , required, , null


An annotation processor has been created. But that's not all. It needs to be registered for Spring to know about it. Everything is simple here. Create a configuration file that implements Spring's WebMvcConfigurer interface and override the addArgumentResolvers method



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




This concludes the writing of the annotation.



5. Handling AuthenticationException



In the previous section, in the annotation handler, if authorization is required for a controller method, but the user is not authorized, we threw an AuthenticationException .



Now we need to add the class of this exception and handle it in order to return json to the user with the information we need.



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




And now the exception handler itself. In order to handle the exceptions that have arisen and give the user not some standard Spring error page, but the json we need, Spring has a ControllerAdvice annotation .



Let's add a class for handling our execution.



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




Now, if an AuthenticationException is thrown , it will be caught and a json will be returned to the user with an AUTHENTICATION_ERROR error



6. Controller



Now, in fact, for the sake of which everything was started. Let's create a controller with 3 methods:



  1. Mandatory authorization
  2. With no mandatory authorization
  3. Registration of a new user. Minimal code. It just saves the user to the database, no passwords. Which will also return the new user's token


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




In the required and notRequired methods, we insert our annotation.

In the first case, if the user is not authorized, json should be returned with an error, and if authorized, information about the user will be returned.



In the second case, if the user is not logged in, the message Hello guest! , and if authorized, its name will be returned.

Let's check that everything really works.



First, let's check both methods as an unauthorized user.



/ required




/ not-required




Everything is as expected. Where authorization was required, an error was returned, and in the second case, the message Hello guest! ...



Now let's register and try to call the same methods, but with the transfer of the token in the request headers.



/ sign-up




The response returned a token that can be used for those requests where authorization is needed.



Let's check this:



/ required




/ not-required




In the first case, just information about the user is returned. In the second case, a welcome message is returned.



Working!



7. Conclusion



This method does not claim to be the only correct solution. Someone might prefer using Spring Security. But, as mentioned at the very beginning, this method is proven, easy to use, and works very well.



All Articles