JWT是什么?如何使用?

JWT是什么?如何使用?

  • 前言
  • 什么是JWT?
    • 概念
    • 工作方式
    • JWT的组成
      • Header
      • Payload
      • Signatrue
    • 实战
      • 引入依赖
      • 自定义注解
      • 定义实体类
      • 定义一个JWT工具类
      • 业务校验并生成token
      • 定义拦截器
      • 配置拦截器
      • 定义接口方法并添加注解
      • 开始验证
    • 使用场景
    • 注意事项
  • JWT与传统session认证的区别

前言

搜索这篇文章的小伙伴一定对【基于传统的session认证方式】有所了解,这里就不过多介绍了,我们直奔主题。

基于传统的session认证方式有几个显著的缺点:

  • 每一个登录的用户都需要在服务端存储一个session信息,一般都存储在服务器内存中,当用户数量过多,会过度占用服务器内存,也会增加服务器压力。
  • 对于分布式系统,还要需要解决共享session的问题。

那么,谁能够解决传统session认证方式存在的问题呢,他就是JWT。

什么是JWT?

概念

JWT(JSON WEB TOKEN):它是一种紧凑、安全的表示双方之间传输声明的方法。JWT是一个包含头部(Header)、负载(Payload)和签名(Signature)的JSON对象。JWT可用于认证和授权用户,它们是自包含的,意味着验证它们所需的所有信息都包含在令牌本身中。

  • 紧凑型:数据体积小,可通过POST请求参数或HTTP请求头发送。

  • 自包含:JWT 包含了主体的所有信息,避免了每个请求都需要向 Uaa 服务验证身份,降低了服务器的负载。

工作方式

  • 客户端登录时将用户信息传递给服务器,服务器使用用户信息通过密钥创建JWT,服务器不需要进行存储,直接将JWT返回给浏览器。
  • 客户端浏览器拿到JWT 后,存储在浏览器中,对于以后的每次请求,都不需要再通过授权服务来判断该请求的用户 以及该用户的权限。在微服务系统中,可以利用 JWT 实现单点登录

认证流程图如下:

在这里插入图片描述

JWT的组成

我们先来看看实际的JWT长什么样子:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

很复杂,看不懂是不是?其实这一串是经过加密之后的密文字符串,中间通过.来分割。每个.之前的字符串分别表示JWT的三个组成部分:头部(Header)、负载(Payload)和签名(Signature)。

Header

Header的主要作用是用来标识。通常是两部分组成:

  • typ:type 的简写,令牌类型,也就是JWT。
  • alg:Algorithm 的简写,加密签名算法。一般使用HS256,jwt官网提供了12种的加密算法,截图如下:

在这里插入图片描述
Header的明文示例:

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

经过Base64编码之后的明文,变为:

eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9

也就是第一个.之前的密文串。以下是Header部分常用部分的声明:
在这里插入图片描述

Payload

也称为JWT claims,放置需要传输的信息,有三类:

  • 保留claims:主要包括iss发行者、exp过期时间、sub主题、aud用户等。
  • 公共claims:定义新创的信息,比如用户信息和其他重要信息。
  • 私有claims:用于发布者和消费者都同意以私有的方式使用的信息。

以下是Payload的官方定义内容:
在这里插入图片描述
Payload明文示例:

{"sub": "12344321","name": "Mars酱", // 私有claims"iat": 1516239022
}

经过Base64加密之后的明文,变为:

eyJzdWIiOiIxMjM0NDMyMSIsIm5hbWUiOiJNYXJz6YWxIiwiaWF0IjoxNTE2MjM5MDIyfQ

也就是第一个.和第二个. 之间的密文串内容。

Signatrue

Signature 部分是对Header和Payload两部分的签名,作用是防止 JWT 被篡改。这个部分的生成规则主要是是公式(伪代码)是:

Header中定义的签名算法(base64编码(header) + "." + base64编码(payload), secret
)

secret是存放在服务端加密使用到的盐。

