Maison >Java >javaDidacticiel >Implémentation de l'authentification basée sur les jetons dans Spring Boot à l'aide du modèle Spring Security, JWT et JDBC

Implémentation de l'authentification basée sur les jetons dans Spring Boot à l'aide du modèle Spring Security, JWT et JDBC

Susan Sarandon
Susan Sarandonoriginal
2024-10-29 02:22:301028parcourir

Lien Github pour la référence du code

Introduction

Dans les applications Web modernes, l'authentification sécurisée des utilisateurs est cruciale. Traditionnellement, l'authentification basée sur la session a été largement utilisée, mais à mesure que les applications deviennent de plus en plus distribuées et évolutives, l'authentification basée sur les jetons offre plusieurs avantages.

L'authentification basée sur les jetons permet aux applications d'être sans état, ce qui signifie que le serveur n'a pas besoin de stocker de données de session, ce qui le rend bien adapté aux API RESTful évolutives.

Ce tutoriel vous guidera dans la mise en œuvre de l'authentification JWT (JSON Web Token) dans une application Spring Boot à l'aide de Spring Security avec le modèle JDBC.

JWT (JSON Web Token) est un moyen compact et sécurisé pour les URL de représenter les réclamations transférées entre deux parties. Il est couramment utilisé pour l'authentification sans état où chaque demande est authentifiée à l'aide d'un jeton signé.

Pourquoi JWT et l'authentification basée sur les jetons ?

Authentification apatride
Les jetons JWT sont autonomes et transportent les informations d'authentification de l'utilisateur directement dans la charge utile du jeton, ce qui réduit l'utilisation de la mémoire du serveur et augmente l'évolutivité.

Support multiplateforme
Les jetons sont faciles à utiliser dans les applications mobiles et Web car ils peuvent être stockés en toute sécurité dans le client (par exemple, dans un stockage local ou dans des cookies).

Sécurité
Chaque jeton est signé numériquement, garantissant son intégrité et permettant au serveur de le vérifier sans interroger une base de données à chaque demande.

Ce que vous apprendrez

Dans ce tutoriel, vous apprendrez à :

  1. Configurez une application Spring Boot avec Spring Security.
  2. Implémentez l'authentification basée sur les jetons JWT à l'aide du modèle JDBC pour gérer les utilisateurs et stocker les jetons d'actualisation en toute sécurité.
  3. Configurez les points de terminaison pour la connexion, la génération de jetons d'accès et la gestion des jetons d'actualisation.

À la fin de ce didacticiel, vous disposerez d'un système d'authentification sécurisé et sans état qui exploite Spring Boot et JWT pour fournir un contrôle d'accès transparent et évolutif pour vos applications.


Flux API :

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template


Exigences technologiques :

  • Java 17/11/8
  • Spring Boot 3/2 (avec Spring Security, Spring Web)
  • jjwt-api 0.11.5
  • PostgreSQL/MySQL
  • Maven

1. Configurez votre projet Spring Boot

Utilisez l'outil Web Spring ou votre outil de développement (STS, Intellij ou tout autre IDE) pour créer un projet Spring Boot.

ouvrez pom.xml et ajoutez des dépendances pour Spring Security, JWT et le modèle JDBC :

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>

2. Configurer la base de données, les propriétés de l'application

Sous le dossier src/main/resources, ouvrez application.properties et ajoutez les configurations ci-dessous. J'utiliserai la base de données Postgres pour ce tutoriel.

spring.application.name= authmanager

server.port= 1001
servlet.context-path= /authmanager

database.username= postgres 
database.password= admin@123
database.driverClassName= org.postgresql.Driver
database.jdbcUrl= jdbc:postgresql://localhost:5432/postgres
database.maxActive= 5
database.minIdle= 5
database.poolName= Authmanager Postgres Datasource

app.jwtSecret= ###############ib-Spring###############
app.jwtExpirationMs= 3600000
app.jwtRefreshExpirationMs= 86400000

3. Configurer les tables de base de données

Définissez une structure de tableau simple pour les informations utilisateur, les rôles, le mappage des rôles utilisateur et les jetons d'actualisation :

CREATE SCHEMA IB;
-------------------------------------------------------------------------

create sequence users_uniqueid_seq START 1;

create table ib.users(
        uniqueid bigint not null default nextval('users_uniqueid_seq') PRIMARY KEY,
        email varchar(75),
        password varchar(200),
        username varchar(20)
);

