[Pratique] Vous apprendre à utiliser l'authentification JWT --- chatter à partir d'un coupon | Équipe technique JD Cloud

introduction

Au cours du récent processus d'entretien, j'ai parlé par inadvertance de choses liées à JWT avec le candidat, et j'ai également pensé aux projets que j'avais atterris sur JWT.

En ce qui concerne JWT, on peut dire qu'il s'agit d'un outil puissant dans un système distribué.Dans bon nombre de mes pratiques de projet, JWT est le premier choix pour le système d'authentification. Ses avantages vous garderont accro, tout comme vous obtenez des coupons.

Rappelons-nous une scène, si vous et votre copine vouliez manger le poisson grillé d'un certain Jiang, que feriez-vous ?

À l'époque traditionnelle, je pense que la scène ressemble à ceci : lorsque nous entrons dans un certain restaurant de Jiang, nous serons guidés vers une table par le serveur, puis nous commencerons à commander. Le service enregistrera nos informations de commande, puis nous enverra à l'arrière cuisine. Dans ce processus, cette table équivaut à une session, et nos informations de commande sont enregistrées dans cette session puis envoyées à l'arrière-cuisine. Il s'agit d'un processus d'authentification typique basé sur la session. Mais nous avons également trouvé ses inconvénients, c'est-à-dire que l'authentification basée sur la session dépend fortement du serveur et que les informations sont stockées sur le serveur, ce qui réduit considérablement la flexibilité et l'évolutivité.

À l'ère d'Internet, Dianping, Meituan et Ele.me nous ont donné un autre choix. Nous pouvons immédiatement rechercher des coupons en dehors de la ville de Jiangbian sur ces plateformes. Détails du package. Ce coupon est notre JWT, et nous pouvons utiliser ce coupon dans n'importe quel restaurant participant au lieu d'être limité au même restaurant. En même temps, ce coupon enregistre directement les détails de notre commande.Lorsque nous arrivons au restaurant, nous n'avons qu'à informer le serveur du code QR du coupon, et le serveur nous servira la nourriture que nous voulons.

Eh bien, ce qui précède n'est qu'un petit exemple. En fait, je veux juste expliquer les avantages de JWT par rapport au cadre d'authentification traditionnel basé sur la session.

L'avantage de JWT est qu'il peut être utilisé sur plusieurs domaines et serveurs, tandis que Session ne peut être utilisé que sous ce nom de domaine. De plus, JWT n'a pas besoin d'enregistrer les informations utilisateur côté serveur, mais uniquement du côté client, ce qui réduit la charge côté serveur. Cet avantage est encore évident sous l'architecture distribuée.

Qu'est-ce que JWT

Cela dit, comment définir JWT ?

JWT (JSON Web Token) est une norme ouverte (RFC7519) pour l'authentification dans les applications Web. Il peut transférer des informations en toute sécurité entre les utilisateurs et les serveurs car il utilise des signatures numériques pour vérifier l'intégrité et l'authenticité des données.

JWT se compose de trois parties : en-tête, charge utile et signature . L'en-tête contient des informations sur l'algorithme et le type, la charge utile contient des informations sur l'utilisateur et la signature est utilisée pour vérifier l'intégrité et l'authenticité des données.

Permettez-moi de parler de poload, qui est la partie load. Il s'agit du module de base de jwt, qui inclut certaines réclamations. Les déclarations se composent de trois types :

Revendications enregistrées : il s'agit d'un nom de réclamation prédéfini, comprenant principalement les éléments suivants :

  • iss : émetteur de jetons
  • sub : sujet du jeton
  • aud : public du jeton
  • exp : délai d'expiration du jeton
  • iat : heure d'émission du jeton
  • jti : identifiant unique du jeton

Revendications publiques : la revendication publique est un nom de revendication auto-défini pour éviter les conflits.

Revendications privées : une réclamation privée est similaire à une réclamation publique, sauf qu'elle est utilisée pour partager des informations entre deux parties.

Lorsque l'utilisateur se connecte, le serveur génère un JWT et le renvoie au client en réponse. Le client enverra ce JWT dans les requêtes suivantes. Le serveur utilisera la même clé pour vérifier la signature du JWT et obtenir les informations de l'utilisateur à partir de la charge utile. Si la signature est vérifiée et que les informations de l'utilisateur sont valides, le serveur autorisera la poursuite de la demande.

Avantages de JWT