得到签名之后,把Header的密文、Payload的密文、Signatrue的密文按顺序拼接成为一个字符串,中间通过.来连接并分割,整个串就是JWT了。

实战

这里使用SpringBoot框架。

引入依赖

引入JWT依赖,由于是基于Java,所以需要的是java-jwt。

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

自定义注解

在这一步,我们在annotation包下定义一个用户需要登录才能进行其他接口访问等一系列操作的注解TokenRequired。

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenRequired {    boolean required() default true;
}

@Target旨意为我们自定义注解@TokenRequired的作用目标,因为我们本次注解的作用目标为方法层级,因此使用 ElementType.METHOD。

@Retention旨意为我们自定义注解 @TokenRequired的保留位置,@TokenRequired的保留位置被定义为RetentionPolicy.RUNTIME这种类型的注解将被JVM保留,他能在运行时被JVM或其他使用反射机制的代码所读取和使用。

定义实体类

在entity包中,我们使用lombok,简单自定义一个实体类User。

@Data
@AllArgsConstructor
@NoArgsConstructorpublic 
class User {String Id;String username;String password;
}

定义一个JWT工具类

在这一步,我们在util包下面创建一个JwtUtil工具类,用于生成token和校验token。

public class JwtUtil {//过期时间15分钟    private static final long EXPIRE_TIME = 15*60*1000;         //生成签名,15分钟后过期    public static String sign(String username,String userId,String password){        //过期时间        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);        //使用用户密码作为私钥进行加密        Algorithm algorithm = Algorithm.HMAC256(password);        //设置头信息        HashMap<String, Object> header = new HashMap<>(2);        header.put("typ", "JWT");        header.put("alg", "HS256");        //附带username和userID生成签名        return JWT.create().withHeader(header).withClaim("userId",userId)                .withClaim("username",username).withExpiresAt(date).sign(algorithm);    }//校验token    public static boolean verity(String token,String password){        try {            Algorithm algorithm = Algorithm.HMAC256(password);            JWTVerifier verifier = JWT.require(algorithm).build();            verifier.verify(token);            return true;        } catch (IllegalArgumentException e) {            return false;        } catch (JWTVerificationException e) {            return false;        }    }
}

业务校验并生成token

在service包下,我们创建一个UserService,并定义一个login方法,用于做登录接口的业务层数据校验,并调取JwtUtil中方法生成token。

