当前位置: 首页 > article >正文

登录认证(4):令牌技术:JWT令牌

如上文所说(登录认证(1):登录的基本逻辑及实现思路登录),因为 HTTP协议是无状态的协议,我们需要使用会话跟踪技术实现同一会话中不同请求之间的数据共享,但Cookie技术Session技术都有各自的使用局限,所以说在当今的企业开发中,越来越青睐令牌技术 来进行会话跟踪。

令牌技术概览

在登录认证中,令牌是用户的身份标识,是合法的身份凭证,好像十分神秘和高大上,但其本质是一个字符串。如果使用令牌技术进行会话跟踪,在浏览器发起请求,请求登录接口,如果登录成功,那么就在服务端生成一个令牌令牌就是用户的合法身份凭证,在响应数据的时候,就可以将令牌直接响应给前端。

前端程序接收到令牌之后,就需要将令牌存储起来,可以存储在cookie中,也可以存储在localStorage这样的其他存储空间。之后,在后续的每一次请求中,都需要携带令牌,服务端的统一拦截器需要校验令牌的有效性。如果令牌有效,则说明用户已经执行了登录操作,拦截器就可以放行;如果令牌无效(解析令牌报错),那么则说明用户没有执行登录操作,拦截器就需要拦截,并返回错误代码,让用户登录。整个流程如下图所示:

此时,在同一个会话的多次请求之间,我们就通过将数据存储在令牌中的方式完成了数据共享。令牌技术有很多优点,比如:支持多端,不但支持PC端,而且支持移动令牌技术也可以解决服务器集群的认证问题,因为只需要成功解析令牌,就可以证明令牌是有效的,是无需在服务器存储的令牌的安全性也非常强悍。但也是因为其强悍的性能,令牌使用起来会更加的复杂,但是这些劣势在优势面前就不值一提了。

JWT令牌

令牌的形式有很多,本文讲解功能强大、使用最广泛的JWT令牌。JWT令牌(JSON Web Token),定义了一种简洁的自包含的格式,可用于通信双方以Json数据格式安全的传输信息。

特性

简洁

JWT令牌的本质就是字符串,可以作为请求参数或者在请求头中直接传递。

自包含

JWT令牌虽然是一个字符串,但是可以根据需求,在令牌中存储自定义的数据内容,比如在登录操作中,可以在JWT令牌中存储用户相关信息。

安全

简单理解,JWT令牌就是将原本简单的Json数据进行了安全的封装,这样就可以直接在JWT令牌中进行信息传输了,并且由于数字签名的存在,这些信息是安全可靠的

组成

JWT令牌三个部分组成,每个部分之间使用.进行分隔。一个完整的JWT令牌如图所示:

第一部分:Header(头)

该部分主要是记录令牌类型令牌使用的签名算法等。例如:{"alg":"HS256", "type":"JWT"},从这个Header信息就可以看出:这是一个JWT令牌,使用了HS256签名算法

第二部分:Payload(有效载荷)

该部分主要是携带一些自定义的信息,或者一些默认的信息等,例如:{"id":"1","username":"Tom"},从这个Payload信息可以看出:这个令牌携带的数据是一个用户数据,id为1,username为Tom

第三部分:Signature(签名)

这个部分主要是令牌的签名,签名可以防止Token被篡改,可以提高令牌的安全性。其构成是将HeaderPayload两个部分,加入指定密钥(Secret),并通过指定的签名算法计算而来。正是因为数字签名,所以说JWT令牌是非常安全的,一旦令牌中的任何一个部分、任何一个字符被篡改了,整个令牌在校验时都会失效。

Base64编码

那么JWT令牌是如何将原始的Json数据,转变为字符串的呢?在生成JWT令牌的时候,对原始数据进行了Base64编码这并非是一种加密方式,只是一种编码方式)。

Base64编码:是一种基于64个可打印的字符来表示二进制数据的编码方式。所使用的64个字符分别是A到Za到z0-9一个加号(+),一个斜杠(/),加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。在有些情况下,Basae64编码可能会出现一个等号(=)。等号是一个补位的符号。

