[Übung] Bringen Sie Ihnen bei, wie Sie die JWT-Authentifizierung verwenden – Chatten über einen Coupon | Technisches Team von JD Cloud

Einführung

Während des letzten Vorstellungsgesprächs habe ich versehentlich mit dem Kandidaten über JWT-bezogene Dinge gesprochen und auch an die Projekte gedacht, die ich bei JWT gelandet war.

In Bezug auf JWT kann man sagen, dass es ein leistungsstarkes Werkzeug in einem verteilten System ist. In vielen meiner Projektpraktiken ist JWT die erste Wahl für das Authentifizierungssystem. Seine Vorteile werden Sie fesseln, genau wie Sie Gutscheine erhalten.

Erinnern wir uns an eine Szene: Wenn Sie und Ihre Freundin den gegrillten Fisch eines bestimmten Jiang essen wollten, was würden Sie tun?

In der traditionellen Ära sieht die Szene meiner Meinung nach so aus: Wenn wir in ein bestimmtes Jiang's-Restaurant gehen, werden wir vom Kellner zu einem Tisch geführt und beginnen dann mit der Bestellung. Der Service zeichnet unsere Bestellinformationen auf und sendet sie dann es in die hintere Küche. In diesem Prozess entspricht dieser Tisch einer Sitzung, und unsere Bestellinformationen werden in dieser Sitzung aufgezeichnet und dann an die hintere Küche gesendet. Dies ist ein typischer sitzungsbasierter Authentifizierungsprozess. Wir haben jedoch auch Nachteile festgestellt, nämlich dass die sitzungsbasierte Authentifizierung stark vom Server abhängt und Informationen auf dem Server gespeichert werden, was die Flexibilität und Skalierbarkeit erheblich einschränkt.

Im Internetzeitalter haben uns Dianping, Meituan und Ele.me eine weitere Wahl geboten. Auf diesen Plattformen können wir sofort nach Gutscheinen außerhalb der Stadt Jiangbian suchen. Paketdetails. Dieser Gutschein ist unser JWT und wir können ihn in jedem teilnehmenden Restaurant verwenden, anstatt uns auf dasselbe Restaurant zu beschränken. Gleichzeitig erfasst dieser Coupon direkt unsere Bestelldaten. Wenn wir im Restaurant ankommen, müssen wir dem Kellner nur den QR-Code des Coupons mitteilen, und der Kellner serviert uns das gewünschte Essen.

Nun, das Obige ist nur ein kleines Beispiel. Tatsächlich möchte ich nur die Vorteile von JWT im Vergleich zum herkömmlichen sitzungsbasierten Authentifizierungsframework erläutern.

Der Vorteil von JWT besteht darin, dass es domänen- und serverübergreifend genutzt werden kann, während Session nur unter diesem Domänennamen genutzt werden kann. Darüber hinaus muss JWT Benutzerinformationen nicht auf der Serverseite speichern, sondern nur auf der Clientseite, was die Belastung auf der Serverseite verringert. Dieser Vorteil ist auch bei der verteilten Architektur immer noch offensichtlich.

Was ist JWT?

Nachdem ich so viel gesagt habe, wie definiert man JWT?

JWT (JSON Web Token) ist ein offener Standard (RFC7519) zur Authentifizierung in Webanwendungen. Es kann Informationen sicher zwischen Benutzern und Servern übertragen, da es digitale Signaturen verwendet, um die Integrität und Authentizität der Daten zu überprüfen.

JWT besteht aus drei Teilen: Header, Payload und Signatur . Der Header enthält Algorithmus- und Typinformationen, die Nutzlast enthält Benutzerinformationen und die Signatur wird zur Überprüfung der Integrität und Authentizität der Daten verwendet.

Lassen Sie mich über Poload sprechen, den Ladeteil. Dies ist das Kernmodul von JWT, das einige Ansprüche enthält. Deklarationen bestehen aus drei Arten:

Registrierte Ansprüche: Dies ist ein vordefinierter Anspruchsname, der hauptsächlich Folgendes umfasst:

  • iss: Token-Emittent
  • sub: Token-Betreff
  • aud: Zielgruppe von Token
  • exp: Token-Ablaufzeit
  • iat: Zeitpunkt der Token-Ausgabe
  • jti: Eindeutige Token-ID

Öffentliche Ansprüche: Der öffentliche Anspruch ist ein selbst definierter Anspruchsname, um Konflikte zu vermeiden.

Private Ansprüche: Ein privater Anspruch ähnelt einem öffentlichen Anspruch, außer dass er dazu verwendet wird, Informationen zwischen zwei Parteien auszutauschen.

