Fonctionnement de l'authentification basée sur des jetons
Dans l'authentification basée sur des jetons, le client échange des informations d'identification matérielles (telles que le nom d'utilisateur et le mot de passe) contre une donnée appelée jeton . Pour chaque demande, au lieu d'envoyer les informations d'identification matérielles, le client enverra le jeton au serveur pour effectuer l'authentification puis l'autorisation.
En quelques mots, un schéma d'authentification basé sur des jetons suit ces étapes:
- Le client envoie ses informations d'identification (nom d'utilisateur et mot de passe) au serveur.
- Le serveur authentifie les informations d'identification et, si elles sont valides, génèrent un jeton pour l'utilisateur.
- Le serveur stocke le jeton généré précédemment dans un certain stockage avec l'identifiant utilisateur et une date d'expiration.
- Le serveur envoie le jeton généré au client.
- Le client envoie le jeton au serveur dans chaque demande.
- Le serveur, dans chaque demande, extrait le jeton de la demande entrante. Avec le jeton, le serveur recherche les détails de l'utilisateur pour effectuer l'authentification.
- Si le jeton est valide, le serveur accepte la demande.
- Si le jeton n'est pas valide, le serveur refuse la demande.
- Une fois l'authentification effectuée, le serveur effectue l'autorisation.
- Le serveur peut fournir un point de terminaison pour actualiser les jetons.
Remarque: l'étape 3 n'est pas requise si le serveur a émis un jeton signé (tel que JWT, qui vous permet d'effectuer une authentification sans état ).
Ce que vous pouvez faire avec JAX-RS 2.0 (Jersey, RESTEasy et Apache CXF)
Cette solution utilise uniquement l'API JAX-RS 2.0, évitant toute solution spécifique au fournisseur . Il devrait donc fonctionner avec les implémentations JAX-RS 2.0, telles que Jersey , RESTEasy et Apache CXF .
Il convient de mentionner que si vous utilisez l'authentification par jeton, vous ne comptez pas sur les mécanismes de sécurité d'application Web Java EE standard offerts par le conteneur de servlet et configurables via le web.xml
descripteur de l'application . C'est une authentification personnalisée.
Authentification d'un utilisateur avec son nom d'utilisateur et son mot de passe et émission d'un jeton
Créez une méthode de ressource JAX-RS qui reçoit et valide les informations d'identification (nom d'utilisateur et mot de passe) et émet un jeton pour l'utilisateur:
@Path("/authentication")
public class AuthenticationEndpoint {
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response authenticateUser(@FormParam("username") String username,
@FormParam("password") String password) {
try {
// Authenticate the user using the credentials provided
authenticate(username, password);
// Issue a token for the user
String token = issueToken(username);
// Return the token on the response
return Response.ok(token).build();
} catch (Exception e) {
return Response.status(Response.Status.FORBIDDEN).build();
}
}
private void authenticate(String username, String password) throws Exception {
// Authenticate against a database, LDAP, file or whatever
// Throw an Exception if the credentials are invalid
}
private String issueToken(String username) {
// Issue a token (can be a random String persisted to a database or a JWT token)
// The issued token must be associated to a user
// Return the issued token
}
}
Si des exceptions sont levées lors de la validation des informations d'identification, une réponse avec le statut 403
(Interdit) sera retournée.
Si les informations d'identification sont validées avec succès, une réponse avec le statut 200
(OK) sera retournée et le jeton émis sera envoyé au client dans la charge utile de réponse. Le client doit envoyer le jeton au serveur dans chaque demande.
Lors de la consommation application/x-www-form-urlencoded
, le client doit envoyer les informations d'identification au format suivant dans la charge utile de la demande:
username=admin&password=123456
Au lieu de paramètres de formulaire, il est possible d'encapsuler le nom d'utilisateur et le mot de passe dans une classe:
public class Credentials implements Serializable {
private String username;
private String password;
// Getters and setters omitted
}
Et puis consommez-le en JSON:
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response authenticateUser(Credentials credentials) {
String username = credentials.getUsername();
String password = credentials.getPassword();
// Authenticate the user, issue a token and return a response
}
En utilisant cette approche, le client doit envoyer les informations d'identification au format suivant dans la charge utile de la demande:
{
"username": "admin",
"password": "123456"
}
Extraire le token de la requête et le valider
Le client doit envoyer le jeton dans l'en- Authorization
tête HTTP standard de la demande. Par exemple:
Authorization: Bearer <token-goes-here>
Le nom de l'en-tête HTTP standard est regrettable car il contient des informations d' authentification , pas d' autorisation . Cependant, il s'agit de l'en-tête HTTP standard pour l'envoi des informations d'identification au serveur.
JAX-RS fournit @NameBinding
une méta-annotation utilisée pour créer d'autres annotations pour lier les filtres et les intercepteurs aux classes et méthodes de ressources. Définissez une @Secured
annotation comme suit:
@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured { }
L'annotation de liaison de nom définie ci-dessus sera utilisée pour décorer une classe de filtre, qui implémente ContainerRequestFilter
, vous permettant d'intercepter la demande avant qu'elle ne soit traitée par une méthode de ressource. Le ContainerRequestContext
peut être utilisé pour accéder aux en-têtes de requête HTTP, puis extraire le jeton:
@Secured
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {
private static final String REALM = "example";
private static final String AUTHENTICATION_SCHEME = "Bearer";
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Get the Authorization header from the request
String authorizationHeader =
requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
// Validate the Authorization header
if (!isTokenBasedAuthentication(authorizationHeader)) {
abortWithUnauthorized(requestContext);
return;
}
// Extract the token from the Authorization header
String token = authorizationHeader
.substring(AUTHENTICATION_SCHEME.length()).trim();
try {
// Validate the token
validateToken(token);
} catch (Exception e) {
abortWithUnauthorized(requestContext);
}
}
private boolean isTokenBasedAuthentication(String authorizationHeader) {
// Check if the Authorization header is valid
// It must not be null and must be prefixed with "Bearer" plus a whitespace
// The authentication scheme comparison must be case-insensitive
return authorizationHeader != null && authorizationHeader.toLowerCase()
.startsWith(AUTHENTICATION_SCHEME.toLowerCase() + " ");
}
private void abortWithUnauthorized(ContainerRequestContext requestContext) {
// Abort the filter chain with a 401 status code response
// The WWW-Authenticate header is sent along with the response
requestContext.abortWith(
Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE,
AUTHENTICATION_SCHEME + " realm=\"" + REALM + "\"")
.build());
}
private void validateToken(String token) throws Exception {
// Check if the token was issued by the server and if it's not expired
// Throw an Exception if the token is invalid
}
}
Si des problèmes surviennent lors de la validation du jeton, une réponse avec le statut 401
(Non autorisé) sera retournée. Sinon, la demande passera à une méthode de ressource.
Sécurisation de vos points de terminaison REST
Pour lier le filtre d'authentification aux méthodes ou classes de ressources, annotez-les avec l' @Secured
annotation créée ci-dessus. Pour les méthodes et / ou classes annotées, le filtre sera exécuté. Cela signifie que ces points d'extrémité ne seront atteints que si la demande est effectuée avec un jeton valide.
Si certaines méthodes ou classes n'ont pas besoin d'authentification, ne les annotez tout simplement pas:
@Path("/example")
public class ExampleResource {
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myUnsecuredMethod(@PathParam("id") Long id) {
// This method is not annotated with @Secured
// The authentication filter won't be executed before invoking this method
...
}
@DELETE
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response mySecuredMethod(@PathParam("id") Long id) {
// This method is annotated with @Secured
// The authentication filter will be executed before invoking this method
// The HTTP request must be performed with a valid token
...
}
}
Dans l'exemple ci-dessus, le filtre sera exécuté uniquement pour la mySecuredMethod(Long)
méthode car il est annoté avec @Secured
.
Identifier l'utilisateur actuel
Il est très probable que vous aurez besoin de connaître l'utilisateur qui exécute la demande contre votre API REST. Les approches suivantes peuvent être utilisées pour y parvenir:
Remplacement du contexte de sécurité de la demande actuelle
Dans votre ContainerRequestFilter.filter(ContainerRequestContext)
méthode, une nouvelle SecurityContext
instance peut être définie pour la demande actuelle. Remplacez ensuite le SecurityContext.getUserPrincipal()
, renvoyant une Principal
instance:
final SecurityContext currentSecurityContext = requestContext.getSecurityContext();
requestContext.setSecurityContext(new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return () -> username;
}
@Override
public boolean isUserInRole(String role) {
return true;
}
@Override
public boolean isSecure() {
return currentSecurityContext.isSecure();
}
@Override
public String getAuthenticationScheme() {
return AUTHENTICATION_SCHEME;
}
});
Utilisez le jeton pour rechercher l'identifiant de l'utilisateur (nom d'utilisateur), qui sera le Principal
nom du.
Injectez le SecurityContext
dans n'importe quelle classe de ressources JAX-RS:
@Context
SecurityContext securityContext;
La même chose peut être effectuée dans une méthode de ressource JAX-RS:
@GET
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myMethod(@PathParam("id") Long id,
@Context SecurityContext securityContext) {
...
}
Et puis obtenez Principal
:
Principal principal = securityContext.getUserPrincipal();
String username = principal.getName();
Utilisation de CDI (Context and Dependency Injection)
Si, pour une raison quelconque, vous ne voulez pas remplacer le SecurityContext
, vous pouvez utiliser CDI (Context and Dependency Injection), qui fournit des fonctionnalités utiles telles que les événements et les producteurs.
Créez un qualificatif CDI:
@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER })
public @interface AuthenticatedUser { }
Dans votre AuthenticationFilter
création ci-dessus, injectez un Event
annoté avec @AuthenticatedUser
:
@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;
Si l'authentification réussit, déclenchez l'événement en passant le nom d'utilisateur comme paramètre (rappelez-vous, le jeton est émis pour un utilisateur et le jeton sera utilisé pour rechercher l'identifiant de l'utilisateur):
userAuthenticatedEvent.fire(username);
Il est très probable qu'une classe représente un utilisateur dans votre application. Appelons cette classe User
.
Créez un bean CDI pour gérer l'événement d'authentification, recherchez une User
instance avec le nom d'utilisateur correspondant et affectez-le au authenticatedUser
champ producteur:
@RequestScoped
public class AuthenticatedUserProducer {
@Produces
@RequestScoped
@AuthenticatedUser
private User authenticatedUser;
public void handleAuthenticationEvent(@Observes @AuthenticatedUser String username) {
this.authenticatedUser = findUser(username);
}
private User findUser(String username) {
// Hit the the database or a service to find a user by its username and return it
// Return the User instance
}
}
Le authenticatedUser
champ produit une User
instance qui peut être injectée dans des beans gérés par conteneur, tels que les services JAX-RS, les beans CDI, les servlets et les EJB. Utilisez le morceau de code suivant pour injecter unUser
instance (en fait, c'est un proxy CDI):
@Inject
@AuthenticatedUser
User authenticatedUser;
Notez que l' @Produces
annotation CDI est différente de la JAX-RS@Produces
annotation :
Assurez-vous d'utiliser le CDI @Produces
annotation dans votre AuthenticatedUserProducer
bean.
La clé ici est le bean annoté de @RequestScoped
, vous permettant de partager des données entre les filtres et vos beans. Si vous ne souhaitez pas utiliser d'événements, vous pouvez modifier le filtre pour stocker l'utilisateur authentifié dans un bean à portée de requête, puis le lire à partir de vos classes de ressources JAX-RS.
Par rapport à l'approche qui l'emporte sur le SecurityContext
, l'approche CDI vous permet d'obtenir l'utilisateur authentifié à partir de beans autres que les ressources et les fournisseurs JAX-RS.
Prise en charge de l'autorisation basée sur les rôles
Veuillez vous référer à mon autre réponse pour plus de détails sur la façon de prendre en charge l'autorisation basée sur les rôles.
Emission de jetons
Un jeton peut être:
- Opaque: ne révèle aucun détail autre que la valeur elle-même (comme une chaîne aléatoire)
- Autonome: contient des détails sur le jeton lui-même (comme JWT).
Voir détails ci-dessous:
Chaîne aléatoire en tant que jeton
Un jeton peut être émis en générant une chaîne aléatoire et en la conservant dans une base de données avec l'identifiant utilisateur et une date d'expiration. Un bon exemple de la façon de générer une chaîne aléatoire en Java peut être vu ici . Vous pouvez également utiliser:
Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);
JWT (jeton Web JSON)
JWT (JSON Web Token) est une méthode standard de représentation sécurisée des revendications entre deux parties et est définie par la RFC 7519 .
C'est un jeton autonome et il vous permet de stocker des détails dans les revendications . Ces revendications sont stockées dans la charge utile du jeton, qui est un JSON codé en Base64 . Voici quelques revendications enregistrées dans le RFC 7519 et ce qu'elles signifient (lire le RFC complet pour plus de détails):
iss
: Principal qui a émis le jeton.
sub
: Principal faisant l'objet du JWT.
exp
: Date d'expiration du jeton.
nbf
: Heure à laquelle le jeton commencera à être accepté pour traitement.
iat
: Heure à laquelle le jeton a été émis.
jti
: Identifiant unique du jeton.
Sachez que vous ne devez pas stocker de données sensibles, telles que des mots de passe, dans le jeton.
La charge utile peut être lue par le client et l'intégrité du jeton peut être facilement vérifiée en vérifiant sa signature sur le serveur. La signature est ce qui empêche la falsification du jeton.
Vous n'aurez pas besoin de conserver les jetons JWT si vous n'avez pas besoin de les suivre. Cependant, en conservant les jetons, vous aurez la possibilité d'invalider et de révoquer l'accès de ceux-ci. Pour garder la trace des jetons JWT, au lieu de conserver l'intégralité du jeton sur le serveur, vous pouvez conserver l'identificateur de jeton ( jti
revendication) ainsi que d'autres détails tels que l'utilisateur pour lequel vous avez émis le jeton, la date d'expiration, etc.
Lorsque des jetons persistants, envisagez toujours de supprimer les anciens afin d'empêcher votre base de données de croître indéfiniment.
Utilisation de JWT
Il existe quelques bibliothèques Java pour émettre et valider les jetons JWT, telles que:
Pour trouver d'autres excellentes ressources pour travailler avec JWT, consultez http://jwt.io .
Gestion de la révocation de jetons avec JWT
Si vous souhaitez révoquer des jetons, vous devez en garder la trace. Vous n'avez pas besoin de stocker l'intégralité du jeton côté serveur, stockez uniquement l'identifiant du jeton (qui doit être unique) et certaines métadonnées si vous en avez besoin. Pour l'identifiant de jeton, vous pouvez utiliser UUID .
La jti
revendication doit être utilisée pour stocker l'identifiant de jeton sur le jeton. Lors de la validation du jeton, assurez-vous qu'il n'a pas été révoqué en vérifiant la valeur de la jti
revendication par rapport aux identificateurs de jeton que vous avez côté serveur.
Pour des raisons de sécurité, révoquez tous les jetons d'un utilisateur lorsqu'il change de mot de passe.
Information additionnelle
- Peu importe le type d'authentification que vous décidez d'utiliser. Faites -le toujours au-dessus d'une connexion HTTPS pour empêcher l' attaque de l' homme du milieu .
- Jetez un œil à cette question de la sécurité de l'information pour plus d'informations sur les jetons.
- Dans cet article, vous trouverez des informations utiles sur l'authentification basée sur les jetons.
The server stores the previously generated token in some storage along with the user identifier and an expiration date. The server sends the generated token to the client.
Comment est-ce RESTful?Prise en charge de l'autorisation basée sur les rôles avec l'
@Secured
annotationOutre le flux d'authentification indiqué dans l'autre réponse , l'autorisation basée sur les rôles peut être prise en charge dans les points de terminaison REST.
Créez une énumération et définissez les rôles en fonction de vos besoins:
Modifiez l'
@Secured
annotation de liaison de nom créée précédemment pour prendre en charge les rôles:Et puis annotez les classes de ressources et les méthodes avec
@Secured
pour effectuer l'autorisation. Les annotations de méthode remplaceront les annotations de classe:Créez un filtre avec la
AUTHORIZATION
priorité, qui est exécuté après leAUTHENTICATION
filtre de priorité défini précédemment.Le
ResourceInfo
peut être utilisé pour obtenir la ressourceMethod
et la ressourceClass
qui géreront la demande, puis en extraire les@Secured
annotations:Si l'utilisateur n'est pas autorisé à exécuter l'opération, la demande est abandonnée avec un
403
(Interdit).Pour connaître l'utilisateur qui exécute la demande, consultez ma réponse précédente . Vous pouvez l'obtenir à partir du
SecurityContext
(qui devrait déjà être défini dans leContainerRequestContext
) ou l'injecter à l'aide de CDI, selon l'approche que vous choisissez.Si une
@Secured
annotation n'a aucun rôle déclaré, vous pouvez supposer que tous les utilisateurs authentifiés peuvent accéder à ce point de terminaison, sans tenir compte des rôles des utilisateurs.Prise en charge de l'autorisation basée sur les rôles avec les annotations JSR-250
Alternativement à la définition des rôles dans l'
@Secured
annotation comme indiqué ci-dessus, vous pouvez envisager des annotations JSR-250 telles que@RolesAllowed
,@PermitAll
et@DenyAll
.JAX-RS ne prend pas en charge de telles annotations hors de la boîte, mais cela pourrait être réalisé avec un filtre. Voici quelques considérations à garder à l'esprit si vous souhaitez les prendre en charge toutes:
@DenyAll
sur la méthode a priorité sur@RolesAllowed
et@PermitAll
sur la classe.@RolesAllowed
sur la méthode a priorité@PermitAll
sur la classe.@PermitAll
sur la méthode a priorité@RolesAllowed
sur la classe.@DenyAll
ne peut pas être attaché aux cours.@RolesAllowed
sur la classe a priorité@PermitAll
sur la classe.Ainsi, un filtre d'autorisation qui vérifie les annotations JSR-250 pourrait ressembler à:
Remarque: L'implémentation ci-dessus est basée sur le Jersey
RolesAllowedDynamicFeature
. Si vous utilisez Jersey, vous n'avez pas besoin d'écrire votre propre filtre, utilisez simplement l'implémentation existante.la source
user_id
==token.userId
, ou quelque chose comme ça, mais c'est très répétitif.