insert into ib.users(email,password,username) values ('admin@ib.com','a$VcdzH8Q.o4KEo6df.XesdOmXdXQwT5ugNQvu1Pl0390rmfOeA1bhS','admin');

#(password = 12345678)
-------------------------------------------------------------------------

create sequence roles_id_seq START 1;

create table ib.roles(
        id int not null default nextval('roles_id_seq') PRIMARY KEY,
        name varchar(20)
);

INSERT INTO ib.roles(name) VALUES('ROLE_USER');
INSERT INTO ib.roles(name) VALUES('ROLE_MODERATOR');
INSERT INTO ib.roles(name) VALUES('ROLE_ADMIN');

-------------------------------------------------------------------------

create table ib.user_roles(
        user_uniqueid bigint not null,
        role_id int not null,
        primary key(user_uniqueid,role_id)
);
insert into ib.user_roles (user_uniqueid,role_id) values (1,3);
-------------------------------------------------------------------------

create sequence refresh_tokens_id_seq START 1;

create table ib.refresh_tokens(
        id bigint not null default nextval('refresh_tokens_id_seq') PRIMARY KEY,
        uniqueid bigint,
        token varchar(500) not null,
        expiryDate TIMESTAMP WITH TIME ZONE not null
);

-------------------------------------------------------------------------

4. Créez les modèles

Définissons les modèles suivants.
Dans le package models, créez ci-dessous 4 fichiers :

model/ERole.java

package com.security.authmanager.model;

public enum ERole {
    ROLE_USER,
    ROLE_MODERATOR,
    ROLE_ADMIN
}

modèle/Rôle.java

package com.security.authmanager.model;

public class Role {
    private Integer id;
    private ERole name;

    public Role() {
    }

    public Role(ERole name) {
        this.name = name;
    }

    //generate getters and setters
}

modèle/Utilisateur.java

package com.security.authmanager.model;

import java.util.HashSet;
import java.util.Set;

public class User {
    private Long id;
    private String username;
    private String email;
    private String password;

    private Set<Role> roles = new HashSet<>();

    public User() {
    }

    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    //generate getters and setters
}

modèle/RefreshToken.java

package com.security.authmanager.model;

import java.util.HashSet;
import java.util.Set;

public class User {
    private Long id;
    private String username;
    private String email;
    private String password;

    private Set<Role> roles = new HashSet<>();

    public User() {
    }

    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    //generate getters and setters
}

5. Implémenter la classe CustomUserDetailsRepository

La classe CustomUserDetailsRepository est un Spring @Repository qui gère les opérations de base de données personnalisées liées aux entités User et Role. Il utilise JdbcTemplate pour exécuter des requêtes SQL pour des tâches telles que récupérer des utilisateurs, vérifier si un utilisateur existe par nom d'utilisateur ou e-mail, créer de nouveaux utilisateurs et récupérer des rôles.

package com.security.authmanager.repository;

import com.security.authmanager.common.QueryConstants;
import com.security.authmanager.model.ERole;
import com.security.authmanager.model.Role;
import com.security.authmanager.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

@Repository
public class CustomUserDetailsRepository {

