Frühling. Benutzerdefinierte Authentifizierung mit JWT

In diesem Artikel möchte ich meiner Meinung nach die erfolgreiche Erfahrung mit dem Schreiben meines Fahrrads für die Benutzerauthentifizierung in der REST-API mithilfe von JWT mitteilen.

Es ist kein Ersatz für Spring Security, aber es läuft seit über zwei Jahren gut in der Produktion.



Ich werde versuchen, den gesamten Prozess so detailliert wie möglich zu beschreiben, von der Generierung eines Schlüssels für eine JWT bis hin zu einem Controller, damit auch jemand, der mit JWT nicht vertraut ist, alles versteht.







Inhalt



  • Hintergrund
  • Schlüsselgenerierung
  • Erstellung des Frühlingsprojekts
  • TokenHandler
  • Anmerkung und Handler
  • Umgang mit AuthenticationException
  • Regler


0. Hintergrund



Zunächst möchte ich Ihnen sagen, was mich genau dazu veranlasst hat, diese Methode der Clientauthentifizierung zu implementieren, und warum ich Spring Security nicht verwendet habe. Wenn Sie nicht interessiert sind, können Sie mit dem nächsten Kapitel fortfahren.



Zu dieser Zeit arbeitete ich in einer kleinen Firma, die Websites entwickelt. Dies war mein erster Job auf diesem Gebiet, also wusste ich nichts wirklich. Nach ungefähr einem Monat Arbeit sagten sie, dass es ein neues Projekt geben würde und dass es notwendig sei, die grundlegenden Funktionen dafür vorzubereiten. Ich habe mich entschlossen, genauer zu sehen, wie dieser Prozess in bestehenden Projekten implementiert wurde. Zu meinem Bedauern war dort nicht alles so glücklich.



Bei jeder Methode der Steuerung, bei der der autorisierte Benutzer herausgezogen werden musste, gab es etwa Folgendes



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

     //  
 }


Und so war es überall ... Das Hinzufügen eines neuen Endpunkts begann mit der Tatsache, dass dieser Code kopiert wurde. Ich fand es etwas seltsam und völlig umständlich zu bedienen.



Um dieses Problem zu lösen, ging ich zu Google. Vielleicht habe ich nach etwas Falschem gesucht, aber ich konnte keine geeignete Lösung finden. Anweisungen zum Konfigurieren von Spring Security gab es überall.



Lassen Sie mich erklären, warum ich Spring Security nicht verwenden wollte. Es schien mir zu kompliziert und irgendwie nicht sehr bequem, es in REST zu verwenden. Und bei den Endpunktverarbeitungsmethoden müssen Sie den Benutzer wahrscheinlich immer noch aus dem Kontext herausholen. Vielleicht irre ich mich, da ich nicht viel darüber wusste, aber der Artikel handelt sowieso nicht davon.



Ich brauchte etwas Einfaches und Leichtes. Die Idee kam dazu durch Annotation.



Die Idee ist, dass wir unseren Benutzer in jede Methode des Controllers einfügen, für die eine Autorisierung erforderlich ist. Und alle. Es stellt sich heraus, dass innerhalb der Controller-Methode bereits ein autorisierter Benutzer vorhanden ist und ! = Null (außer in Fällen, in denen keine Autorisierung erforderlich ist).



Wir haben die Gründe für die Entwicklung dieses Fahrrads herausgefunden. Jetzt lass uns üben.



1. Schlüsselgenerierung



Zunächst müssen wir einen Schlüssel generieren, der die minimal erforderlichen Informationen über den Benutzer verschlüsselt.



Es gibt eine sehr praktische Bibliothek für die Arbeit in Java mit JWT .



Der Github enthält alle Anweisungen zum Arbeiten mit jwt. Um den Vorgang zu vereinfachen, werde ich im Folgenden ein Beispiel geben.



Erstellen Sie zum Generieren des Schlüssels ein reguläres Maven-Projekt und fügen Sie die folgenden Abhängigkeiten hinzu



