热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入浅出JWT

JWT(JSONWEBTOKEN)的组成https:jwt.ioheader(头部)承载两部分信息:声明
JWT(JSON WEB TOKEN)的组成

https://jwt.io/

header(头部)承载两部分信息:

  • 声明类型,这里是JWT;
  • 声明加密的算法,通常直接使用 HMAC SHA256

playload(载荷)就是存放有效信息的地方,这些有效信息包含三个部分:

  • 标准中注册的声明;
  • 公共的声明;
  • 私有的声明;

signature(签证信息)

  • header+.+payload进行base64编码,然后实用签证算法和密钥加密

在这里插入图片描述

三种认证流程

基于session+COOKIE的认证流程


  • 用户在浏览器中输入用户名和密码,服务器通过密码校验后生成一个session并保存到数据库
  • 服务器为用户生成一个sessionId,并将具有sesssionId的COOKIE放置在用户浏览器中,在后续的请求中都将带有这个COOKIE信息进行访问
  • 服务器获取COOKIE,通过获取COOKIE中的sessionId查找数据库判断当前请求是否有效

不足之处:

  • 服务器压力增大:通常session是存储在内存中的,每个用户通过认证之后都会将session数据保存在服务器的内存中,而当用户量增大时,服务器的压力增大。
  • CSRF跨站伪造请求攻击:session是基于COOKIE进行用户识别的, COOKIE如果被截获,用户就会很容易受到跨站请求伪造的攻击。
  • 扩展性不强:如果将来搭建了多个服务器,虽然每个服务器都执行的是同样的业务逻辑,但是session数据是保存在内存中的(不是共享的),用户第一次访问的是服务器1,当用户再次请求时可能访问的是另外一台服务器2,服务器2获取不到session信息,就判定用户没有登陆过。

基于JWT的认证流程

JWT保存在客户端

  • 用户在浏览器中输入用户名和密码,服务器通过密码校验后生成一个token并保存到数据库
  • 前端获取到token,存储到COOKIE或者local storage中,在后续的请求中都将带有这个token信息进行访问。当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 COOKIE 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。
  • 服务器获取token值,通过查找数据库判断当前token是否有效

JWT和session的优缺点


  • 都保存了用户身份信息,都有过期时间。session翻译为会话,token翻译为令牌。session是空间换时间,token是时间换空间。

  • JWT保存在客户端,在分布式环境下不需要做额外工作。而session因为保存在服务端,分布式环境下需要实现多机数据共享

  • session一般需要结合COOKIE实现认证,所以需要浏览器支持COOKIE,因此移动端无法使用session认证方案

  • 安全性:JWT的payload使用的是base64编码的,因此在JWT中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。

  • 性能:经过编码之后JWT将非常长,COOKIE的限制大小一般是4k,COOKIE很可能放不下,所以JWT一般放在local storage里面。并且用户在系统中的每一次http请求都会把JWT携带在Header里面,HTTP请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用JWT的HTTP请求比使用session的开销大得多

  • 一次性:无状态是JWT的特点,但也导致了这个问题,JWT是一次性的。想修改里面的内容,就必须签发一个新的JWT,若想废弃,一种常用的处理手段是结合redis或者续约


什么是 Token(令牌)

Acesss Token

Acesss Token是访问资源接口(API)时所需要的资源凭证。简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign。每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里,基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库token 完全由应用管理,所以它可以避开同源策略

特点:

  • 服务端无状态化、可扩展性好
  • 支持移动端设备
  • 安全
  • 支持跨程序调用

token 的身份验证流程:

  • 客户端使用用户名跟密码请求登录
  • 服务端收到请求,去验证用户名与密码
  • 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端
  • 客户端收到 token 以后,会把它存储起来,比如放在 COOKIE 里或者 localStorage 里
  • 客户端每次向服务端请求资源的时候需要带着服务端签发的 token
  • 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据

Refresh Token

refresh token 是专用于刷新 access token 的 token。如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。

  • Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录了。
  • Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样一直保持在内存中以应对大量的请求。

Token 和 JWT 的区别

相同:

  • 都是访问资源的令牌
  • 都可以记录用户的信息
  • 都是使服务端无状态化
  • 都是只有验证成功后,客户端才能访问服务端上受保护的资源

区别:

  • Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否效。
  • JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。

SpringBoot整合JWT

JWT使用流程


  • 用户登录成功后,获取token
  • 单体项目创建拦截器,分布式项目创建网关
  • 配置拦截器

引入依赖

<dependency><groupId>com.auth0groupId><artifactId>java-jwtartifactId><version>3.18.1version>
dependency>

创建JWT工具类