    private static final Logger logger = LoggerFactory.getLogger(CustomUserDetailsRepository.class);
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public User fetchUserByUserName(String userName){
        try{
            return jdbcTemplate.query((conn) ->{
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FETCH_USER);
                ps.setString(1, userName.toUpperCase());
                return ps;
            },rs->{
                User user = null;
                Set<Role> roles = new HashSet<>();
                while (rs.next()) {
                    if (user == null) {
                        user = new User();
                        user.setEmail(rs.getString("email"));
                        user.setId(rs.getLong("uniqueid"));
                        user.setPassword(rs.getString("password"));
                        user.setUsername(rs.getString("username"));
                    }
                    Role role = new Role();
                    role.setId(rs.getInt("id"));
                    role.setName(ERole.valueOf(rs.getString("name")));
                    roles.add(role);
                }
                if (user != null) {
                    user.setRoles(roles);
                }
                return user;
            });
        }catch(Exception e){
            logger.error("Exception in fetchUserByUserName()",e);
            throw new RuntimeException(e);
        }
    }

    public boolean existsByUsername(String userName) {
        try{
              return jdbcTemplate.query((conn) -> {
                 final PreparedStatement ps = conn.prepareStatement(QueryConstants.CHECK_USER_BY_USERNAME);
                 ps.setString(1, userName.toUpperCase());
                 return ps;
             }, (rs,rownum) -> rs.getInt("count")).get(0)>0;
        }catch(Exception e){
            logger.error("Exception in existsByUsername()",e);
            throw new RuntimeException(e);
        }
    }

    public boolean existsByEmail(String email) {
        try{
            return jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.CHECK_USER_BY_EMAIL);
                ps.setString(1, email.toUpperCase());
                return ps;
            }, (rs,rownum) -> rs.getInt("count")).get(0)>0;
        }catch(Exception e){
            logger.error("Exception in existsByEmail()",e);
            throw new RuntimeException(e);
        }
    }

    public Role findRoleByName(ERole eRole) {
        try{
            return jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FETCH_ROLE_BY_NAME);
                ps.setString(1, String.valueOf(eRole));
                return ps;
            }, rs -> {
                Role role=null;
                while(rs.next()){
                    role = new Role();
                    role.setName(ERole.valueOf(rs.getString("name")));
                    role.setId(rs.getInt("id"));
                }
                return role;
            });
        }catch(Exception e){
            logger.error("Exception in findRoleByName()",e);
            throw new RuntimeException(e);
        }
    }

    public void createUser(User user) {
        try(Connection conn = Objects.requireNonNull(jdbcTemplate.getDataSource()).getConnection()){
            try (PreparedStatement userStatement = conn.prepareStatement(QueryConstants.INSERT_TO_USERS,Statement.RETURN_GENERATED_KEYS)) {
                userStatement.setString(1, user.getEmail().toUpperCase());
                userStatement.setString(2, user.getPassword());
                userStatement.setString(3, user.getUsername().toUpperCase());
                userStatement.executeUpdate();
                // Retrieve generated userId
                try (ResultSet generatedKeys = userStatement.getGeneratedKeys()) {
                    if (generatedKeys.next()) {
                        Long userId = generatedKeys.getLong(1); // Assuming userId is of type VARCHAR
                        logger.info("gen userid {}",userId.toString());
                        user.setId(userId);
                    }
                }
            }
            if (user.getRoles() != null && !user.getRoles().isEmpty()) {
                try (PreparedStatement userRoleStatement = conn.prepareStatement(QueryConstants.INSERT_TO_USER_ROLES)) {
                    for (Role role : user.getRoles()) {
                        userRoleStatement.setLong(1, user.getId());
                        userRoleStatement.setLong(2, role.getId());
                        userRoleStatement.executeUpdate();
                    }
                }
            }
        }catch(Exception e){
            logger.error("Exception in existsByEmail()",e);
            throw new RuntimeException(e);
        }
    }
}

Ce référentiel effectue des opérations CRUD personnalisées basées sur SQL pour gérer les données d'utilisateur et de rôle dans la base de données.

Fonctions clés :

  • Récupérez les utilisateurs par nom d'utilisateur et récupérez les rôles associés.
  • Vérifiez si un utilisateur existe par nom d'utilisateur ou par e-mail.
  • Insérez les nouveaux utilisateurs et leurs rôles dans la base de données.
  • Récupérez les détails du rôle en fonction des noms de rôle.

6. Implémenter la classe RefreshTokenRepository

La classe RefreshTokenRepository est un Spring @Repository qui gère les opérations de base de données liées à l'entité RefreshToken. Il utilise le JdbcTemplate de Spring pour interagir avec la base de données via des requêtes SQL brutes, encapsulant la logique d'enregistrement, de suppression et de récupération des jetons d'actualisation.

package com.security.authmanager.repository;

import com.security.authmanager.common.QueryConstants;
import com.security.authmanager.model.ERole;
import com.security.authmanager.model.RefreshToken;
import com.security.authmanager.model.Role;
import com.security.authmanager.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.sql.PreparedStatement;
import java.sql.Timestamp;
import java.util.Optional;

@Repository
public class RefreshTokenRepository {
    private static final Logger logger = LoggerFactory.getLogger(RefreshTokenRepository.class);
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void deleteRefreshToken(RefreshToken refreshToken) {
        try{
            jdbcTemplate.update(QueryConstants.DELETE_REFRESH_TOKEN,(final PreparedStatement ps) ->{
                ps.setString(1,refreshToken.getToken());
            });
        }catch (Exception e){
            logger.error("Exception in deleteRefreshToken()",e);
            throw new RuntimeException(e);
        }
    }

    public int deleteRefreshTokenByUser(User user) {
        return 0;
    }

