JWT explicación y aplicación práctica


1. Introducción

        Este blog está basado en el blog de un gran jefe. El propósito es escribirlo a mano para que pueda recordarlo más profundamente. Al mismo tiempo, también es en caso de que ese gran jefe borre el blog y gané. No podré leer un artículo tan bueno en el futuro. Al principio, no noté la tecnología JWT, hasta la entrevista hace unos días, el entrevistador hizo esta pregunta y yo estaba confundido en el lugar, así que lo compensaré esta vez. Artículo original: JWT Explicación detallada

2. ¿Qué es JWT?

        Antes de presentar JWT, revisemos el proceso de uso del token para la autenticación de usuarios:

  • El cliente solicita un inicio de sesión con un nombre de usuario y contraseña.
  • Después de recibir la solicitud, el servidor verifica el nombre de usuario y la contraseña.
  • Después de que la verificación sea exitosa, el servidor emitirá un token y luego devolverá el token al cliente.
  • Después de recibir el token, el cliente puede almacenarlo, como ponerlo en una cookie.
  • El cliente necesita llevar este token cada vez que envía interés al servidor, y puede llevarse en una cookie o encabezado.
  • Después de recibir la solicitud, el servidor verifica el token incluido en la solicitud del cliente y devuelve los datos de la solicitud al cliente si la verificación es exitosa.

En comparación con el método de autenticación de sesión tradicional, este método de autenticación basado en tokens ahorra recursos del servidor y es más amigable con los terminales móviles y la distribución. Sus ventajas son las siguientes:

  • Admite el acceso entre dominios : las cookies no pueden cruzar dominios y los tokens no usan cookies (siempre que el token exista en el encabezado), por lo que no habrá pérdida de información después de cruzar dominios.
  • Sin estado : el mecanismo del token no necesita almacenar información de la sesión en el lado del servidor, porque el token en sí contiene la información de todos los usuarios que iniciaron sesión, por lo que puede reducir la presión en el lado del servidor.
  • Más aplicable a CDN : todos los datos en el servidor se pueden solicitar a través de la red de distribución de contenido.
  • Más adecuado para terminales móviles : cuando el cliente es una plataforma sin navegador, las cookies no son compatibles, y será mucho más sencillo utilizar la autenticación mediante token en este momento.
  • No hay necesidad de considerar CSRF : dado que las cookies no se consideran, CSRF no ocurrirá en el método de autenticación de token, por lo que no es necesario considerar la defensa CSRF.

Y JWT es una implementación específica del token mencionado anteriormente, el nombre completo se llama JSON Web Token , la dirección del sitio web oficial: https://jwt.io/

En términos sencillos, JWT es esencialmente una cadena, que guarda la información del usuario en una cadena JSON y luego codifica un token JWT, y este token JWT tiene información de firma, que puede verificarse después de recibirlo. Ha sido manipulado, por lo que puede ser se utiliza para transmitir información de forma segura entre las partes como objetos JSON. El proceso de verificación de JWT es el siguiente:

  • El cliente envía su nombre de usuario y contraseña a la interfaz de back-end en forma de formulario.Este proceso es generalmente una solicitud POST. La forma recomendada es utilizar la transmisión encriptada SSL para evitar la manipulación de información confidencial.
  • Después de que el backend verifique el nombre de usuario y la contraseña, los datos que contienen la información del usuario se usan como la carga útil de JWT, que se codifica y concatena con Base64 en el encabezado de JWT, y luego se firma para formar un token de JWT (como una cadena de 111 .aaa.nnn).
  • El backend devuelve la cadena del token JWT al cliente como resultado de un inicio de sesión exitoso. El cliente puede guardar el resultado devuelto en el navegador y eliminar el token JWT guardado al cerrar la sesión.
  • El cliente colocará el token JWT en el atributo de autorización en el encabezado de la solicitud HTTP cada vez que lo solicite.
  • El servidor comprueba el Token JWT enviado por el cliente y la validez del verificador, como comprobar si la firma es correcta, si ha caducado, si el destinatario del token es él mismo, etc.
  • Una vez que se pasa la verificación, el servidor analiza la información del usuario contenida en el token JWT, realiza otras operaciones lógicas y devuelve el resultado.