Si nous résumons systématiquement les avantages de JWT, ils sont les suivants :

  1. Inter-langage et plate-forme : JWT est basé sur la norme JSON, il peut donc être échangé et utilisé entre différents langages de programmation et plates-formes. Sans état : étant donné que le JWT contient toutes les informations nécessaires, le serveur n'a pas besoin de stocker de données de session avec chaque requête, ce qui permet un équilibrage de charge facile.
  2. Sécurité : JWT utilise des signatures numériques pour vérifier l'intégrité et l'authenticité des données, empêchant ainsi la falsification ou la falsification des données.
  3. Extensibilité : JWT peut contenir n'importe quelle information utilisateur, il peut donc être facilement étendu à d'autres applications.
  4. Un schéma basé sur l'authentification JWT

Je vais donner un exemple de mon atterrissage d'affaires réel.

Dans mon scénario d'entreprise, il existe généralement une passerelle d'entreprise, et les fonctions principales de la passerelle sont l'authentification et la conversion de fichiers en ligne. La demande de l'utilisateur stockera la chaîne JWT dans l'en-tête, puis analysera le JWT après être allé à la passerelle. Les informations de contexte analysées seront converties en texte brut KV et stockées dans l'en-tête ici, pour une utilisation entre les microservices du système Fournir un texte brut informations contextuelles lors de l'appel. Le chronogramme spécifique est le suivant :

Pratique JWT basée sur la sécurité Spring

Le principe de JWT est très simple. Bien sûr, vous pouvez complètement implémenter tout le processus de JWT par vous-même, mais en pratique, nous n'avons généralement pas besoin de le faire, car il existe de nombreuses roues matures et faciles à utiliser. pour nous, et l'encapsulation et la sécurité sont loin. C'est plus élevé que d'encapsuler un simple JWT à la hâte.

S'il est basé sur l'apprentissage de JWT, je vous suggère d'écrire une démo par vous-même, mais s'il est déclenché du point de vue de la pratique, nous pouvons utiliser le composant JWT fourni par Spring Security pour implémenter efficacement et rapidement une stabilité et une sécurité très élevées. Cadre d'authentification JWT.

Voici mon code de pratique JWT simplifié basé sur la situation réelle de mon entreprise et selon les exigences de confidentialité. Il peut être considéré comme une introduction, et j'espère qu'il pourra être utilisé comme référence pour que tout le monde utilise JWT dans des scénarios d'entreprise .

dépendances maven

Tout d'abord, nous devons ajouter les dépendances suivantes au fichier pom.xml :

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

Encapsulation de classe d'outils JWT

Nous pouvons ensuite créer une classe JwtTokenUtil pour générer et valider des jetons JWT :

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtTokenUtil {
    private static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
    @Value("${jwt.secret}")
    private String secret;

    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = newHashMap <>();
        return createToken(claims, userDetails.getUsername());
    }
    private String createToken(Map<String, Object> claims, String subject) {
        Date now = new Date();
        Date expiration = new Date(now.getTime() + JWT_TOKEN_VALIDITY * 1000);
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(now)
                .setExpiration(expiration)
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();
    }
    public boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }
    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }
    private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }
    private Claims extractAllClaims(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }
}

Dans cette implémentation, nous avons utilisé la bibliothèque jjwt pour créer et analyser des jetons JWT. Nous définissons les méthodes suivantes :

  • generateToken : génère un jeton JWT.
  • createToken : crée un jeton JWT.
  • validateToken : vérifiez que le jeton JWT est valide.
  • isTokenExpired : Vérifie si le jeton JWT a expiré.
  • extractUsername : extrait le nom d'utilisateur du jeton JWT.
  • extractExpiration : extrayez l'heure d'expiration du jeton JWT.
  • extractClaim : extrait la revendication spécifiée du jeton JWT.
  • extractAllClaims : extrait toutes les revendications du jeton JWT.

Définition de la classe UserDetailsService

Ensuite, nous pouvons créer un UserDetailsService personnalisé pour valider les informations de connexion de l'utilisateur :

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class JwtUserDetailsService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        UserEntity user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found with username: " + username);
        }
        return new User(user.getUsername(), user.getPassword(),
                new ArrayList<>());
    }
}

Dans cette implémentation, nous utilisons UserRepository pour récupérer les informations utilisateur. Nous implémentons l'interface UserDetailsService et remplaçons la méthode loadUserByUsername afin de vérifier les informations de connexion de l'utilisateur.

Définition de JwtAuthenticationFilter

