- 什么是jwt
- jsonwebtoken (JWT)是一个开放标准(rfc7519).它定义了一种紧凑的,自包含式的方式,用于在各方之间以JSON对象安全地传输信息,此信息可以验证和信任,因为他是数字签名的,jwt可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名
- 通俗解释
- JWT简称JSON Web Token ,也就是通过json形式作为web应用的令牌,用在各方之间安全的将信息作为json对象传输,在数据传输过程中还可以完成数据加密,签名相关处理
- JWT能做什么?
- 授权
- 这是使用jwt的最常见方案,一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该领牌允许的路由,服务和资源,单点登录是的当今广泛使用JWT的一项功能,因为他的开销很小,并且可以在不同的域中使用
- 信息交换
- JSON Web Toekn 是在各方面之间安全地传输信息的好方法,因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人,此外由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改
- 授权
- 为什么是JWT?
- 基于传统的session认证
- 认证方式
- 我们知道,http协议本身是一种无状态协议,而这就意味着用户向我们的应用提供了用户名和密码来进行用户验证,那么下一次请求时,用户还需要再一次进行用户认证才行,因为根据http协议,我们并不知道是哪个用户发出的请求,所以为了让我们的用户知道是哪一个用户发送的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应信息时传递给浏览器,告诉其保存为cookie以便下次请求是发送给我们的应用,这样我们的应用就能识别请求来自于那个用户了,这就是传统的基于session认证
- 认证流程
- 暴露问题
- 每个用户经过我们的应用认证之后,我们的应用都要在服务器端做一次记录,以便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销就会明显增大
- 用户认证之后,服务端就做认证记录,如果认证的记录被保存到内存中的话,这意味着用户下次请求还必须要请求在这台服务器上面,这样才可以拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力,这也意味着限制了应用的扩展能力
- 因为是基于cookie来进行用户识别的,cookie如果被获取,用户就很容易受到跨站请求伪造的攻击
- 在前后端分离系统中就更加痛苦了,如下图所示:
- 也就是说前后端分离在应用解耦后增加了部署的复杂性,通常用户,一次请求要转发多次,如果使用session 每次携带sessionid 到服务器,服务器还要查询用户信息,同事如果用户很多,这些信息存储在服务器内存中,给服务器增加负担,还有就是CSRF(跨站伪造请求攻击),session是基于cookie进行用户识别的,cookie如果被截获用户容易受到跨站请求伪造的攻击,还有就是sessionid就是一个特征值,表达的信息不够丰富,不容易扩展,而且如果你后端应用是多个节点部署,那么就需要实现session共享机制,不方便集群
- 认证方式
- 基于JWT认证
- 认证流程
- 首先,前端通过web表单将自己的用户名和密码发送到后端接口,这一过程一般是一个HTTP,POST请求,建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探
- 后端核对用户名和密码 成功后,将用户的id等其他信息最为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(token),形成的JWT就是一个形同111.zzz.xxx的字符串
- 后端将JWT字符串作为登录成功的返回结果给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录是前端删除保存的JWT即可
- 前端将每次请求的JWT都放入放入HTTP Header 中的Authorization为(解决XSS和XSRF问题)
- 后端检查是否存在,如存在检查JWT的有效性。例如,检查签名是否正确;检查Token是否过期,检查Token的接收方收否是自己(可选)
- 验证通过后后端使用JWT中包含的用户信息进行其他的逻辑操作,返回相应的结果
- jwt优势
- 简介(Compact):可以通过URL,POST参数或者在HTTP header中发送,因为数据量小,所以传输的速度也快
- 自包含(Self-contained): 负载中包含了所有用户所需的信息,避免了多次查询数据库
- 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何Web形式都支持
- 不要再服务端保存信息,特别适用于分布式微服务
- 认证流程
- 基于传统的session认证
- JWT的结构是什么?
- 令牌的组成
- 有效的标头(Header)
- 有效负载(Payload)
- 签名(Signature)
- 因此JWT通常如下所示 xxx.yyy.zzz, Header.Payload.Signature
- Header
- 通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC,SHA265或RSA,它会使用Base64编码组成JWT结构的一部分
- 注意: Base64是一种编码,也就是说,他是可以被翻译回原来的样子的,他并不是一种加密的过程
- { "alg":"HS365", "typ":"JWT"}
- PayLoad
- 令牌的第二部分是有效负载,其中包含声明,声明有关实体(通常是指用户)和其他数据的声明。同样的他会使用Base64编码组成JWT结构的第二部分
- {"sub":"12334798", "name":"John Doe", "admin":true}
- Signature
- 前面两部分都是使用Base64进行编码的,即前端可以解开知道里面的信息,Signature需要使用编码后的Header和Payload以及我们提供的一个密钥,然后使用Header中指定的签名算法(HS256)进行签名,没有被篡改过:
- 如:
- HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payLoad).secret)
- # 签名的目的
- 最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被篡改,如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合成新的JWT的话,那么服务器会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的,如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
- 最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被篡改,如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合成新的JWT的话,那么服务器会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的,如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
- # 信息安全的问题
- 在这里大家一定会问一个问题,:Base64是一种编码,是可逆的,那么我的信息不就暴露了嘛?
- 是的,所以,在JWT中不应该在负载里面加入任何敏感的数据,在上面的例子中,我们传输的是用户的User ID,这个值实际上不是什么敏感内容,一般情况下被知道也是安全的,到那时像密码这样的内容就不可以放在JWT中了,如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了,因此JWT适合用于向Web应用传递一些非敏感信息,JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录
- 在这里大家一定会问一个问题,:Base64是一种编码,是可逆的,那么我的信息不就暴露了嘛?
- 令牌的组成
- 使用JWT
- # 先引入依赖
1 <!--引入jwt--> 2 <dependency> 3 <groupId>com.auth0</groupId> 4 <artifactId>java-jwt</artifactId> 5 <version>3.4.0</version> 6 </dependency>
- 生成token
1 Calendar instance = Calendar.getInstance(); 2 Calendar instance = Calendar.getInstance(); 3 instance.add(Calendar.SECOND, 60); 4 String token = JWT.create() 5 .withHeader(map) 6 .withClaim("userId", 60) 7 .withClaim("username", "xiao") 8 .withExpiresAt(instance.getTime()) 9 .sign(Algorithm.HMAC256("!@Qw#$R"));// 签名System.out.println(token); 10 生成结果 11 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDY4OTY1MzIsInVzZXJJZCI6NjAsInVzZXJuYW1lIjoieGlhbyJ9.eblcFVkLQuE-TQTuWgqYpI-GOLOZthZ4_fZBB6XR2nA
- 根据令牌和签名解析数据
1 JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("!@Qw#$R")).build(); 2 DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDY4OTY1MzIsInVzZXJJZCI6NjAsInVzZXJuYW1lIjoieGlhbyJ9.eblcFVkLQuE-TQTuWgqYpI-GOLOZthZ4_fZBB6XR2nA");
3 System.out.println(verify.getClaim("userId").asInt());
4 System.out.println(verify.getClaim("username").asString()); 5 System.out.println(verify.getExpiresAt()); - 封装成工具类
1 package com.example.utils; 2 import com.auth0.jwt.JWT; 3 import com.auth0.jwt.JWTCreator; 4 import com.auth0.jwt.algorithms.Algorithm; 5 import com.auth0.jwt.interfaces.DecodedJWT; 6 import java.util.Calendar; 7 import java.util.Map; 8 /** * @Author: LiuLi * @Description: * @Date: Create in 16:21 2020/12/2 */ 9 public class JWTUtils { 10 private static final String SING = "!Q@W3142%^TY"; 11 /** * 生成token header.payload.sing */ 12 public static String getToken(Map<String, String> map) { 13 Calendar instance = Calendar.getInstance(); 14 instance.add(Calendar.DATE, 7); // 默认是7天的时间 // 创建 JWT builder JWTCreator.Builder builder = JWT.create(); 15 map.forEach((k, v) -> { 16 builder.withClaim(k, v); 17 }); 18 String token = builder 19 .withExpiresAt(instance.getTime()) // 指定令牌的过期时间 .sign(Algorithm.HMAC256(SING));// 签名 return token; 20 } 21 /** * 验证token的合法性 */ 22 public static void verify(String token) { 23 JWT.require(Algorithm.HMAC256(SING)).build().verify(token); 24 } 25 /** * 获取token的方法 */ 26 public static DecodedJWT getTokenInfo(String token) { 27 DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING)).build().verify(token); 28 return verify; 29 } 30 }
- # 先引入依赖