3. Por qué usar JWT

        Antes de hablar de por qué usamos JWT, hablemos de las desventajas de Session. Todos sabemos que HTTP en sí mismo es un protocolo sin estado, lo que significa que si el usuario proporciona un nombre de usuario y una contraseña a nuestra aplicación para la autenticación del usuario, el protocolo HTTP no registrará el estado autenticado después de que se pase la autenticación, luego la siguiente solicitud, el usuario necesita realizar una autenticación, porque de acuerdo con el protocolo HTTP, no sabemos qué usuario envió la solicitud, por lo que para que nuestra aplicación identifique qué usuario envió la solicitud, solo podemos iniciar sesión en el servidor después de la usuario inicia sesión correctamente por primera vez. Almacene la información de inicio de sesión de un usuario, y esta información de inicio de sesión se pasará al navegador en respuesta, diciéndole que se guarde como una cookie, para que pueda enviarse a nuestro usuario cuando la próxima solicitud se hace, para que nuestra aplicación pueda identificar de qué usuario proviene la solicitud. Este es el proceso de autenticación tradicional basado en sesiones.
inserte la descripción de la imagen aquí
Entonces, la autenticación de sesión tradicional tendrá los siguientes problemas:

  • La información de inicio de sesión de cada usuario se guardará en la sesión del servidor. A medida que aumenta el número de usuarios, la sobrecarga del servidor aumentará significativamente.
  • Dado que la sesión existe en la memoria física del servidor, este método fallará en un sistema distribuido. Aunque el consentimiento de la sesión se puede guardar en redis, esto sin duda aumentará la complejidad del sistema y, para las aplicaciones que no requieren redis, se introducirá en vano un middleware de caché adicional.
  • Debido a que la sesión depende de la cookie, a menudo no hay cookies para clientes que no sean navegadores o terminales móviles, por lo que la forma de usar la sesión no será válida.
  • Debido a que la esencia de la autenticación de sesión se basa en cookies, si se interceptan cookies, los usuarios son vulnerables a ataques de falsificación de solicitudes entre sitios. Y si el navegador desactiva las cookies, este método también fallará.
  • Es aún más inaplicable en un sistema donde el front-end y el back-end están separados, la implementación del back-end es complicada, la solicitud enviada por el cliente a menudo pasa a través de múltiples middleware para llegar al servidor y la información de la cookie sobre el la sesión se reenviará varias veces.
  • Como se basa en cookies y las cookies no pueden cruzar dominios, la autenticación de sesión no puede cruzar dominios.

Hablando de sesiones, recordé que el entrevistador me hizo una pregunta durante la entrevista: Si las cookies están deshabilitadas, ¿cómo puedo encontrar la sesión?
        Respuesta: Hay dos soluciones comunes. El primero es la reescritura de URL, que consiste en agregar la identificación de la sesión directamente después de la ruta de la URL. El primero es el campo oculto del formulario, agregando un campo oculto al formulario para que la identificación de la sesión se pueda pasar al servidor cuando se envía el formulario. Por ejemplo:

<form name="testform" action="/xxx"> 
     <input type="hidden" name="jsessionid" value="ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764"/>
     <input type="text"> 
</form>

Ventajas de los JWT:

  • Conciso: el token JWT tiene una pequeña cantidad de datos y una velocidad de transmisión rápida.
  • Dado que el token JWT se almacena en el cliente en formato cifrado JSON, JWT es multilingüe y, en principio, cualquier formulario web lo admite.
  • No es necesario que el servidor guarde la información de la sesión, es decir, no depende de cookies y sesiones, por lo que no tiene las desventajas de la autenticación de sesión tradicional, y es especialmente adecuado para microservicios distribuidos.
  • Inicio de sesión único amigable. Si la sesión se usa para la autenticación de identidad, es difícil lograr un inicio de sesión único porque las cookies no pueden ser de dominio cruzado. Sin embargo, si el token se usa para la verificación, el token se puede almacenar en la memoria de cualquier ubicación del cliente, no necesariamente como una cookie, por lo que estos problemas no ocurrirán sin depender de las cookies.
  • Aplicable al terminal móvil: si la sesión se utiliza para la autenticación, se debe guardar una información en el servidor, y este método se basará en cookies, por lo que no es aplicable al terminal móvil.