    public RefreshToken saveRefreshToken(RefreshToken refreshToken) {
        try{
            jdbcTemplate.update(QueryConstants.SAVE_REFRESH_TOKEN,(final PreparedStatement ps) ->{
                ps.setLong(1,refreshToken.getUser().getId());
                ps.setString(2,refreshToken.getToken());
                ps.setTimestamp(3, Timestamp.from(refreshToken.getExpiryDate()));
            });
        }catch (Exception e){
            logger.error("Exception in saveRefreshToken()",e);
            throw new RuntimeException(e);
        }
        return refreshToken;
    }

    public Optional<RefreshToken> findByToken(String token) {
        RefreshToken refreshToken = new RefreshToken();
        try{
            return Optional.ofNullable(jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FIND_BY_TOKEN);
                ps.setString(1, token);
                return ps;
            }, rs -> {
                User user = new User();
                while (rs.next()) {
                    refreshToken.setId(rs.getLong("id"));
                    refreshToken.setToken(rs.getString("token"));
                    refreshToken.setExpiryDate(rs.getTimestamp("expiryDate").toInstant());
                    user.setId(rs.getLong("uniqueid"));
                    user.setEmail(rs.getString("email"));
                    user.setUsername(rs.getString("username"));
                    refreshToken.setUser(user);
                }
                return refreshToken;
            }));
        }catch(Exception e){
            logger.error("Exception in findByToken()",e);
            throw new RuntimeException(e);
        }
    }
}

Ce référentiel interagit directement avec la base de données pour les opérations CRUD sur les entités RefreshToken.

Fonctions clés :

  • Enregistrez les jetons d'actualisation.
  • Récupérez les jetons d'actualisation par valeur de jeton.
  • Supprimez les jetons d'actualisation par jeton ou par utilisateur.

7. Configurer la sécurité Spring

  • WebSecurityConfig configure Spring Security pour utiliser l'authentification par jeton basée sur JWT.

  • Il définit des beans pour divers composants nécessaires à l'authentification comme AuthTokenFilter (pour gérer les jetons JWT), DaoAuthenticationProvider (pour récupérer les détails de l'utilisateur et valider les mots de passe) et BCryptPasswordEncoder (pour hacher et comparer les mots de passe).

SecurityFilterChain configure la façon dont les requêtes HTTP entrantes sont sécurisées :
- Permet d'accéder à certaines routes publiques (/auth/**, /test/**).
- Protège toutes les autres routes, nécessitant une authentification.
- Désactive la gestion des sessions (rendant le système apatride).
- Configure un filtre pour intercepter et traiter les jetons JWT pour
authentification de l'utilisateur.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>

8. Implémenter UserDetailsImpl

Cette classe est principalement utilisée dans Spring Security pour représenter l'utilisateur actuellement authentifié.

Lorsqu'un utilisateur tente de se connecter :

  1. Spring Security appelle UserDetailsService.loadUserByUsername() pour charger l'utilisateur de la base de données.
  2. Il crée une instance de UserDetailsImpl avec les détails de l'utilisateur (y compris leurs rôles).
  3. Cet objet UserDetailsImpl est ensuite utilisé par Spring Security pour authentifier l'utilisateur et vérifier ses autorisations tout au long du processus séance.

UserDetailsImpl est un pont entre votre entité utilisateur et les mécanismes internes d'authentification et d'autorisation de Spring Security.

spring.application.name= authmanager

server.port= 1001
servlet.context-path= /authmanager

database.username= postgres 
database.password= admin@123
database.driverClassName= org.postgresql.Driver
database.jdbcUrl= jdbc:postgresql://localhost:5432/postgres
database.maxActive= 5
database.minIdle= 5
database.poolName= Authmanager Postgres Datasource

app.jwtSecret= ###############ib-Spring###############
app.jwtExpirationMs= 3600000
app.jwtRefreshExpirationMs= 86400000

Ci-dessous les requêtes : (QueryConstants.java)

CREATE SCHEMA IB;
-------------------------------------------------------------------------

create sequence users_uniqueid_seq START 1;

create table ib.users(
        uniqueid bigint not null default nextval('users_uniqueid_seq') PRIMARY KEY,
        email varchar(75),
        password varchar(200),
        username varchar(20)
);

insert into ib.users(email,password,username) values ('admin@ib.com','a$VcdzH8Q.o4KEo6df.XesdOmXdXQwT5ugNQvu1Pl0390rmfOeA1bhS','admin');

#(password = 12345678)
-------------------------------------------------------------------------

