Maison  >  Article  >  Java  >  Sécurité Spring avec JWT

Sécurité Spring avec JWT

WBOY
WBOYoriginal
2024-08-16 06:35:32954parcourir

Spring Security with JWT

Dans cet article, nous explorerons comment intégrer Spring Security à JWT pour créer une couche de sécurité solide pour votre application. Nous passerons en revue chaque étape, de la configuration de base à la mise en œuvre d'un filtre d'authentification personnalisé, en veillant à ce que vous disposiez des outils nécessaires pour protéger vos API de manière efficace et à grande échelle.

Configuration

Au Spring Initializr, nous allons construire un projet avec Java 21, Maven, Jar et ces dépendances :

  • Données de printemps JPA
  • Web de printemps
  • Lombok
  • Sécurité du printemps
  • Pilote PostgreSQL
  • Serveur de ressources OAuth2

Configurer la base de données PostgreSQL

Avec Docker vous allez créer une base de données PostgreSql avec Docker-compose.
Créez un fichier docker-compose.yaml à la racine de votre projet.

services:
  postgre:
    image: postgres:latest
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_DB=database
      - POSTGRES_USER=admin
      - POSTGRES_PASSWORD=admin
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Exécutez la commande pour démarrer le conteneur.

docker compose up -d

Configurer le fichier application.properties

Ce fichier est la configuration de l'application Spring Boot.

spring.datasource.url=jdbc:postgresql://localhost:5432/database
spring.datasource.username=admin
spring.datasource.password=admin

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

jwt.public.key=classpath:public.key
jwt.private.key=classpath:private.key

Les jwt.public.key et jwt.private.key sont des clés que nous allons créer davantage.

Générer les clés privées et publiques

NE JAMAIS valider ces clés sur votre github

Exécuter sur la console pour générer la clé privée dans le répertoire des ressources

cd src/main/resources
openssl genrsa > private.key

Après, créez la clé publique liée à la clé privée.

openssl rsa -in private.key -pubout -out public.key 

Code

Créer un fichier SecurityConfig

Plus près de la fonction principale, créez un répertoire de configuration et à l'intérieur de celui-ci un fichier SecurityConfig.java.

import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.web.SecurityFilterChain;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Value("${jwt.public.key}")
    private RSAPublicKey publicKey;

    @Value("${jwt.private.key}")
    private RSAPrivateKey privateKey;

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth.requestMatchers(HttpMethod.POST, "/signin").permitAll()
                        .requestMatchers(HttpMethod.POST, "/login").permitAll()
                        .anyRequest().authenticated())
                .oauth2ResourceServer(config -> config.jwt(jwt -> jwt.decoder(jwtDecoder())));

        return http.build();
    }

    @Bean
    BCryptPasswordEncoder bPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    JwtEncoder jwtEncoder() {
        var jwk = new RSAKey.Builder(this.publicKey).privateKey(this.privateKey).build();

        var jwks = new ImmutableJWKSet<>(new JWKSet(jwk));

        return new NimbusJwtEncoder(jwks);
    }

    @Bean
    JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withPublicKey(publicKey).build();
    }
}

Explication

  • @EnableWebScurity : lorsque vous utilisez @EnableWebSecurity, il déclenche automatiquement la configuration de Spring Security pour sécuriser les applications Web. Cette configuration comprend la configuration de filtres, la sécurisation des points de terminaison et l'application de diverses règles de sécurité.

  • @EnableMethodSecurity : est une annotation dans Spring Security qui active la sécurité au niveau de la méthode dans votre application Spring. Il vous permet d'appliquer des règles de sécurité directement au niveau de la méthode à l'aide d'annotations telles que @PreAuthorize, @PostAuthorize, @Secured et @RolesAllowed.

  • privateKey et publicKey : sont les clés publiques et privées RSA utilisées pour signer et vérifier les JWT. L'annotation @Value injecte les clés du fichier de propriétés (application.properties) dans ces champs.

  • CSRF : désactive la protection CSRF (Cross-Site Request Forgery), qui est souvent désactivée dans les API REST sans état où JWT est utilisé pour l'authentification.

  • authorizeHttpRequests : configure les règles d'autorisation basées sur les URL.

    • requestMatchers(HttpMethod.POST, "/signin").permitAll() : permet un accès non authentifié aux points de terminaison /signin et /login, ce qui signifie que n'importe qui peut accéder à ces routes sans être connecté.
    • anyRequest().authenticated() : nécessite une authentification pour toutes les autres demandes.
  • oauth2ResourceServer : configure l'application en tant que serveur de ressources OAuth 2.0 qui utilise JWT pour l'authentification.

    • config.jwt(jwt -> jwt.decoder(jwtDecoder())) : Spécifie le bean décodeur JWT (jwtDecoder) qui sera utilisé pour décoder et valider les jetons JWT.
  • BCryptPasswordEncoder : Ce bean définit un encodeur de mot de passe qui utilise l'algorithme de hachage BCrypt pour encoder les mots de passe. BCrypt est un choix populaire pour stocker en toute sécurité les mots de passe en raison de sa nature adaptative, ce qui le rend résistant aux attaques par force brute.

  • JwtEncoder : ce bean est responsable de l'encodage (signature) des jetons JWT.

    • RSAKey.Builder : crée une nouvelle clé RSA à l'aide des clés RSA publiques et privées fournies.
    • ImmutableJWKSet<>(new JWKSet(jwk)) : encapsule la clé RSA dans un jeu de clés Web JSON (JWKSet), la rendant immuable.
    • NimbusJwtEncoder(jwks) : utilise la bibliothèque Nimbus pour créer un encodeur JWT qui signera les jetons avec la clé privée RSA.
  • JwtDecoder : ce bean est responsable du décodage (vérification) des jetons JWT.

    • NimbusJwtDecoder.withPublicKey(publicKey).build() : crée un décodeur JWT à l'aide de la clé publique RSA, qui est utilisée pour vérifier la signature des jetons JWT.