Ensuite, nous pouvons créer une classe JwtAuthenticationFilter qui intercepte les demandes de connexion et génère des jetons JWT :

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final AuthenticationManager authenticationManager;
    private final JwtTokenUtil jwtTokenUtil;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil) {
        this.authenticationManager = authenticationManager;
        this.jwtTokenUtil = jwtTokenUtil;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            LoginRequest loginRequest = new ObjectMapper().readValue(request.getInputStr eam(), LoginRequest.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword(), Collections.emptyList())
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)throwsIOException,ServletException {
        UserDetails userDetails = (UserDetails) authResult.getPrincipal();
        String token = jwtTokenUtil.generateToken(userDetails);
        response.addHeader("Authorization", "Bearer " + token);
    }

    private static class LoginRequest {
        private String username;
        private String password;

        public String getUsername() {
            return username;
        }

        public void setUsername(String username) {
            this.username = username;
        }

        public String getPassword() {
            return password;
        }

        public void setPassword(String password) {
            this.password = password;
        }
    }
}

Dans cette implémentation, nous héritons
de la classe UsernamePasswordAuthenticationFilter et remplaçons les méthodes de tentative d'authentification et de réussite Authentication pour générer un jeton JWT et l'ajouter à l'en-tête de réponse HTTP lorsque la connexion est réussie.

Classe de configuration Spring Security

Enfin, nous pouvons créer une classe de configuration Spring Security afin de configurer les règles d'authentification et d'autorisation :

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtUserDetailsService jwtUserDetailsService;
    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests().antMatchers("/authenticate").permitAll()
                .anyRequest().authenticated().and()
                .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.addFilterBefore(newJwtAuthenticationFilter(authenticationManager(), jwtTokenUtil), UsernamePasswordAuthenticationFilter.class);
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
    }
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Dans cette implémentation, nous utilisons JwtUserDetailsService pour valider les informations de connexion de l'utilisateur et
JwtAuthenticationEntryPoint pour gérer les erreurs d'authentification.

Nous avons également configuré le JwtAuthenticationFilter pour générer un jeton JWT et l'ajouter aux en-têtes de réponse HTTP. Nous définissons également un bean PasswordEncoder pour chiffrer les mots de passe des utilisateurs.

Vérification de l'interface de débogage

Nous pouvons maintenant envoyer une requête POST au point de terminaison /authenticate pour authentifier la connexion de l'utilisateur et générer un jeton JWT. Par exemple:

bash
curl -X POST \
  http://localhost:8080/authenticate \
  -H 'Content-Type: application/json'\
  -d '{
    "username": "user",
    "password": "password"
}'

Si les informations de connexion sont vérifiées avec succès, un en-tête de réponse HTTP avec un jeton JWT sera renvoyé. Nous pouvons utiliser ce jeton pour accéder aux points de terminaison qui nécessitent une autorisation. Par exemple:

bash
curl -X GET \
  http://localhost:8080/hello \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjI0MDM2NzA4LCJleHAiOjE2MjQwMzc1MDh9.9fZS7jPp0NzB0JyOo4y4jO4x3s3KjV7yW1nLzV7cO_c'

Dans cet exemple, nous envoyons une requête GET au point de terminaison /hello et ajoutons le jeton JWT dans l'en-tête HTTP. Renvoie une réponse HTTP réussie si le jeton est valide et que l'utilisateur est autorisé à accéder au point de terminaison.

Résumer

JWT est un mécanisme d'authentification simple, sécurisé et évolutif adapté à diverses applications et scénarios. Il réduit la charge du serveur, améliore la sécurité des applications et peut être facilement étendu à d'autres applications.

Cependant, JWT présente également certaines lacunes.Par exemple, son module de charge utile n'indique pas clairement qu'il doit être crypté pour la transmission, donc si vous ne prenez pas de mesures de sécurité supplémentaires, une fois que jwt est intercepté par d'autres, il est facile de divulguer des informations sur l'utilisateur. . Par conséquent, si vous souhaitez augmenter la sécurité de JWT dans des projets réels, des mesures de renforcement de la sécurité sont essentielles, notamment des méthodes de chiffrement, le stockage de clés secrètes, des politiques d'expiration JWT, etc.

Bien sûr, il y a plus que JWT dans le cadre d'authentification et d'authentification réel. JWT ne résout que le problème de la transmission du contexte utilisateur. Dans les projets réels, JWT est souvent utilisé conjointement avec d'autres systèmes d'authentification, tels que OAuth2.0. L'espace ici est limité, il ne sera donc pas agrandi. J'aurai l'occasion d'écrire un article séparé sur l'architecture d'authentification OAuth2.0 à l'avenir.

Auteur : JD Logistics Zhao Yongping

Source de contenu : communauté de développeurs JD Cloud

{{o.name}}
{{m.name}}

おすすめ

転載: my.oschina.net/u/4090830/blog/8816392