Wenn sich der Benutzer anmeldet, generiert der Server ein JWT und gibt es als Antwort an den Client zurück. Der Client sendet dieses JWT in nachfolgenden Anfragen. Der Server verwendet denselben Schlüssel, um die Signatur des JWT zu überprüfen und die Benutzerinformationen aus der Nutzlast abzurufen. Wenn die Signatur überprüft wird und die Benutzerinformationen gültig sind, lässt der Server die Fortsetzung der Anfrage zu.

Vorteile von JWT

Wenn wir die Vorteile von JWT systematisch zusammenfassen, lauten sie wie folgt:

  1. Sprach- und plattformübergreifend: JWT basiert auf dem JSON-Standard und kann daher zwischen verschiedenen Programmiersprachen und Plattformen ausgetauscht und verwendet werden. Zustandslos: Da das JWT alle notwendigen Informationen enthält, muss der Server nicht bei jeder Anfrage Sitzungsdaten speichern, was einen einfachen Lastausgleich ermöglicht.
  2. Sicherheit: JWT verwendet digitale Signaturen, um die Integrität und Authentizität von Daten zu überprüfen und so zu verhindern, dass Daten manipuliert oder gefälscht werden.
  3. Erweiterbarkeit: JWT kann beliebige Benutzerinformationen enthalten und kann daher problemlos auf andere Anwendungen erweitert werden.
  4. Ein Schema, das auf der JWT-Authentifizierung basiert

Ich werde ein Beispiel für meine tatsächliche Geschäftslandung geben.

In meinem Geschäftsszenario gibt es normalerweise ein Business-Gateway, und die Kernfunktionen des Gateways sind Authentifizierung und Online-Dateikonvertierung. Die Benutzeranforderung speichert die JWT-Zeichenfolge im Header und analysiert dann das JWT, nachdem sie zum Gateway gegangen ist. Die analysierten Kontextinformationen werden in Klartext-KV konvertiert und hier im Header gespeichert, um sie zwischen Mikrodiensten im System zu verwenden. Stellen Sie Klartext bereit Kontextinformationen beim gegenseitigen Anrufen. Das spezifische Zeitdiagramm lautet wie folgt:

JWT-Praxis basierend auf Spring-Sicherheit

Das Prinzip von JWT ist sehr einfach. Natürlich können Sie den gesamten JWT-Prozess vollständig selbst implementieren, aber in der Praxis müssen wir dies im Allgemeinen nicht tun, da viele ausgereifte und benutzerfreundliche Räder bereitgestellt werden Für uns sind Kapselung und Sicherheit weit entfernt. Es ist höher, als ein einfaches JWT in Eile zu kapseln.

Wenn es auf dem Erlernen von JWT basiert, schlage ich vor, dass Sie selbst eine Demo schreiben. Wenn es jedoch aus der Perspektive der Praxis ausgelöst wird, können wir die von Spring Security bereitgestellte JWT-Komponente verwenden, um eine sehr hohe Stabilität und Sicherheit effizient und schnell zu implementieren . JWT-Authentifizierungsframework.

Das Folgende ist mein vereinfachter JWT-Praxiskodex, der auf der tatsächlichen Situation meines Unternehmens und gemäß den Vertraulichkeitsanforderungen basiert. Es kann als Einführung betrachtet werden und ich hoffe, dass es jedem als Referenz für die Verwendung von JWT in Geschäftsszenarien dienen kann .

Maven-Abhängigkeiten

Zuerst müssen wir der Datei pom.xml die folgenden Abhängigkeiten hinzufügen:

<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>

Kapselung der JWT-Toolklasse

Anschließend können wir eine JwtTokenUtil-Klasse erstellen, um JWT-Tokens zu generieren und zu validieren:

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();
    }
}

In dieser Implementierung haben wir die jjwt-Bibliothek verwendet, um JWT-Tokens zu erstellen und zu analysieren. Wir definieren die folgenden Methoden:

  • genericToken: Generieren Sie ein JWT-Token.
  • createToken: Erstellen Sie ein JWT-Token.
  • validateToken: Überprüfen Sie, ob das JWT-Token gültig ist.
  • isTokenExpired: Prüft, ob das JWT-Token abgelaufen ist.
  • extractUsername: Benutzernamen aus JWT-Token extrahieren.
  • extractExpiration: Extrahieren Sie die Ablaufzeit aus dem JWT-Token.
  • extractClaim: Extrahieren Sie den angegebenen Anspruch aus dem JWT-Token.
  • extractAllClaims: Extrahiert alle Ansprüche aus dem JWT-Token.