create sequence roles_id_seq START 1;

create table ib.roles(
        id int not null default nextval('roles_id_seq') PRIMARY KEY,
        name varchar(20)
);

INSERT INTO ib.roles(name) VALUES('ROLE_USER');
INSERT INTO ib.roles(name) VALUES('ROLE_MODERATOR');
INSERT INTO ib.roles(name) VALUES('ROLE_ADMIN');

-------------------------------------------------------------------------

create table ib.user_roles(
        user_uniqueid bigint not null,
        role_id int not null,
        primary key(user_uniqueid,role_id)
);
insert into ib.user_roles (user_uniqueid,role_id) values (1,3);
-------------------------------------------------------------------------

create sequence refresh_tokens_id_seq START 1;

create table ib.refresh_tokens(
        id bigint not null default nextval('refresh_tokens_id_seq') PRIMARY KEY,
        uniqueid bigint,
        token varchar(500) not null,
        expiryDate TIMESTAMP WITH TIME ZONE not null
);

-------------------------------------------------------------------------

9. Implémenter UserDetailsServiceImpl

La classe UserDetailsServiceImpl agit comme un pont entre la base de données de votre application et le processus d'authentification de Spring Security. Il récupère les détails de l'utilisateur de la base de données à l'aide de CustomUserDetailsRepository, convertit l'objet User en UserDetailsImpl (un format compatible avec Spring Security) et gère les cas où un utilisateur n'est pas trouvé en lançant une exception. Ce service permet à Spring Security d'authentifier les utilisateurs et de gérer les autorisations en fonction des rôles et des autorisations de l'utilisateur.

package com.security.authmanager.model;

public enum ERole {
    ROLE_USER,
    ROLE_MODERATOR,
    ROLE_ADMIN
}

10. Filtrer la demande

La classe AuthTokenFilter étend OncePerRequestFilter de Spring, ce qui en fait un filtre qui traite chaque requête HTTP une fois dans une chaîne de requêtes. Son rôle principal est d'extraire et de valider un JWT (JSON Web Token) de la requête et de définir l'authentification de l'utilisateur dans SecurityContext de Spring Security si le jeton est valide.

package com.security.authmanager.model;

public class Role {
    private Integer id;
    private ERole name;

    public Role() {
    }

    public Role(ERole name) {
        this.name = name;
    }

    //generate getters and setters
}

Chaque fois qu'une demande est faite :

  • Le filtre vérifie si la requête contient un JWT valide.
  • S'il est valide, il authentifie l'utilisateur en définissant le UsernamePasswordAuthenticationToken dans SecurityContext.
  • Si le JWT est invalide ou manquant, aucune authentification n'est définie et la demande est traitée comme une demande non authentifiée.