public class JWTUtil {private static final Logger logger &#61; LoggerFactory.getLogger(JWTUtil.class);//私钥private static final String TOKEN_SECRET &#61; "123456";/*** 生成token&#xff0c;自定义过期时间 毫秒*/public static String generateToken(UserTokenDTO userTokenDTO) {try {// 私钥和加密算法Algorithm algorithm &#61; Algorithm.HMAC256(TOKEN_SECRET);// 设置头部信息Map<String, Object> header &#61; new HashMap<>(2);header.put("Type", "Jwt");header.put("alg", "HS256");return JWT.create().withHeader(header).withClaim("token", JSONObject.toJSONString(userTokenDTO))//.withExpiresAt(date).sign(algorithm);} catch (Exception e) {logger.error("generate token occur error, error is:{}", e);return null;}}/*** 检验token是否正确*/public static UserTokenDTO parseToken(String token) {Algorithm algorithm &#61; Algorithm.HMAC256(TOKEN_SECRET);JWTVerifier verifier &#61; JWT.require(algorithm).build();DecodedJWT jwt &#61; verifier.verify(token);String tokenInfo &#61; jwt.getClaim("token").asString();return JSON.parseObject(tokenInfo, UserTokenDTO.class);}
}

Redis工具类

public final class RedisServiceImpl implements RedisService {/*** 过期时长*/private final Long DURATION &#61; 1 * 24 * 60 * 60 * 1000L;&#64;Resourceprivate RedisTemplate redisTemplate;private ValueOperations<String, String> valueOperations;&#64;PostConstructpublic void init() {RedisSerializer redisSerializer &#61; new StringRedisSerializer();redisTemplate.setKeySerializer(redisSerializer);redisTemplate.setValueSerializer(redisSerializer);redisTemplate.setHashKeySerializer(redisSerializer);redisTemplate.setHashValueSerializer(redisSerializer);valueOperations &#61; redisTemplate.opsForValue();}&#64;Overridepublic void set(String key, String value) {valueOperations.set(key, value, DURATION, TimeUnit.MILLISECONDS);log.info("key&#61;{}, value is: {} into redis cache", key, value);}&#64;Overridepublic String get(String key) {String redisValue &#61; valueOperations.get(key);log.info("get from redis, value is: {}", redisValue);return redisValue;}&#64;Overridepublic boolean delete(String key) {boolean result &#61; redisTemplate.delete(key);log.info("delete from redis, key is: {}", key);return result;}&#64;Overridepublic Long getExpireTime(String key) {return valueOperations.getOperations().getExpire(key);}
}

登录方法

public String login(LoginUserVO loginUserVO) {//1.判断用户名密码是否正确UserPO userPO &#61; userMapper.getByUsername(loginUserVO.getUsername());if (userPO &#61;&#61; null) {throw new UserException(ErrorCodeEnum.TNP1001001);}if (!loginUserVO.getPassword().equals(userPO.getPassword())) {throw new UserException(ErrorCodeEnum.TNP1001002);}//2.用户名密码正确生成tokenUserTokenDTO userTokenDTO &#61; new UserTokenDTO();PropertiesUtil.copyProperties(userTokenDTO, loginUserVO);userTokenDTO.setId(userPO.getId());userTokenDTO.setGmtCreate(System.currentTimeMillis());String token &#61; JWTUtil.generateToken(userTokenDTO);//3.存入token至redisredisService.set(userPO.getId(), token);return token;
}

登出方法

public boolean loginOut(String id) {boolean result &#61; redisService.delete(id);if (!redisService.delete(id)) {throw new UserException(ErrorCodeEnum.TNP1001003);}return result;
}

修改密码方法

public String updatePassword(UpdatePasswordUserVO updatePasswordUserVO) {// 1.修改密码UserPO userPO &#61; UserPO.builder().password(updatePasswordUserVO.getPassword()).id(updatePasswordUserVO.getId()).build();UserPO user &#61; userMapper.getById(updatePasswordUserVO.getId());if (user &#61;&#61; null) {throw new UserException(ErrorCodeEnum.TNP1001001);}if (userMapper.updatePassword(userPO) !&#61; 1) {throw new UserException(ErrorCodeEnum.TNP1001005);}// 2.生成新的tokenUserTokenDTO userTokenDTO &#61; UserTokenDTO.builder().id(updatePasswordUserVO.getId()).username(user.getUsername()).gmtCreate(System.currentTimeMillis()).build();String token &#61; JWTUtil.generateToken(userTokenDTO);// 3.更新tokenredisService.set(user.getId(), token);return token;
}

拦截器验证