使用

想要使用JWT令牌,首先需要引入JWT对应的Maven坐标

<!-- JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
生成JWT令牌

在引入了JWT依赖之后,就可以使用对应的工具类Jwts提供的API来完成JWT令牌生成与校验

/**
 * 生成JWT令牌
 */
@Test
public void testGenerateJWT() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("id", 10);
    claims.put("username", "wzb");
    // 通过Jwts工具类中的builder方法构建一个JWT令牌
    String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "hello") // 加密算法和签名
            .addClaims(claims) // 添加数据:键值对
            .setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000)) // 设置JWT令牌有效期
            .compact(); // 生成令牌
    log.info("jwt令牌是:{}", jwt);
}

这个代码主要是使用Jwts工具类提供的API生成一个JWT令牌。使用signWith方法,指定该令牌的加密算法为HS256,并且指定签名(这里的签名指定的十分粗糙,实际开发中需要指定更加复杂的签名来增加令牌安全性。);Jwt令牌中的数据都是以键值对的形式出现的,所以说可以直接把需要的数据封装为一个Map,然后使用addClaims方法,将Map中的数据封装到令牌中;然后再使用setExpiration方法,设置令牌的有效时间,此处设置的有效时间是12h;最后使用compact方法生成JWT令牌。运行这个测试方法,得到的JWT令牌

eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoid3piIiwiZXhwIjoxNzM3NTk0NDgxfQ.m4BT-AR8cMCEmMJoNNL0iuyNCHylKdasuu77ETsLHcw

将该令牌拿到JWT令牌官网(JSON Web Tokens - jwt.io)解析: 

JWT令牌的最后一个部分并非Base64编码,而是签名算法计算的,所以说最后一个部分不会解析。

成功解析令牌内容,加密算法、数据等都和代码封装得相同,说明我们成功生成了一个JWT令牌

校验JWT令牌

校验(解析)JWT令牌和生成一样,同样需要使用Jwts工具类提供的API

/**
 * 解析JWT令牌
 */
@Test
public void testParseJWT() {
    Claims claims = Jwts.parser().setSigningKey("hello")
            .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoid3piIiwiZXhwIjoxNzM3NDI1MTg" +
                    "5fQ.cHdgtWD-BtrnM2Xnl418CcB_6h-rB2ogGSK8aT4T0dk")
            .getBody();
    log.info("JWT解析结果为:{}", claims);
}

解析JWT令牌必须要使用和生成时同样的签名,通过Jwts中的parser方法解析一个JWT令牌,必须在setSignKey方法中传递和生成时一样的签名,否则解析失败;在parseClaimsJws方法中传递需要解析的JWT令牌,即可解析其Payload部分,也就是令牌中的数据;最后通过getBody方法即可获得令牌数据的键值对对象Claims,Claims继承了Map类,可以理解为一种特殊的Map。让我们用该程序解析刚才生成的JWT令牌

成功解析令牌,说明解析令牌方法成功。JWT令牌的解析(校验)十分严格,只要有一点错误,不论哪个部分,都无法解析,即使我们只是改变了一个字符的大小写,在运行解析方法的时候都会报错,可以看出JWT令牌的可靠性。过期的JWT令牌也无法解析,令牌过期之后,令牌就会马上失效,解析就会失败。

利用JWT令牌完善登录功能

上文已经对JWT令牌做出了详细的解释与分析,JWT令牌最典型的应用就是登录,所以说让我们用JWT令牌技术来完善登录功能。具体思路前面已经分析过了,主要分为两步操作:

生成令牌

在用户登录成功之后,生成一个JWT令牌,并且把这个令牌直接返回给客户端,之后的每一个请求,都要携带这个令牌。

校验令牌

请求携带了JWT令牌,统一拦截器需要拦截请求,从请求中获取令牌,并对令牌进行解析,如果成功解析,那么就放行;如果解析失败,则返回错误代码