UserDetailsService-Klassendefinition

Als Nächstes können wir einen benutzerdefinierten UserDetailsService zur Validierung der Benutzeranmeldeinformationen erstellen:

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<>());
    }
}

In dieser Implementierung verwenden wir UserRepository, um Benutzerinformationen abzurufen. Wir implementieren die Schnittstelle „UserDetailsService“ und überschreiben die Methode „loadUserByUsername“, um die Anmeldeinformationen des Benutzers zu überprüfen.

JwtAuthenticationFilter-Definition

Als Nächstes können wir eine JwtAuthenticationFilter-Klasse erstellen, die Anmeldeanfragen abfängt und JWT-Tokens generiert:

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;
        }
    }
}

In dieser Implementierung erben wir
die Klasse „UsernamePasswordAuthenticationFilter“ und überschreiben die Methoden „tryAuthentication“ und „successfulAuthentication“, um ein JWT-Token zu generieren und es dem HTTP-Antwortheader hinzuzufügen, wenn die Anmeldung erfolgreich ist.

Spring Security-Konfigurationsklasse

Schließlich können wir eine Spring Security-Konfigurationsklasse erstellen, um Authentifizierungs- und Autorisierungsregeln zu konfigurieren:

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();
    }
}

In dieser Implementierung verwenden wir JwtUserDetailsService, um Benutzeranmeldeinformationen zu validieren, und
JwtAuthenticationEntryPoint, um Authentifizierungsfehler zu behandeln.

Wir haben außerdem den JwtAuthenticationFilter so konfiguriert, dass er ein JWT-Token generiert und es den HTTP-Antwortheadern hinzufügt. Wir definieren auch eine PasswordEncoder-Bean zum Verschlüsseln von Benutzerkennwörtern.

Überprüfung der Debug-Schnittstelle

Wir können jetzt eine POST-Anfrage an den Endpunkt /authenticate senden, um die Benutzeranmeldung zu authentifizieren und ein JWT-Token zu generieren. Zum Beispiel:

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

Wenn die Anmeldeinformationen erfolgreich überprüft wurden, wird ein HTTP-Antwortheader mit einem JWT-Token zurückgegeben. Mit diesem Token können wir auf Endpunkte zugreifen, die eine Autorisierung erfordern. Zum Beispiel:

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

In diesem Beispiel senden wir eine GET-Anfrage an den /hello-Endpunkt und fügen das JWT-Token im HTTP-Header hinzu. Gibt eine erfolgreiche HTTP-Antwort zurück, wenn das Token gültig ist und der Benutzer berechtigt ist, auf den Endpunkt zuzugreifen.

Zusammenfassen

JWT ist ein einfacher, sicherer und skalierbarer Authentifizierungsmechanismus, der für verschiedene Anwendungen und Szenarien geeignet ist. Es reduziert die Serverlast, verbessert die Anwendungssicherheit und kann problemlos auf andere Anwendungen erweitert werden.

JWT weist jedoch auch bestimmte Mängel auf. Beispielsweise gibt das Nutzlastmodul nicht eindeutig an, dass es für die Übertragung verschlüsselt werden muss. Wenn Sie also keine zusätzlichen Sicherheitsmaßnahmen ergreifen, kann es leicht zu einem Verlust von Benutzerinformationen kommen, sobald JWT von anderen abgefangen wird . Wenn Sie daher die Sicherheit von JWT in tatsächlichen Projekten erhöhen möchten, sind Maßnahmen zur Sicherheitsverstärkung unerlässlich, einschließlich Verschlüsselungsmethoden, Speicherung geheimer Schlüssel, JWT-Ablaufrichtlinien usw.

Natürlich gibt es im eigentlichen Authentifizierungs- und Authentifizierungsframework mehr als JWT. JWT löst nur das Problem der Benutzerkontextübertragung. In tatsächlichen Projekten wird JWT häufig in Verbindung mit anderen Authentifizierungssystemen wie OAuth2.0 verwendet. Der Platz hier ist begrenzt und wird daher nicht erweitert. Ich werde in Zukunft die Gelegenheit haben, einen separaten Artikel über die OAuth2.0-Authentifizierungsarchitektur zu schreiben.

Autor: JD Logistics Zhao Yongping

Inhaltsquelle: JD Cloud-Entwickler-Community

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

Supongo que te gusta

Origin my.oschina.net/u/4090830/blog/8816392
Recomendado
Clasificación