Ce n'est pas un remplacement pour Spring Security, mais il se porte bien en production depuis plus de deux ans maintenant.
Je vais essayer de décrire l'ensemble du processus avec autant de détails que possible, de la génération d'une clé pour un JWT à un contrôleur, afin que même quelqu'un qui ne connaît pas JWT comprenne tout.
Contenu
- Contexte
- Génération de clés
- Création de projet de printemps
- TokenHandler
- Annotation et gestionnaire
- Gestion de l'exception AuthenticationException
- Manette
0. Contexte
Pour commencer, je veux vous dire ce qui m'a exactement incité à mettre en œuvre cette méthode d'authentification client et pourquoi je n'ai pas utilisé Spring Security. Si vous n'êtes pas intéressé, vous pouvez passer au chapitre suivant.
À ce moment-là, je travaillais dans une petite entreprise qui développe des sites Web. C'était mon premier travail dans ce domaine, donc je ne savais vraiment rien. Après environ un mois de travail, ils ont dit qu'il y aurait un nouveau projet et qu'il était nécessaire de préparer les fonctionnalités de base pour cela. J'ai décidé de voir plus en détail comment ce processus était implémenté dans les projets existants. À mon grand regret, tout n'était pas si heureux là-bas.
Dans chaque méthode du contrôleur, où il était nécessaire de retirer l'utilisateur autorisé, il y avait quelque chose comme ce qui suit
@RequestMapping(value = "/endpoint", method = RequestMethod.GET)
public Response endpoint() {
User user = getUser(); //
if (null == user)
return new ErrorResponse.Builder(Error.AUTHENTICATION_ERROR).build();
//
}
Et donc c'était partout ... L'ajout d'un nouveau point de terminaison a commencé avec le fait que ce morceau de code a été copié. Je l'ai trouvé un
Pour résoudre ce problème, je suis allé sur google. Peut-être que je cherchais quelque chose qui n'allait pas, mais je ne pouvais pas trouver de solution appropriée. Les instructions pour configurer Spring Security étaient partout.
Laissez-moi vous expliquer pourquoi je ne voulais pas utiliser Spring Security. Cela m'a paru trop compliqué et pas très pratique de l'utiliser dans REST. Oui, et dans les méthodes de traitement des points de terminaison, vous devez probablement sortir l'utilisateur du contexte. Peut-être que je me trompe, car je ne savais pas grand-chose à ce sujet, mais l'article n'en parle pas de toute façon.
J'avais besoin de quelque chose de simple et de facile à utiliser. L'idée est venue de le faire par annotation.
L'idée est que nous injectons notre utilisateur dans chaque méthode du contrôleur où une autorisation est nécessaire. Et c'est tout. Il s'avère qu'à l'intérieur de la méthode du contrôleur, il y aura déjà un utilisateur autorisé et ce sera ! = Null (sauf dans les cas où l'autorisation n'est pas requise).
Nous avons découvert les raisons de la création de ce vélo. Passons maintenant à la pratique.
1. Génération de clés
Tout d'abord, nous devons générer une clé qui cryptera les informations minimales requises sur l'utilisateur.
Il existe une bibliothèque très pratique pour travailler en java avec jwt .
Le github a toutes les instructions sur la façon de travailler avec jwt, mais pour simplifier le processus, je vais donner un exemple ci-dessous.
Pour générer la clé, créez un projet maven normal et ajoutez les dépendances suivantes
dépendances
<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>
Et la classe qui générera le 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);
}
}
En conséquence, nous obtenons une clé secrète, que nous utiliserons à l'avenir.
2. Création d'un projet Spring
Je ne décrirai pas le processus de création, car il existe de nombreux articles et tutoriels sur ce sujet. Et sur le site officiel de Spring, il y a un initialiseur , où vous pouvez créer un projet minimal en deux clics.
Je ne laisserai que le fichier 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>
Après avoir créé le projet, copiez la clé créée précédemment dans application.properties
app.api.jwtEncodedSecretKey=teTN1EmB5XADI5iV4daGVAQhBlTwLMAE+LlXZp1JPI2PoQOpgVksRqe79EGOc5opg+AmxOOmyk8q1RbfSWcOyg==
3. TokenHandler
Nous aurons besoin d'un service pour générer et déchiffrer les jetons.
Le token contiendra un minimum d'informations sur l'utilisateur (uniquement son identifiant) et le délai d'expiration du token. Pour ce faire, nous allons créer des interfaces.
Pour transférer la durée de vie du jeton.
Expiration.java
package org.website.jwt;
import java.time.LocalDateTime;
import java.util.Optional;
public interface Expiration {
Optional<LocalDateTime> getAuthTokenExpire();
}
Et pour le transfert d'identité. Il sera mis en œuvre par l'entité utilisatrice
CreateBy.java
package org.website.jwt;
public interface CreateBy {
Long getId();
}
Nous allons également créer une implémentation par défaut pour l'interface d' expiration . Par défaut, le jeton vivra pendant 24 heures.
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));
}
}
Ajoutons quelques classes d'assistance.
GeneratedTokenInfo - pour plus d'informations sur le jeton généré.
TokenInfo - pour plus d'informations sur le token qui nous est parvenu.
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;
}
}
Maintenant, le TokenHandler lui-même . Il générera un jeton lors de l'autorisation de l'utilisateur, ainsi que des informations sur le jeton avec lequel l'utilisateur précédemment autorisé est venu.
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();
}
}
Je n'attirerai pas votre attention, car tout doit être clair à ce sujet.
4. Annotation et gestionnaire
Alors, après tous les travaux préparatoires, passons au plus intéressant. Comme mentionné précédemment, nous avons besoin d'une annotation qui sera injectée dans les méthodes du contrôleur, où un utilisateur autorisé est nécessaire.
Créez une annotation avec le code suivant
AuthUser.java
package org.website.annotation;
import java.lang.annotation.*;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthUser {
boolean required() default true;
}
Il a été dit précédemment que l'autorisation peut être facultative. Juste pour cela et nous avons besoin d'une méthode requise dans le résumé. Si l'autorisation pour une méthode spécifique est facultative et si l'utilisateur entrant n'est vraiment pas autorisé, alors null sera injecté dans la méthode . Mais nous serons prêts pour cela.
L'annotation a été créée, mais nous avons toujours besoin d'un gestionnaire , qui récupérera le jeton de la requête, le recevra de la base d'utilisateurs et le transmettra à la méthode du contrôleur. Dans de tels cas, Spring a une interface HandlerMethodArgumentResolver . Nous allons le mettre en œuvre.
Créez la classe AuthUserHandlerMethodArgumentResolver qui implémente l'interface ci-dessus.
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));
}
}
Dans le constructeur, nous acceptons les noms du cookie et l'en-tête dans lequel le jeton peut être passé. Je les ai retirés dans application.properties
app.api.tokenKeyName=Auth-Token
app.api.tokenHeaderName=Auth-Token
Les TokenHandler et UserJwtSignatureService créés précédemment sont également passés dans le constructeur .
Nous ne considérerons pas UserJwtSignatureService, car il existe une extraction standard d'un utilisateur de la base de données par son identifiant et sa signature de jeton.
Mais analysons plus en détail le code du gestionnaire lui-même.
supportsParameter - Vérifie si la méthode répond aux exigences requises.
ResolutionArgument est la méthode principale à l'intérieur de laquelle toute la "magie" se produit.
Alors que se passe-t-il ici:
- Nous obtenons la valeur du champ requis à partir de notre annotation
- HttpServletRequest
- ,
- required, , .
, , ( , ).
, , , . - , required, , null
Un processeur d'annotations a été créé. Mais ce n'est pas tout. Il doit être enregistré au printemps pour le savoir. Tout est simple ici. Créez un fichier de configuration qui implémente l'interface WebMvcConfigurer de Spring et remplacez la méthode 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));
}
}
Ceci conclut l'écriture de l'annotation.
5. Gestion de AuthenticationException
Dans la section précédente, dans le gestionnaire d'annotations, si une autorisation est requise pour une méthode de contrôleur, mais que l'utilisateur n'est pas autorisé, nous avons lancé une AuthenticationException .
Nous devons maintenant ajouter la classe de cette exception et la gérer afin de renvoyer json à l'utilisateur avec les informations dont nous avons besoin.
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));
}
}
Et maintenant le gestionnaire d'exceptions lui-même. Afin de gérer les exceptions qui se sont produites et de ne pas donner à l'utilisateur une page d'erreur Spring standard, mais le json dont nous avons besoin, Spring a une annotation ControllerAdvice .
Ajoutons une classe pour gérer notre exécution.
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();
}
}
Maintenant, si une AuthenticationException est lancée , elle sera interceptée et un json avec une erreur AUTHENTICATION_ERROR sera renvoyé à l' utilisateur
6. Contrôleur
Maintenant, en fait, pour le bien de quoi tout a été commencé. Créons un contrôleur avec 3 méthodes:
- Autorisation obligatoire
- Avec aucune autorisation obligatoire
- Enregistrement d'un nouvel utilisateur. Code minimal. Il enregistre simplement l'utilisateur dans la base de données, sans mot de passe. Ce qui renverra également le jeton du nouvel utilisateur
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();
}
}
Dans les méthodes requises et non requises, nous insérons notre annotation.
Dans le premier cas, si l'utilisateur n'est pas autorisé, json doit être renvoyé avec une erreur, et s'il est autorisé, des informations sur l'utilisateur seront renvoyées.
Dans le second cas, si l'utilisateur n'est pas connecté, le message Hello guest! , et si autorisé, son nom sera retourné.
Vérifions que tout fonctionne vraiment.
Tout d'abord, vérifions les deux méthodes en tant qu'utilisateur non autorisé.
Tout est comme prévu. Là où une autorisation était requise, une erreur a été renvoyée, et dans le second cas, le message Hello guest! ...
Maintenant, enregistrons et essayons d'appeler les mêmes méthodes, mais avec le transfert du jeton dans les en-têtes de la requête.
La réponse a renvoyé un jeton qui peut être utilisé pour les demandes pour lesquelles une autorisation est nécessaire.
Vérifions ceci:
Dans le premier cas, seules les informations sur l'utilisateur sont renvoyées. Dans le second cas, un message de bienvenue est renvoyé.
Travail!
7. Conclusion
Cette méthode ne prétend pas être la seule solution correcte. Quelqu'un pourrait préférer utiliser Spring Security. Mais, comme mentionné au tout début, cette méthode est éprouvée, facile à utiliser et fonctionne très bien.