Ce filtre garantit que toutes les requêtes contenant des jetons JWT valides sont authentifiées automatiquement, sans que l'utilisateur ait besoin de fournir des informations d'identification (comme un nom d'utilisateur/un mot de passe) dans les requêtes ultérieures après la connexion.

11. Implémenter RefreshTokenService

La classe RefreshTokenService fournit des services liés à la gestion des jetons d'actualisation dans un système d'authentification basé sur des jetons. Les jetons d'actualisation sont utilisés pour obtenir de nouveaux jetons JWT après l'expiration du JWT initial sans que l'utilisateur ne se ré-authentifie.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>

RefreshTokenService gère la création, la validation et la suppression des jetons d'actualisation. Il utilise des référentiels pour enregistrer et récupérer les jetons et les utilisateurs de la base de données.

Ce service est un élément essentiel d'un système d'authentification dans lequel des jetons d'actualisation sont utilisés pour maintenir les utilisateurs connectés sans leur demander de fournir à nouveau leurs informations d'identification après l'expiration du JWT.

12. Implémenter l'utilitaire JWT

La classe JwtUtils est une classe utilitaire qui gère la création, l'analyse et la validation de JWT (JSON Web Token) à des fins d'authentification dans une application Spring Boot. Il utilise la bibliothèque jjwt pour travailler avec les JWT.

spring.application.name= authmanager

server.port= 1001
servlet.context-path= /authmanager

database.username= postgres 
database.password= admin@123
database.driverClassName= org.postgresql.Driver
database.jdbcUrl= jdbc:postgresql://localhost:5432/postgres
database.maxActive= 5
database.minIdle= 5
database.poolName= Authmanager Postgres Datasource

app.jwtSecret= ###############ib-Spring###############
app.jwtExpirationMs= 3600000
app.jwtRefreshExpirationMs= 86400000

La classe JwtUtils est responsable de la génération, de l'analyse et de la validation des jetons JWT. Il signe en toute sécurité les jetons à l'aide d'une clé secrète (HMAC-SHA256) et garantit que les jetons ne peuvent être lus ou vérifiés que par les parties possédant la clé secrète correcte.

La classe extrait également le nom d'utilisateur du jeton et vérifie si le jeton est valide avant d'accorder l'accès à l'utilisateur. Cet utilitaire est essentiel pour maintenir une authentification sécurisée basée sur des jetons dans votre application.

13. Gérer l'exception d'authentification

La classe AuthEntryPointJwt implémente l'interface AuthenticationEntryPoint de Spring Security. Il gère ce qui se passe lorsqu'une demande non autorisée est effectuée, généralement lorsqu'un utilisateur tente d'accéder à une ressource protégée sans authentification valide (par exemple, pas de JWT ou un JWT non valide).

CREATE SCHEMA IB;
-------------------------------------------------------------------------

create sequence users_uniqueid_seq START 1;

create table ib.users(
        uniqueid bigint not null default nextval('users_uniqueid_seq') PRIMARY KEY,
        email varchar(75),
        password varchar(200),
        username varchar(20)
);

insert into ib.users(email,password,username) values ('admin@ib.com','a$VcdzH8Q.o4KEo6df.XesdOmXdXQwT5ugNQvu1Pl0390rmfOeA1bhS','admin');

#(password = 12345678)
-------------------------------------------------------------------------

create sequence roles_id_seq START 1;

create table ib.roles(
        id int not null default nextval('roles_id_seq') PRIMARY KEY,
        name varchar(20)
);

INSERT INTO ib.roles(name) VALUES('ROLE_USER');
INSERT INTO ib.roles(name) VALUES('ROLE_MODERATOR');
INSERT INTO ib.roles(name) VALUES('ROLE_ADMIN');

-------------------------------------------------------------------------

create table ib.user_roles(
        user_uniqueid bigint not null,
        role_id int not null,
        primary key(user_uniqueid,role_id)
);
insert into ib.user_roles (user_uniqueid,role_id) values (1,3);
-------------------------------------------------------------------------

create sequence refresh_tokens_id_seq START 1;

create table ib.refresh_tokens(
        id bigint not null default nextval('refresh_tokens_id_seq') PRIMARY KEY,
        uniqueid bigint,
        token varchar(500) not null,
        expiryDate TIMESTAMP WITH TIME ZONE not null
);

-------------------------------------------------------------------------

La classe AuthEntryPointJwt est un point d'entrée personnalisé qui intercepte les tentatives d'accès non autorisées et renvoie une réponse JSON structurée avec un code d'erreur 401, un message d'erreur et des détails sur la demande.
Il enregistre l'erreur et fournit une réponse claire et conviviale aux clients lorsque l'authentification échoue.

14. Créer une classe de charge utile pour le contrôleur

Vous trouverez ci-dessous les charges utiles de nos RestAPI :

1. Demandes :

- LoginRequest.java :

package com.security.authmanager.model;

public enum ERole {
    ROLE_USER,
    ROLE_MODERATOR,
    ROLE_ADMIN
}

- SignupRequest.java :

package com.security.authmanager.model;

public class Role {
    private Integer id;
    private ERole name;

    public Role() {
    }

    public Role(ERole name) {
        this.name = name;
    }

    //generate getters and setters
}

- TokenRefreshRequest.java :

package com.security.authmanager.model;

import java.util.HashSet;
import java.util.Set;

public class User {
    private Long id;
    private String username;
    private String email;
    private String password;

    private Set<Role> roles = new HashSet<>();

    public User() {
    }

    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    //generate getters and setters
}

2. Réponses :

- JwtResponse.java

package com.security.authmanager.model;

import java.util.HashSet;
import java.util.Set;

public class User {
    private Long id;
    private String username;
    private String email;
    private String password;

    private Set<Role> roles = new HashSet<>();

    public User() {
    }

    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    //generate getters and setters
}

- MessageResponse.java

package com.security.authmanager.repository;

import com.security.authmanager.common.QueryConstants;
import com.security.authmanager.model.ERole;
import com.security.authmanager.model.Role;
import com.security.authmanager.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

@Repository
public class CustomUserDetailsRepository {

    private static final Logger logger = LoggerFactory.getLogger(CustomUserDetailsRepository.class);
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public User fetchUserByUserName(String userName){
        try{
            return jdbcTemplate.query((conn) ->{
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FETCH_USER);
                ps.setString(1, userName.toUpperCase());
                return ps;
            },rs->{
                User user = null;
                Set<Role> roles = new HashSet<>();
                while (rs.next()) {
                    if (user == null) {
                        user = new User();
                        user.setEmail(rs.getString("email"));
                        user.setId(rs.getLong("uniqueid"));
                        user.setPassword(rs.getString("password"));
                        user.setUsername(rs.getString("username"));
                    }
                    Role role = new Role();
                    role.setId(rs.getInt("id"));
                    role.setName(ERole.valueOf(rs.getString("name")));
                    roles.add(role);
                }
                if (user != null) {
                    user.setRoles(roles);
                }
                return user;
            });
        }catch(Exception e){
            logger.error("Exception in fetchUserByUserName()",e);
            throw new RuntimeException(e);
        }
    }

    public boolean existsByUsername(String userName) {
        try{
              return jdbcTemplate.query((conn) -> {
                 final PreparedStatement ps = conn.prepareStatement(QueryConstants.CHECK_USER_BY_USERNAME);
                 ps.setString(1, userName.toUpperCase());
                 return ps;
             }, (rs,rownum) -> rs.getInt("count")).get(0)>0;
        }catch(Exception e){
            logger.error("Exception in existsByUsername()",e);
            throw new RuntimeException(e);
        }
    }

    public boolean existsByEmail(String email) {
        try{
            return jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.CHECK_USER_BY_EMAIL);
                ps.setString(1, email.toUpperCase());
                return ps;
            }, (rs,rownum) -> rs.getInt("count")).get(0)>0;
        }catch(Exception e){
            logger.error("Exception in existsByEmail()",e);
            throw new RuntimeException(e);
        }
    }

    public Role findRoleByName(ERole eRole) {
        try{
            return jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FETCH_ROLE_BY_NAME);
                ps.setString(1, String.valueOf(eRole));
                return ps;
            }, rs -> {
                Role role=null;
                while(rs.next()){
                    role = new Role();
                    role.setName(ERole.valueOf(rs.getString("name")));
                    role.setId(rs.getInt("id"));
                }
                return role;
            });
        }catch(Exception e){
            logger.error("Exception in findRoleByName()",e);
            throw new RuntimeException(e);
        }
    }

    public void createUser(User user) {
        try(Connection conn = Objects.requireNonNull(jdbcTemplate.getDataSource()).getConnection()){
            try (PreparedStatement userStatement = conn.prepareStatement(QueryConstants.INSERT_TO_USERS,Statement.RETURN_GENERATED_KEYS)) {
                userStatement.setString(1, user.getEmail().toUpperCase());
                userStatement.setString(2, user.getPassword());
                userStatement.setString(3, user.getUsername().toUpperCase());
                userStatement.executeUpdate();
                // Retrieve generated userId
                try (ResultSet generatedKeys = userStatement.getGeneratedKeys()) {
                    if (generatedKeys.next()) {
                        Long userId = generatedKeys.getLong(1); // Assuming userId is of type VARCHAR
                        logger.info("gen userid {}",userId.toString());
                        user.setId(userId);
                    }
                }
            }
            if (user.getRoles() != null && !user.getRoles().isEmpty()) {
                try (PreparedStatement userRoleStatement = conn.prepareStatement(QueryConstants.INSERT_TO_USER_ROLES)) {
                    for (Role role : user.getRoles()) {
                        userRoleStatement.setLong(1, user.getId());
                        userRoleStatement.setLong(2, role.getId());
                        userRoleStatement.executeUpdate();
                    }
                }
            }
        }catch(Exception e){
            logger.error("Exception in existsByEmail()",e);
            throw new RuntimeException(e);
        }
    }
}