@Service("UserService")
public class UserService {    
@Autowired	UserMapper userMapper;    public String login(String name, String password) {String token = null;try {//校验用户是否存在            User user = userMapper.findByUsername(name);if (user == null) {ResultDTO.failure(new ResultError(UserError.EMP_IS_NULL_EXIT));} else {//检验用户密码是否正确                if (!user.getPassword().equals(password)) {ResultDTO.failure(new ResultError(UserError.PASSWORD_OR_NAME_IS_ERROR));} else {// 生成token,将 user id 、userName保存到 token 里面                    token = JwtUtil.sign(user.getUsername(), user.getId(), user.getPassword());}}} catch (Exception e) {e.printStackTrace();}return token;}
}

Algorithm.HMAC256():使用HS256生成token,密钥则是用户的密码,唯一密钥的话可以保存在服务端。

withAudience()存入需要保存在token的信息,这里我把用户ID存入token中。

定义拦截器

接下来我们需要写一个拦截器去获取token并验证token。

public class AuthenticationInterceptor implements HandlerInterceptor {@AutowiredUserService userService;@Overridepublic boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {// 从 http 请求头中取出 tokenString token = httpServletRequest.getHeader("token");// 如果不是映射到方法直接通过if (!(object instanceof HandlerMethod)) {return true;}HandlerMethod handlerMethod = (HandlerMethod) object;Method method = handlerMethod.getMethod();//检查有没有需要用户权限的注解if (method.isAnnotationPresent(TokenRequired.class)) {TokenRequired userLoginToken = method.getAnnotation(TokenRequired.class);if (userLoginToken.required()) {// 执行认证if (token == null) {throw new RuntimeException("无token,请重新登录");}// 获取 token 中的 user idString userId;try {userId = JWT.decode(token).getClaim("userId").asString();} catch (JWTDecodeException j) {throw new RuntimeException("401");}User user = userService.findUserById(userId);if (user == null) {throw new RuntimeException("用户不存在,请重新登录");}// 验证 tokentry {if (!JwtUtil.verity(token, user.getPassword())) {throw new RuntimeException("无效的令牌");}} catch (JWTVerificationException e) {throw new RuntimeException("401");}return true;}}return true;}@Overridepublic void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {}
}

AuthenticationInterceptor拦截器实现了HandlerInterceptor接口的三个方法:

  • boolean preHandle ():
    预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller返回值,返回值为true会调用下一个拦截器或处理器,或者接着执行postHandle()和afterCompletion();false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。

  • void postHandle():
    后处理回调方法,实现处理器的后处理(DispatcherServlet进行视图返回渲染之前进行调用),此时我们可以通过modelAndView对模型数据进行处理或对视图进行处理,modelAndView也可能为null。

  • void afterCompletion():
    整个请求处理完毕回调方法,该方法也是需要当前对应的Interceptor的preHandle()的返回值为true时才会执行,也就是在DispatcherServlet渲染了对应的视图之后执行。用于进行资源清理。

该拦截器的执行流程为:

  1. 从 http 请求头中取出 token;
  2. 检查有没有需要用户权限的注解,如果需要,检验token是否为空;
  3. 如果token不为空,查询用户信息并校验token;
  4. 校验通过,则进行业务访问处理,校验失败则返回token失效信息。

配置拦截器

在配置类上添加了注解@Configuration,标明了该类是一个配置类并且会将该类作为一个SpringBean添加到IOC容器内。

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {@Beanpublic AuthenticationInterceptor authenticationInterceptor() {return new AuthenticationInterceptor();}@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 将我们上步定义的实现了HandlerInterceptor接口的拦截器实例authenticationInterceptor添加InterceptorRegistration中,并设置过滤规则,所有请求都要经过authenticationInterceptor拦截。registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/**");}
}

WebMvcConfigurer接口是Spring内部的一种配置方式,采用JavaBean的形式来代替传统的xml配置文件来实现基本的配置需要。

InterceptorConfig内的addInterceptor需要一个实现HandlerInterceptor接口的拦截器实例,addPathPatterns方法用于设置拦截器的过滤路径规则。

在addInterceptors方法中,我们将第6步定义的实现了HandlerInterceptor接口的拦截器实例authenticationInterceptor,添加至InterceptorRegistration中,并设置过滤路径。现在,我们所有请求都要经过authenticationInterceptor的拦截,拦截器authenticationInterceptor通过preHandle方法的业务过滤,判断是否有@TokenRequired 来决定是否需要登录。

定义接口方法并添加注解

@RestController
@RequestMapping("user")
public class UserController {@AutowiredUserService userService;/*** 用户登录     * @param user     * @return*/@PostMapping("/login")public ResultDTO login(User user) {String token = userService.login(user.getUsername(), user.getPassword());if (token == null) {return ResultDTO.failure(new ResultError(UserError.PASSWORD_OR_NAME_IS_ERROR));}Map<String, String> tokenMap = new HashMap<>();tokenMap.put("token", token);return ResultDTO.success(tokenMap);}@TokenRequired@GetMapping("/hello")public String getMessage() {return "你好哇,我是小码仔";}
}

不加注解的话默认不验证,登录接口一般是不验证的。所以我在getMessage()中加上了登录注解,说明该接口必须登录获取token后,在请求头中加上token并通过验证才可以访问。

开始验证

我在代码中对getMessage()添加了@TokenRequired注解,此刻访问该方法时必须要通过登录拿取到token值,并在请求头中添加token才可以访问。我们现在做以下校验:

  1. 直接访问,不在请求头里添加token:
    在这里插入图片描述
    如上图所示,请求结果显示:无token,请重新登录。

  2. 访问登录接口,获取token,并在请求头中添加token信息:
    在这里插入图片描述
    此时,访问成功。

  3. 15分钟后,token失效,我们再次在请求头中添加token信息访问:
    在这里插入图片描述
    此时token已失效,返回:无效的令牌。

使用场景

  • 授权:这是使用 JWT 的最常见的使用场景。用户登录后,每个后续请求都将包含 JWT,允许用户访问使用该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小,并且能够跨不同域轻松使用,还能解决CSRF问题。
  • 信息交换:JWT是在各方之间安全传输信息的比较便捷的方式。由于 JWT 可以签名(例如,使用公钥/私钥对),因此可以确定发送者是否是在您的授权范围之内。并且,由于签名是使用标头和有效负载计算的,因此还可以验证内容是否未被篡改。

注意事项

  • 不要存储敏感信息:JWT的Payload是可解码的,不应存储敏感信息,一般存储用户id。
  • 保护密钥:确保用于签名JWT的密钥安全且不可预测。
  • 设置合理的过期时间:避免JWT有效期过长。
  • HTTPS:在通过网络发送JWT时应使用HTTPS,以保护其免受中间人攻击。

JWT与传统session认证的区别

  • 工作原理不同:
    • Session 机制依赖于服务器端的存储。当用户首次登录时,服务器会创建一个会话,并生成一个唯一的会话 ID,然后将这个 ID 返回给客户端(通常是通过Cookie)。客户端在后续的请求中会携带这个会话 ID,服务器根据会话ID来识别用户并获取其会话信息;
    • JWT 是一种无状态的认证机制,它通过在客户端存储令牌(Token)来实现认证。当用户登录时,服务器会生成一个包含用户信息和有效期的 JWT,并将其返回给客户端。客户端在后续的请求中会携带这个 JWT,服务器通过验证 JWT 的有效性来识别用户。
  • 存储方式不同:
    • Session 信息存储在服务器端,通常是保存在内存或数据库中。这种方式需要服务器维护会话状态,因此在分布式系统或微服务架构中,会话信息的共享和同步可能会成为问题;
    • JWT信息存储在客户端,通常是保存在浏览器的本地存储或 HTTP 请求的头部中。这种方式无需服务器维护会话状态,使得 JWT 在分布式系统或微服务架构中更加灵活和易于扩展。
  • 有效期和灵活性不同:
    • Session 的有效期通常由服务器控制,并且在会话期间用户状态可以在服务器端动态改变。但这也意味着服务器需要管理会话的生命周期;
    • JWT 的有效期可以在令牌生成时设置,并且可以在客户端进行缓存和重复使用。这使得 JWT 在需要频繁访问资源且不需要频繁更改用户状态的场景中更加适用。此外,JWT 还支持在令牌中包含自定义的用户信息,提供了更大的灵活性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/660113.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024最新的,免费的 ChatGPT 网站AI(八个)

ChatGPT是美国人工智能研究实验室OpenAI在2022年11月推出的一款人工智能技术驱动的语言模型应用。它基于GPT-3.5架构&#xff08;后续还有GPT-4架构的升级版&#xff09;构建&#xff0c;拥有强大的自然语言处理能力和上下文理解能力&#xff0c;能够参与多轮对话&#xff0c;为…

【RAG 博客】Haystack 中的 DiversityRanker 与 LostInMiddleRanker 用来增强 RAG pipelines

Blog&#xff1a;Enhancing RAG Pipelines in Haystack: Introducing DiversityRanker and LostInTheMiddleRanker ⭐⭐⭐⭐ 文章目录 Haystack 是什么1. DiversityRanker2. LostInTheMiddleRanker使用示例 这篇 blog 介绍了什么是 Haystack&#xff0c;以及如何在 Haystack 框…

上位机图像处理和嵌入式模块部署(树莓派4b开机界面程序自启动)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 前面我们学习了如何在树莓派4b上面开发qt&#xff0c;也学习了如何用/etc/rc.local启动控制台程序&#xff0c;那今天我们继续学习一下如何利用树莓…

PotatoPie 4.0 实验教程(35) —— FPGA实现摄像头图像二值化膨胀效果

手机扫码 链接直达 https://item.taobao.com/item.htm?ftt&id776516984361 什么是图像二值化膨胀&#xff0c;有什么作用&#xff1f; 图像二值化膨胀是图像处理中的一种基本操作&#xff0c;它用于扩展和增强二值图像中的白色区域。具体而言&#xff0c;二值化膨胀操作…

【VSCode调试技巧】Pytorch分布式训练调试

最近遇到个头疼的问题&#xff0c;对于单机多卡的训练脚本&#xff0c;不知道如何使用VSCode进行Debug。 解决方案&#xff1a; 1、找到控制分布式训练的启动脚本&#xff0c;在自己的虚拟环境的/lib/python3.9/site-packages/torch/distributed/launch.py中 2、配置launch.…

微视网媒:引领新媒体时代的视觉先锋

在信息爆炸的时代&#xff0c;内容消费的方式日新月异&#xff0c;而“微视网媒”正是这场媒体变革中的佼佼者。凭借其独特的视角、精湛的制作和广泛的传播渠道&#xff0c;微视网媒不仅改变了人们获取信息的方式&#xff0c;更在不断地塑造着未来的媒体生态。 一、创新内容&am…

Hotcoin Research | 市场洞察:2024年4月22日-28日

加密货币市场表现 本周内加密大盘整体呈现出复苏状态&#xff0c;在BTC减半后进入到震荡上行周期。BTC在$62000-66000徘徊&#xff0c;ETH在$3100-3300徘徊&#xff0c;随着港交所将于 4 月 30 日开始交易嘉实基金的比特币和以太坊现货 ETF&#xff0c;周末行情有一波小的拉升…

【Qt】QtCreator忽然变得很卡

1. 问题 Qt Creator忽然变得很卡。电脑里两个版本的Qt Creator&#xff0c;老版本的开启就卡死&#xff0c;新版本好一点&#xff0c;但是相比于之前也非常卡&#xff0c;最明显的是在 ctrl鼠标滚轮 放大缩小的时候&#xff0c;要卡好几秒才反应。 2. 解决方案 2.1 方法1 关…

2000.1-2023.8中国经济政策不确定性指数数据(月度)

2000.1-2023.8中国经济政策不确定性指数数据&#xff08;月度&#xff09; 1、时间&#xff1a;2000.1-2023.8 2、指标&#xff1a;CNEPU&#xff08;经济政策不确定性指数&#xff09; 3、来源&#xff1a;China Economic Policy Uncertainty Index 4、用途&#xff1a;可…

虚析构与纯虚析构

这里的new Cat("Tom"&#xff09;是由于基类函数中的构造函数里面带有string变量 1. 法一:利用虚函数&#xff0c;虚化基类中的析构函数 virtual ~Animal() { cout << "动物的析构函数调用" << endl; } 2. 法二:利用纯…

188页 | 2023企业数字化转型建设方案(数据中台、业务中台、AI中台)(免费下载)

1、知识星球下载&#xff1a; 如需下载完整PPTX可编辑源文件&#xff0c;请前往星球获取&#xff1a;https://t.zsxq.com/19KcxSeyA 2、免费领取步骤&#xff1a; 【1】关注公众号 方案驿站 【2】私信发送 2023企业数字化转型建设方案 【3】获取本方案PDF下载链接&#xff0…

文件分块+断点续传 实现大文件上传全栈解决方案(前端+nodejs)

1. 文件分块 将大文件切分成较小的片段&#xff08;通常称为分片或块&#xff09;&#xff0c;然后逐个上传这些分片。这种方法可以提高上传的稳定性&#xff0c;因为如果某个分片上传失败&#xff0c;只需要重新上传该分片而不需要重新上传整个文件。同时&#xff0c;分片上传…