public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception {String authToken &#61; request.getHeader("Authorization");String token &#61; authToken.substring("Bearer".length() &#43; 1).trim();UserTokenDTO userTokenDTO &#61; JWTUtil.parseToken(token);//1.判断请求是否有效if (redisService.get(userTokenDTO.getId()) &#61;&#61; null || !redisService.get(userTokenDTO.getId()).equals(token)) {return false;}//2.判断是否需要续期if (redisService.getExpireTime(userTokenDTO.getId()) < 1 * 60 * 30) {redisService.set(userTokenDTO.getId(), token);log.error("update token info, id is:{}, user info is:{}", userTokenDTO.getId(), token);}return true;
}

配置拦截器

&#64;Configuration
public class InterceptorConfig implements WebMvcConfigurer {&#64;Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(authenticateInterceptor()).excludePathPatterns("/logout/**") //开发的路径.excludePathPatterns("/login/**") //开发的路径.addPathPatterns("/**");}&#64;Beanpublic AuthenticateInterceptor authenticateInterceptor() {return new AuthenticateInterceptor();}
}

不使用redis的JWT

可以参考&#xff1a;https://juejin.cn/post/6962142423879270437

单点登录

将token或者一个唯一标识UUID&#61;UUID.randomUUID().toString()存进COOKIE中&#xff08;别存在Http的header中了&#xff09;&#xff0c;设置路径为整个项目根路径/*&#xff1b; 往往以这个唯一标识为key&#xff0c;用户信息为value缓存在服务器中&#xff01;&#xff01;&#xff01;


推荐阅读
  • 本文探讨了为何采用RESTful架构及其优势,特别是在现代Web应用开发中的重要性。通过前后端分离和统一接口设计,RESTful API能够提高开发效率,支持多种客户端,并简化维护。 ... [详细]
  • 手把手教你构建简易JSON解析器
    本文将带你深入了解JSON解析器的构建过程,通过实践掌握JSON解析的基本原理。适合所有对数据解析感兴趣的开发者。 ... [详细]
  • 本文介绍了在处理财务凭证查询时,如何实现从插入或修改页面返回至原始查询结果页面,并确保数据保持最新状态的方法。通过使用JavaScript和Java的Session管理技术,解决了动态页面中AJAX调用失效的问题。 ... [详细]
  • django项目中使用手机号登录
    本文使用聚合数据的短信接口,需要先获取到申请接口的appkey和模板id项目目录下创建ubtils文件夹,定义返回随机验证码和调取短信接口的函数function.py文件se ... [详细]
  • 使用 NDB 提升 Node.js 应用调试体验
    本文介绍了由 Google Chrome 实验室推出的新一代 Node.js 调试工具 NDB,旨在为开发者提供更加高效和便捷的调试解决方案。 ... [详细]
  • 本文基于https://major.io/2014/05/13/coreos-vs-project-atomic-a-review/的内容,对CoreOS和Atomic两个操作系统进行了详细的对比,涵盖部署、管理和安全性等多个方面。 ... [详细]
  • Iris 开发环境配置指南 (最新 Go & IntelliJ IDEA & Iris V12)
    本指南详细介绍了如何在最新的 Go 语言环境及 IntelliJ IDEA 中配置 Iris V12 框架,适合初学者和有经验的开发者。文章提供了详细的步骤说明和示例代码,帮助读者快速搭建开发环境。 ... [详细]
  • 本文介绍了如何从给定的JSON响应中正确地提取产品标题等信息。 ... [详细]
  • 本文探讨了在Android平台下编写和读取.JSON文件的方法,解决读取文件时遇到的字符间异常空格问题。 ... [详细]
  • 本文详细介绍了如何利用go-zero框架从需求分析到最终部署至Kubernetes的全过程,特别聚焦于微服务架构中的网关设计与实现。项目采用了go-zero及其生态组件,涵盖了从API设计到RPC调用,再到生产环境下的监控与维护等多方面内容。 ... [详细]
  • 本文详细介绍了如何通过 `vue.config.js` 文件配置 Vue CLI 的打包和代理设置,包括开发服务器配置、跨域处理以及生产环境下的代码压缩和资源压缩。 ... [详细]
  • 本文详细介绍了C++标准模板库(STL)中各容器的功能特性,并深入探讨了不同容器操作函数的异常安全性。 ... [详细]
  • 本文探讨了一个特定的问题:当应用程序通过安装器启动后最小化,再次打开时,会触发窗口丢失错误,导致应用重启,并且之前的异步线程无法正常管理。这一现象在直接从应用图标启动时不会出现。 ... [详细]
  • IOSG Weekly Brief | Fat NFT Thesis 与艺术朋克 #68
    IOSG Weekly Brief | Fat NFT Thesis 与艺术朋克 #68 ... [详细]
  • 教程:如何打造令人印象深刻的GitHub个人主页Readme
    本文将指导您如何创建一个既专业又个性化的GitHub个人主页Readme,通过添加统计数据、常用语言和最近活动等元素,让您的主页更加吸引人。 ... [详细]
author-avatar
囌格菈帥厎
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有