- TokenRefreshResponse.java

package com.security.authmanager.repository;

import com.security.authmanager.common.QueryConstants;
import com.security.authmanager.model.ERole;
import com.security.authmanager.model.RefreshToken;
import com.security.authmanager.model.Role;
import com.security.authmanager.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.sql.PreparedStatement;
import java.sql.Timestamp;
import java.util.Optional;

@Repository
public class RefreshTokenRepository {
    private static final Logger logger = LoggerFactory.getLogger(RefreshTokenRepository.class);
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void deleteRefreshToken(RefreshToken refreshToken) {
        try{
            jdbcTemplate.update(QueryConstants.DELETE_REFRESH_TOKEN,(final PreparedStatement ps) ->{
                ps.setString(1,refreshToken.getToken());
            });
        }catch (Exception e){
            logger.error("Exception in deleteRefreshToken()",e);
            throw new RuntimeException(e);
        }
    }

    public int deleteRefreshTokenByUser(User user) {
        return 0;
    }

    public RefreshToken saveRefreshToken(RefreshToken refreshToken) {
        try{
            jdbcTemplate.update(QueryConstants.SAVE_REFRESH_TOKEN,(final PreparedStatement ps) ->{
                ps.setLong(1,refreshToken.getUser().getId());
                ps.setString(2,refreshToken.getToken());
                ps.setTimestamp(3, Timestamp.from(refreshToken.getExpiryDate()));
            });
        }catch (Exception e){
            logger.error("Exception in saveRefreshToken()",e);
            throw new RuntimeException(e);
        }
        return refreshToken;
    }