我们先要创建一个JwtUtils工具类,这个工具类提供生成、解析令牌的方法,以便程序使用:

package com.wzb.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.Map;

public class JWTUtils {

    // 独创数字签名
    private static final String signKey = "hello";

    // 令牌过期时间
    private static final Long expire = 43200000L;

    /**
     * 生成的JWT令牌
     * @param claims JWT令牌中的数据,键值对
     * @return JWT令牌
     */
    public static String generateJWT(Map<String, Object> claims) {
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, signKey)
                .addClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                .compact();
    }

    /**
     * 解析JWT令牌
     * @param jwt JWT令牌
     * @return Claims,JWT令牌中的数据
     */
    public static Claims parseJWT(String jwt) {
        return Jwts.parser()
                .setSigningKey(signKey)
                .parseClaimsJws(jwt)
                .getBody();
    }

}

然后改造登录方法:

/**
 * 员工登录
 *
 * @param emp 登录请求数据封装的Emp实体对象
 * @return LoginInfo员工登录信息
 */
@Override
public LoginInfo login(Emp emp) {
    Emp empLogin = empMapper.getUserByUsernameAndPassword(emp);
    Map<String, Object> claims = new HashMap<>();
    if (empLogin != null) {
        Integer id = empLogin.getId();
        String username = empLogin.getUsername();
        String name = empLogin.getName();
        claims.put("id", id);
        claims.put("username", username);
        return new LoginInfo(id, username, name, JWTUtils.generateJWT(claims));
    }
    return null;
}

从数据库中查询到员工的信息之后,将其以键值对的方式封装到Map中,然后调用工具类中的方法生成JWT令牌之后,封装到登录信息类LoginInfo中返回。此时,就成功给客户端返回了令牌,此时再次发起登录请求: 

 

成功给客户端响应JWT令牌

总结

JWT令牌是现在越来越流行,使用越来越广泛的会话跟踪技术,可以在多端使用,并且有极强的安全性能,还可以应对服务器集群问题,是解决登录认证问题的最佳选择。现在已经有了令牌来标识用户已经登录,但是程序仍然没有对请求进行拦截,验证其是否登录,统一拦截器的部分且听下文分解。


http://www.kler.cn/a/514576.html

相关文章:

  • 在 vscode + cmake + GNU 工具链的基础上配置 JLINK
  • SentencePiece和 WordPiece tokenization 的含义和区别
  • 如何在 Pytest 中使用命令行界面和标记运行测试
  • CPU狂飙900%如何分析?怎么定位?怎么溯源处理
  • JavaScript学习笔记(1)
  • 多监控m3u8视频流,怎么获取每个监控的封面图(纯前端)
  • Selenium-WEB自动化测试环境配置
  • springBoot 整合ModBus TCP
  • EJB如何管理事务
  • Qt Creator 15.0.0如何更换主题和字体
  • 【Java】常用工具类方法:树形结构、获取IP、对象拷贝、File相关、雪花算法等
  • ubuntu 安装显卡驱动gpu-cuda
  • TiDB 对 Hadoop 的影响:大数据时代的新选择
  • 深入理解 Java 的并发容器
  • MongoDB部署模式
  • 《论文阅读》GPT-3是否会产生移情对话?一种新的情境示例选择方法和用于生成同理心对话的自动评估度量 ICCL 2022
  • 手机网络性能测试仪器介绍
  • c#配置config文件
  • 布局预览问题
  • 持续升级《在线写python》小程序的功能,文章页增加一键复制功能,并自动去掉html标签
  • Python从0到100(八十五):神经网络-使用迁移学习完成猫狗分类
  • 快速构建springboot+vue后台管理系统
  • 【C++学习篇】滑动窗口--结合例题讲解思路
  • FPGA自分频产生的时钟如何使用?
  • 风光并网对电网电能质量影响的matlab/simulink仿真建模
  • NLTK句法分析与依存解析