4. La estructura de JWT

        JWT consta de tres partes: encabezado (header), payload (carga útil) y firma (Signature). Durante la transmisión, las tres partes del JWT se codificarán en Base64 y luego se concatenarán con . para formar la cadena transmitida final.
inserte la descripción de la imagen aquí
4.1 Encabezado:
        el encabezado JWT es un objeto JSON que describe los metadatos JWT. El atributo alt indica el algoritmo aplicable a la firma y el valor predeterminado es HMAC SHA256 (es decir, HS256); el atributo typ indica el tipo de token y el JWT token se escribe uniformemente como JWT. Finalmente, aplique el algoritmo de URL Base64 para convertir el objeto JSON anterior en una cadena y guárdelo.

{
    
    
  "alg": "HS256",
  "typ": "JWT"
}

4.2 Carga útil
        La parte de la carga útil es la parte sujeta del JWT, y también es un objeto JSON que contiene los datos que deben pasarse. JWT especifica campos predeterminados para la selección.

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

Además de los campos predeterminados anteriores, también podemos personalizar campos privados. Generalmente, coloque los datos de información del usuario en la carga útil, de la siguiente manera

{
    
    
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
}

Nota: De manera predeterminada, JWT no está cifrado, pero usa el algoritmo base64. Después de obtener la cadena JWT, se puede volver a convertir a los datos JSON originales. Cualquiera puede interpretar su contenido, así que no cree un campo de información privada. Por ejemplo, la contraseña del usuario debe ser No se puede guardar en JWT.
4.3 Firma
        La parte del hash de la firma es para firmar las dos partes anteriores de los datos.También es necesario usar el encabezado codificado en base64 y los datos de carga útil para generar un hash a través del algoritmo especificado para garantizar que los datos no se alteren. Primero debe especificar una clave. Esta clave solo se guarda en el servidor y no se puede divulgar a los usuarios. Luego use el algoritmo de firma especificado en el encabezado para generar una firma. Después de calcular el hash de la firma, las tres partes del encabezado JWT, la carga útil y el hash de la firma forman una cadena, y cada parte está separada por . para formar el objeto JWT completo.

Preste atención a la función de cada parte de JWT, después de que el servidor reciba el token JWT enviado por el cliente:

  • El encabezado y la carga útil pueden usar directamente base64 para analizar el texto original, obtener el algoritmo de firma hash del encabezado y obtener datos válidos de la carga útil.
  • Debido a que la firma utiliza un algoritmo de cifrado irreversible, el texto original no se puede analizar y su función es verificar si el token ha sido manipulado. Una vez que el servidor obtiene el algoritmo de encriptación en el encabezado, usa el algoritmo más la clave secreta para encriptar el encabezado y la carga útil, y compara si los datos encriptados son consistentes con los enviados por el cliente. Tenga en cuenta que la clave secreta solo se puede guardar en el servidor y su significado es diferente para los diferentes algoritmos de cifrado. Generalmente, para el algoritmo de cifrado de resumen MD5, la clave secreta en realidad representa el valor de sal.

5. Tipos de JWT

        De hecho, JWT (JSON Web Token) se refiere a una especificación que nos permite usar JWT para transferir información segura y confiable entre dos organizaciones.La implementación específica de JWT se puede dividir en los siguientes tipos:

  • JWT no seguro: JWT no seguro y sin firmar
  • JWS: JWT firmado
  • JWE: el JWT encriptado de la parte de la carga útil