Abhängigkeiten
<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>




Und die Klasse, die ein Geheimnis erzeugt



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




Als Ergebnis erhalten wir einen geheimen Schlüssel, den wir in Zukunft verwenden werden.



2. Erstellen eines Spring-Projekts



Ich werde den Erstellungsprozess nicht beschreiben, da es zu diesem Thema viele Artikel und Tutorials gibt. Auf der offiziellen Spring-Website gibt es einen Initialisierer , mit dem Sie mit zwei Klicks ein minimales Projekt erstellen können.



Ich werde nur die endgültige POM- Datei hinterlassen



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>




Kopieren Sie nach dem Erstellen des Projekts den zuvor erstellten Schlüssel in application.properties



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


3. TokenHandler



Wir benötigen einen Dienst zum Generieren und Entschlüsseln von Token.



Das Token enthält ein Minimum an Informationen über den Benutzer (nur seine ID) und die Ablaufzeit des Tokens. Dazu erstellen wir Schnittstellen.



So übertragen Sie die Lebensdauer des Tokens



Expiration.java
package org.website.jwt;

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

public interface Expiration {

    Optional<LocalDateTime> getAuthTokenExpire();
}




Und zur Übertragung der ID. Es wird von der Benutzerentität implementiert



CreateBy.java
package org.website.jwt;

public interface CreateBy {

    Long getId();
}




Wir werden auch eine Standardimplementierung für die Ablaufschnittstelle erstellen . Standardmäßig ist das Token 24 Stunden lang gültig.



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




Fügen wir ein paar Hilfsklassen hinzu.



GeneratedTokenInfo - Informationen zum generierten Token.

TokenInfo - für Informationen über das Token, das zu uns gekommen ist.



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




Nun der TokenHandler selbst . Bei der Benutzerautorisierung wird ein Token generiert und es werden Informationen zu dem Token abgerufen, mit dem der zuvor autorisierte Benutzer gekommen ist.



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

}




Ich werde Ihre Aufmerksamkeit nicht auf sich ziehen, da damit alles klar sein sollte.



4. Anmerkung und Handler



Lassen Sie uns nach all den Vorarbeiten zu den interessantesten übergehen. Wie bereits erwähnt, benötigen wir eine Anmerkung, die in die Controller-Methoden eingefügt wird, wenn ein autorisierter Benutzer benötigt wird.



Erstellen Sie eine Anmerkung mit dem folgenden 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;
}




Es wurde bereits früher gesagt, dass die Autorisierung optional sein kann. Nur dafür und wir brauchen eine Methode, die in der Zusammenfassung benötigt wird. Wenn die Autorisierung für eine bestimmte Methode optional ist und der eingehende Benutzer wirklich nicht autorisiert ist, wird null in die Methode eingefügt . Aber wir werden dafür bereit sein.



Die Annotation wurde erstellt, aber wir benötigen noch einen Handler , der das Token aus der Anforderung abruft, es von der Benutzerbasis empfängt und an die Controller-Methode weiterleitet. In solchen Fällen verfügt Spring über eine HandlerMethodArgumentResolver- Schnittstelle . Wir werden es umsetzen.



Erstellen Sie die AuthUserHandlerMethodArgumentResolver- Klasse , die die obige Schnittstelle implementiert.



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




Im Konstruktor akzeptieren wir die Namen des Cookies und den Header, in dem das Token übergeben werden kann. Ich habe sie in application.properties herausgenommen



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


Der zuvor erstellte TokenHandler und UserJwtSignatureService werden ebenfalls im Konstruktor übergeben .



UserJwtSignatureService wird nicht berücksichtigt, da ein Benutzer standardmäßig anhand seiner ID und seiner Tokensignatur aus der Datenbank extrahiert wird.



Aber lassen Sie uns den Code des Handlers selbst genauer analysieren.



supportParameter - Überprüft, ob die Methode die erforderlichen Anforderungen erfüllt.