    public Optional<RefreshToken> findByToken(String token) {
        RefreshToken refreshToken = new RefreshToken();
        try{
            return Optional.ofNullable(jdbcTemplate.query((conn) -> {
                final PreparedStatement ps = conn.prepareStatement(QueryConstants.FIND_BY_TOKEN);
                ps.setString(1, token);
                return ps;
            }, rs -> {
                User user = new User();
                while (rs.next()) {
                    refreshToken.setId(rs.getLong("id"));
                    refreshToken.setToken(rs.getString("token"));
                    refreshToken.setExpiryDate(rs.getTimestamp("expiryDate").toInstant());
                    user.setId(rs.getLong("uniqueid"));
                    user.setEmail(rs.getString("email"));
                    user.setUsername(rs.getString("username"));
                    refreshToken.setUser(user);
                }
                return refreshToken;
            }));
        }catch(Exception e){
            logger.error("Exception in findByToken()",e);
            throw new RuntimeException(e);
        }
    }
}

15. Créer des classes de contrôleur d'API Rest

- AuthController.java

La classe AuthController est un Spring @RestController responsable de la gestion des points de terminaison liés à l'authentification dans l'application. Il fournit des points de terminaison pour les opérations de connexion, d'enregistrement et d'actualisation des jetons des utilisateurs.

Fonctions clés :

  • Connexion de l'utilisateur avec JWT et génération de jetons d'actualisation.
  • Actualisation des jetons à l'aide de jetons d'actualisation valides.
  • Inscription des utilisateurs avec validation et attribution des rôles.
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>

- TestController.java

La classe TestController est un Spring @RestController qui fournit plusieurs points de terminaison pour tester le contrôle d'accès en fonction des rôles d'utilisateur. Il montre comment utiliser l'autorisation basée sur les rôles de Spring Security pour restreindre l'accès à certaines parties de l'application.

Fonctions clés :

  • Point de terminaison d'accès public auquel tout le monde peut accéder.
  • Points de terminaison spécifiques à un rôle qui restreignent l'accès en fonction des rôles d'utilisateur (UTILISATEUR, MODÉRATEUR, ADMIN).
spring.application.name= authmanager

server.port= 1001
servlet.context-path= /authmanager

database.username= postgres 
database.password= admin@123
database.driverClassName= org.postgresql.Driver
database.jdbcUrl= jdbc:postgresql://localhost:5432/postgres
database.maxActive= 5
database.minIdle= 5
database.poolName= Authmanager Postgres Datasource

app.jwtSecret= ###############ib-Spring###############
app.jwtExpirationMs= 3600000
app.jwtRefreshExpirationMs= 86400000

16. Tester les API

1. Inscrivez-vous en tant que mod et utilisateur. (Connexion)

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template

2. Connectez-vous pour obtenir un jeton d'accès.

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template

3. Obtenez l'API de jeton d'actualisation.

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template

4. Tester l'accès de l'utilisateur en transmettant le jeton d'accès.

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template

5. Tester l'accès au mod en passant le jeton d'accès.

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template

6. Tester l'accès administrateur en passant le même jeton d'accès.

Implementing Token-Based Authentication in Spring Boot Using Spring Security, JWT, and JDBC Template

Non autorisé car cet utilisateur n'a pas d'accès administrateur (l'utilisateur n'a que des rôles de mod et d'utilisateur)

Bon apprentissage ! On se reverra.?

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn