热门标签 | 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;


推荐阅读
  • adfs是什么_培训与开发的概念
    adfs是什么_培训与开发的概念(如您转载本文,必须标明本文作者及出处。如有任何疑问请与我联系me@nap7.com)ADFS相关开发技术的中文资料相对匮乏,之前在弄这个东西的时候 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文介绍了高校天文共享平台的开发过程中的思考和规划。该平台旨在为高校学生提供天象预报、科普知识、观测活动、图片分享等功能。文章分析了项目的技术栈选择、网站前端布局、业务流程、数据库结构等方面,并总结了项目存在的问题,如前后端未分离、代码混乱等。作者表示希望通过记录和规划,能够理清思路,进一步完善该平台。 ... [详细]
  • web.py开发web 第八章 Formalchemy 服务端验证方法
    本文介绍了在web.py开发中使用Formalchemy进行服务端表单数据验证的方法。以User表单为例,详细说明了对各字段的验证要求,包括必填、长度限制、唯一性等。同时介绍了如何自定义验证方法来实现验证唯一性和两个密码是否相等的功能。该文提供了相关代码示例。 ... [详细]
  • Servlet多用户登录时HttpSession会话信息覆盖问题的解决方案
    本文讨论了在Servlet多用户登录时可能出现的HttpSession会话信息覆盖问题,并提供了解决方案。通过分析JSESSIONID的作用机制和编码方式,我们可以得出每个HttpSession对象都是通过客户端发送的唯一JSESSIONID来识别的,因此无需担心会话信息被覆盖的问题。需要注意的是,本文讨论的是多个客户端级别上的多用户登录,而非同一个浏览器级别上的多用户登录。 ... [详细]
  • 微信官方授权及获取OpenId的方法,服务器通过SpringBoot实现
    主要步骤:前端获取到code(wx.login),传入服务器服务器通过参数AppID和AppSecret访问官方接口,获取到OpenId ... [详细]
  • .NetCoreWebApi生成Swagger接口文档的使用方法
    本文介绍了使用.NetCoreWebApi生成Swagger接口文档的方法,并详细说明了Swagger的定义和功能。通过使用Swagger,可以实现接口和服务的可视化,方便测试人员进行接口测试。同时,还提供了Github链接和具体的步骤,包括创建WebApi工程、引入swagger的包、配置XML文档文件和跨域处理。通过本文,读者可以了解到如何使用Swagger生成接口文档,并加深对Swagger的理解。 ... [详细]
  • 1.Listener是Servlet的监听器,它可以监听客户端的请求、服务端的操作等。通过监听器,可以自动激发一些操作,比如监听在线的用户的数量。当增加一个HttpSession时 ... [详细]
  • 这篇文章主要介绍PHP如何使用在全部作用域中始终可用的内置变量,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • 本文介绍了在Hibernate配置lazy=false时无法加载数据的问题,通过采用OpenSessionInView模式和修改数据库服务器版本解决了该问题。详细描述了问题的出现和解决过程,包括运行环境和数据库的配置信息。 ... [详细]
  • 关于我们EMQ是一家全球领先的开源物联网基础设施软件供应商,服务新产业周期的IoT&5G、边缘计算与云计算市场,交付全球领先的开源物联网消息服务器和流处理数据 ... [详细]
  • SQLiLabs靶场的介绍、下载与安装
    介绍SQLi-Labs是一个专业的SQL注入练习平台下面的测试场景都支持GET和POST两种注入方式:1.报错注入(联合查询)1)字符型2)数字型2.报错注入(基于 ... [详细]
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社区 版权所有