5.1 JWT no seguro
No se utiliza JWT firmado, es decir, JWT no seguro. La parte del encabezado no especifica el algoritmo de firma y no hay una parte de Firma.
5.2 JWS
        JWS, es decir, JWT Signature, su estructura se basa en el JWT no seguro anterior, el algoritmo de firma se declara en el encabezado y la firma se agrega al final. La creación de una firma es para garantizar que jwt no pueda ser manipulado por otros. El JWT que solemos usar es generalmente JWS. Para completar la firma, además de la información del encabezado y la información de la carga útil, también se requiere la clave del algoritmo, que es la clave secreta. En general, hay dos tipos de algoritmos de cifrado:

  • Cifrado simétrico: secretKey se refiere a la clave de cifrado, que puede generar firmas y verificar firmas
  • Cifrado asimétrico: secretKey se refiere a la clave privada, que solo se usa para generar una firma y no se puede usar para verificar la firma (la clave pública se usa para verificar la firma)

La clave o par de claves de JWT generalmente se denomina JSON Web Key o JWK. Hasta ahora, hay tres algoritmos de firma para jwt:

  • HMAC [Código de autenticación de mensaje hash (simétrico)]: HS256/HS384/HS512
  • RSASSA [algoritmo de firma RSA (asimétrico)] (RS256/RS384/RS512)
  • ECDSA [algoritmo de firma de datos de curva elíptica (asimétrico)] (ES256/ES384/ES512)

6. Aplicación en desarrollo real

6.1 java-jwt
introduce dependencias

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>

generar token

public class JWTTest {
    
    
    @Test
    public void testGenerateToken(){
    
    
        // 指定token过期时间为10秒
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND, 10);

        String token = JWT.create()
                .withHeader(new HashMap<>())  // Header
                .withClaim("userId", 21)  // Payload
                .withClaim("userName", "baobao")
                .withExpiresAt(calendar.getTime())  // 过期时间
                .sign(Algorithm.HMAC256("!34ADAS"));  // 签名用的secret

        System.out.println(token);
    }
}

Analizar la cadena JWT

public class JWTTest {
    
    
    @Test
    public void testGenerateToken(){
    
    
        // 指定token过期时间为10秒
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND, 10);

        String token = JWT.create()
                .withHeader(new HashMap<>())  // Header
                .withClaim("userId", 21)  // Payload
                .withClaim("userName", "baobao")
                .withExpiresAt(calendar.getTime())  // 过期时间
                .sign(Algorithm.HMAC256("!34ADAS"));  // 签名用的secret

        System.out.println(token);
    }
}

6.2 jjwt-root
introduce dependencias

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
public class JwtUtils {
    
    
    // token时效:24小时
    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    // 签名哈希的密钥,对于不同的加密算法来说含义不同
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