resolveArgument ist die Hauptmethode, in der die ganze "Magie" geschieht.



Also, was ist hier los:



  1. Den erforderlichen Feldwert erhalten wir aus unserer Anmerkung
  2. HttpServletRequest
  3. ,
  4. required, , .

    , , ( , ).

    , , , .
  5. , required, , null


Ein Anmerkungsprozessor wurde erstellt. Aber das ist nicht alles. Es muss für Spring registriert sein, um davon zu erfahren. Hier ist alles einfach. Erstellen Sie eine Konfigurationsdatei, die die WebMvcConfigurer- Schnittstelle von Spring implementiert , und überschreiben Sie die Methode 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));
    }
}




Damit ist das Schreiben der Anmerkung abgeschlossen.



5. Umgang mit AuthenticationException



Im vorherigen Abschnitt haben wir im Annotation-Handler eine AuthenticationException ausgelöst, wenn für eine Controller-Methode eine Autorisierung erforderlich ist, der Benutzer jedoch nicht autorisiert ist .



Jetzt müssen wir die Klasse dieser Ausnahme hinzufügen und behandeln, um json mit den benötigten Informationen an den Benutzer zurückzugeben.



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




Und jetzt der Ausnahmebehandler selbst. Um die aufgetretenen Ausnahmen zu behandeln und dem Benutzer keine Standard-Spring-Fehlerseite, sondern den von uns benötigten JSON zu geben, verfügt Spring über eine ControllerAdvice- Annotation .



Fügen wir eine Klasse für die Ausführung unserer Ausführung hinzu.



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




Wenn nun eine AuthenticationException ausgelöst wird , wird sie abgefangen und ein JSON mit einem AUTHENTICATION_ERROR- Fehler wird an den Benutzer zurückgegeben



6. Controller



Nun, in der Tat, um dessen willen alles begonnen wurde. Lassen Sie uns einen Controller mit 3 Methoden erstellen:



  1. Obligatorische Genehmigung
  2. Mit keiner Zulassungspflicht
  3. Registrierung eines neuen Benutzers. Minimaler Code. Es speichert nur den Benutzer in der Datenbank, keine Passwörter. Dadurch wird auch das Token des neuen Benutzers zurückgegeben


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 den erforderlichen und nicht erforderlichen Methoden fügen wir unsere Anmerkung ein.

Im ersten Fall, wenn der Benutzer nicht autorisiert ist, sollte json mit einem Fehler zurückgegeben werden, und wenn autorisiert, werden Informationen über den Benutzer zurückgegeben.



Im zweiten Fall, wenn der Benutzer nicht angemeldet ist, wird die Nachricht Hallo Gast! und wenn autorisiert, wird sein Name zurückgegeben.

Lassen Sie uns überprüfen, ob wirklich alles funktioniert.



Lassen Sie uns zunächst beide Methoden als nicht autorisierter Benutzer überprüfen.



/ erforderlich




/ nicht benötigt




Alles ist wie erwartet. Wo eine Autorisierung erforderlich war, wurde ein Fehler zurückgegeben und im zweiten Fall die Meldung Hallo Gast! ...



Jetzt registrieren wir uns und versuchen, die gleichen Methoden aufzurufen, jedoch mit der Übertragung des Tokens in den Anforderungsheadern.



/ Anmelden




Die Antwort gab ein Token zurück, das für Anforderungen verwendet werden kann, für die eine Autorisierung erforderlich ist.



Lassen Sie uns dies überprüfen:



/ erforderlich




/ nicht benötigt




Im ersten Fall werden nur Informationen über den Benutzer zurückgegeben. Im zweiten Fall wird eine Willkommensnachricht zurückgegeben.



Arbeiten!



7. Fazit



Diese Methode erhebt nicht den Anspruch, die einzig richtige Lösung zu sein. Jemand könnte es vorziehen, Spring Security zu verwenden. Wie eingangs erwähnt, ist diese Methode jedoch bewährt, einfach anzuwenden und funktioniert sehr gut.



All Articles