Entité

import org.springframework.security.crypto.password.PasswordEncoder;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(name = "tb_clients")
@Getter
@Setter
@NoArgsConstructor
public class ClientEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "client_id")
    private Long clientId;

    private String name;

    @Column(unique = true)
    private String cpf;

    @Column(unique = true)
    private String email;

    private String password;

    @Column(name = "user_type")
    private String userType = "client";

    public Boolean isLoginCorrect(String password, PasswordEncoder passwordEncoder) {
        return passwordEncoder.matches(password, this.password);
    }
}

Dépôt

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import example.com.challengePicPay.entities.ClientEntity;

@Repository
public interface ClientRepository extends JpaRepository<ClientEntity, Long> {
    Optional<ClientEntity> findByEmail(String email);

    Optional<ClientEntity> findByCpf(String cpf);

    Optional<ClientEntity> findByEmailOrCpf(String email, String cpf);
}

Services

Service client

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import example.com.challengePicPay.entities.ClientEntity;
import example.com.challengePicPay.repositories.ClientRepository;

@Service
public class ClientService {

    @Autowired
    private ClientRepository clientRepository;

    @Autowired
    private BCryptPasswordEncoder bPasswordEncoder;

    public ClientEntity createClient(String name, String cpf, String email, String password) {

        var clientExists = this.clientRepository.findByEmailOrCpf(email, cpf);

        if (clientExists.isPresent()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Email/Cpf already exists.");
        }

        var newClient = new ClientEntity();

        newClient.setName(name);
        newClient.setCpf(cpf);
        newClient.setEmail(email);
        newClient.setPassword(bPasswordEncoder.encode(password));

        return clientRepository.save(newClient);
    }
}

Service de jetons

import java.time.Instant;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import example.com.challengePicPay.repositories.ClientRepository;

@Service
public class TokenService {

    @Autowired
    private ClientRepository clientRepository;

    @Autowired
    private JwtEncoder jwtEncoder;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public String login(String email, String password) {

        var client = this.clientRepository.findByEmail(email)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Email not found"));

        var isCorrect = client.isLoginCorrect(password, bCryptPasswordEncoder);

        if (!isCorrect) {
            throw new BadCredentialsException("Email/password invalid");
        }

        var now = Instant.now();
        var expiresIn = 300L;

        var claims = JwtClaimsSet.builder()
                .issuer("pic_pay_backend")
                .subject(client.getEmail())
                .issuedAt(now)
                .expiresAt(now.plusSeconds(expiresIn))
                .claim("scope", client.getUserType())
                .build();

        var jwtValue = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();

        return jwtValue;

    }
}

Contrôleurs

Contrôleur client

package example.com.challengePicPay.controllers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import example.com.challengePicPay.controllers.dto.NewClientDTO;
import example.com.challengePicPay.entities.ClientEntity;
import example.com.challengePicPay.services.ClientService;

@RestController
public class ClientController {

    @Autowired
    private ClientService clientService;

    @PostMapping("/signin")
    public ResponseEntity<ClientEntity> createNewClient(@RequestBody NewClientDTO client) {
        var newClient = this.clientService.createClient(client.name(), client.cpf(), client.email(), client.password());

        return ResponseEntity.status(HttpStatus.CREATED).body(newClient);
    }

    @GetMapping("/protectedRoute")
    @PreAuthorize("hasAuthority('SCOPE_client')")
    public ResponseEntity<String> protectedRoute(JwtAuthenticationToken token) {
        return ResponseEntity.ok("Authorized");
    }

}

Explanation

  • The /protectedRoute is a private route that can only be accessed with a JWT after logging in.

  • The token must be included in the headers as a Bearer token, for example.

  • You can use the token information later in your application, such as in the service layer.

  • @PreAuthorize: The @PreAuthorize annotation in Spring Security is used to perform authorization checks before a method is invoked. This annotation is typically applied at the method level in a Spring component (like a controller or a service) to restrict access based on the user's roles, permissions, or other security-related conditions.
    The annotation is used to define the condition that must be met for the method to be executed. If the condition evaluates to true, the method proceeds. If it evaluates to false, access is denied,

  • "hasAuthority('SCOPE_client')": It checks if the currently authenticated user or client has the specific authority SCOPE_client. If they do, the method protectedRoute() is executed. If they don't, access is denied.


Token Controller: Here, you can log in to the application, and if successful, it will return a token.

package example.com.challengePicPay.controllers;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;

import example.com.challengePicPay.controllers.dto.LoginDTO;
import example.com.challengePicPay.services.TokenService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@RestController
public class TokenController {

    @Autowired
    private TokenService tokenService;

    @PostMapping("/login")
    public ResponseEntity<Map<String, String>> login(@RequestBody LoginDTO loginDTO) {
        var token = this.tokenService.login(loginDTO.email(), loginDTO.password());

        return ResponseEntity.ok(Map.of("token", token));
    }

}

Reference

  • Spring Security
  • Spring Security-Toptal article

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