    /**
     * 根据用户id和昵称生成token
     * @param id  用户id
     * @param nickname 用户昵称
     * @return JWT规则生成的token
     */
    public static String getJwtToken(String id, String nickname){
    
    
        String JwtToken = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                .setSubject("baobao-user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .claim("id", id)
                .claim("nickname", nickname)
            	// HS256算法实际上就是MD5加盐值,此时APP_SECRET就代表盐值
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();

        return JwtToken;
    }

    /**
     * 判断token是否存在与有效
     * @param jwtToken token字符串
     * @return 如果token有效返回true,否则返回false
     */
    public static boolean checkToken(String jwtToken) {
    
    
        if(StringUtils.isEmpty(jwtToken)) return false;
        try {
    
    
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判断token是否存在与有效
     * @param request Http请求对象
     * @return 如果token有效返回true,否则返回false
     */
    public static boolean checkToken(HttpServletRequest request) {
    
    
        try {
    
    
            // 从http请求头中获取token字符串
            String jwtToken = request.getHeader("token");
            if(StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根据token获取会员id
     * @param request Http请求对象
     * @return 解析token后获得的用户id
     */
    public static String getMemberIdByJwtToken(HttpServletRequest request) {
    
    
        String jwtToken = request.getHeader("token");
        if(StringUtils.isEmpty(jwtToken)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("id");
    }
}

jjwt ha sufrido cambios importantes después de la versión 0.10, las dependencias deben introducir múltiples

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

Después de la versión jjwt0.10, la longitud de secretKey tiene los siguientes requisitos obligatorios:

  • HS256: requiere al menos 256 bits (32 bytes)
  • HS384: requiere al menos 384 bits (48 bytes)
  • HS512: requiere al menos 512 bits (64 bytes)
  • RS256 y PS256: al menos 2048 bits
  • RS384 y PS384: al menos 3072 bits
  • RS512 y PS512: al menos 4096 bits
  • ES256: al menos 256 bits (32 bytes)
  • ES384: al menos 384 bits (48 bytes)
  • ES512: al menos 512 bits (64 bytes)

En la nueva versión de jjwt, los métodos anteriores de firma y verificación son cadenas de la clave entrante, que está desactualizada. El método más nuevo requiere que se pase un objeto Key.

public class JwtUtils {
    
    
    // token时效:24小时
    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    // 签名哈希的密钥,对于不同的加密算法来说含义不同
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHOsdadasdasfdssfeweee";

    /**
     * 根据用户id和昵称生成token
     * @param id  用户id
     * @param nickname 用户昵称
     * @return JWT规则生成的token
     */
    public static String getJwtToken(String id, String nickname){
    
    
        String JwtToken = Jwts.builder()
                .setSubject("baobao-user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .claim("id", id)
                .claim("nickname", nickname)
                // 传入Key对象
                .signWith(Keys.hmacShaKeyFor(APP_SECRET.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256)
                .compact();
        return JwtToken;
    }

    /**
     * 判断token是否存在与有效
     * @param jwtToken token字符串
     * @return 如果token有效返回true,否则返回false
     */
    public static Jws<Claims> decode(String jwtToken) {
    
    
        // 传入Key对象
        Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(APP_SECRET.getBytes(StandardCharsets.UTF_8))).build().parseClaimsJws(jwtToken);
        return claimsJws;
    }
}

En el proyecto SpringBoot real, generalmente podemos usar el siguiente proceso para iniciar sesión:

  • Después de pasar la verificación de inicio de sesión, se genera un token aleatorio correspondiente para el usuario (tenga en cuenta que este token no se refiere a jwt, se puede generar usando algoritmos como uuid), y luego este token se usa como parte de la clave , la información del usuario se almacena en Redis como valor, y la fecha de caducidad se establece Hora, esta hora de caducidad es la hora en que el inicio de sesión deja de ser válido
  • Use el token aleatorio generado en el paso 1 como la carga útil de JWT para generar una cadena JWT y devolverla al front-end
  • Cada solicitud después del front-end lleva la cadena JWT en el campo de autorización en el encabezado de la solicitud
  • El backend define un interceptor. Cada vez que se recibe una solicitud de frontend, la cadena JWT se toma del campo Autorización en el encabezado de la solicitud y se verifica. Después de pasar la verificación, el token aleatorio en la carga útil se analiza y luego el aleatorio se usa el token Obtenga la clave, obtenga la información del usuario de Redis, si puede obtenerla, significa que el usuario ha iniciado sesión
public class JWTInterceptor implements HandlerInterceptor {
    
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        String JWT = request.getHeader("Authorization");
        try {
    
    
            // 1.校验JWT字符串
            DecodedJWT decodedJWT = JWTUtils.decode(JWT);
            // 2.取出JWT字符串载荷中的随机token,从Redis中获取用户信息
            ...
            return true;
        }catch (SignatureVerificationException e){
    
    
            System.out.println("无效签名");
            e.printStackTrace();
        }catch (TokenExpiredException e){
    
    
            System.out.println("token已经过期");
            e.printStackTrace();
        }catch (AlgorithmMismatchException e){
    
    
            System.out.println("算法不一致");
            e.printStackTrace();
        }catch (Exception e){
    
    
            System.out.println("token无效");
            e.printStackTrace();
        }
        return false;
    }
}

Supongo que te gusta

Origin blog.csdn.net/m0_73845616/article/details/127603451
Recomendado
Clasificación