用户打开浏览器,访问站点服务器,连续操作(连续的访问站点服务器web资源),直到关闭浏览器,这个整个过程就叫做会话。会话技术是一种在网络通信中用于跟踪用户状态的机制,它可以让服务器在处理用户请求时保持特定用户的状态信息,从而实现个性化的服务和用户体验。
HTTP 协议是一种无状态协议,每个请求都是相互独立的,服务器无法直接识别来自同一个用户的连续请求。这导致了一些问题,例如无法跟踪和管理用户状态信息、无法提供个性化服务等。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。于是会话技术就应运而生了。
为了解决这些问题,人们开始探索如何实现会话管理。最初的方案是基于 Cookie 的会话管理。Cookie 是一种在客户端存储数据的机制,服务器可以在响应中设置 Cookie,然后在后续的请求中读取 Cookie。通过将会话标识符(Session ID)存储在 Cookie 中,服务器能够识别特定用户的请求并保持其状态信息。Cookie 在浏览器中存储,可以设置过期时间,也可以通过设置路径和域名来限制 Cookie 的范围。
为了更好地管理会话,开发人员开始使用服务器端的会话存储来替代完全依赖于 Cookie。这就是 Session-Based Session 技术(Session技术)。服务器会为每个会话分配一个唯一的 Session ID,并将该 ID 存储在服务器端的会话存储中,而不是直接依赖于客户端的 Cookie。客户端的 Cookie 只包含 Session ID,服务器通过 Session ID 来查找对应的会话数据。这样可以增加对会话的控制和安全性,但需要在服务器端维护会话存储。
随着移动设备和分布式系统的兴起,无状态会话管理变得更加重要。Token-Based Session 技术(Token技术)应运而生。在这种技术中,服务器使用 Token(令牌)来表示会话状态。当用户登录成功后,服务器生成一个加密的 Token,并将其发送给客户端。客户端在后续的请求中将 Token 带上,服务器通过解析和验证 Token 来识别和管理会话。由于 Token 是无状态的,服务器不需要维护会话存储,可以更好地适应分布式环境。
Cookie是一种在客户端(通常是Web浏览器)和服务器之间传输的小型文本文件。它由服务器通过HTTP响应的头部设置,并存储在客户端的浏览器中。当客户端发送后续请求时,会将该Cookie信息包含在HTTP请求头中发送给服务器。
Cookie认证流程通常包括以下步骤:
- 用户访问需要身份验证的网站。如果用户未经过身份验证,则服务器将重定向用户到登录页面。
- 用户输入用户名和密码,发送Http请求传递给服务器进行验证。
- 服务器验证用户的凭据,并创建一个会话来保存用户的身份验证状态。在这个过程中,服务器生成一个唯一的标识符Session ID,并将其存储在服务器端。
- 服务器将Session ID发送到客户端的浏览器中,通常是将Cookie放在Http请求响应的请求头中,在请求头中有一系列的以Set-Cookie为键的键值对,在这些键值对中包含有Session ID、到期日期、作用域和有效时间等信息。
- 当用户发送后续请求时,浏览器会将Session ID作为Cookie的一部分自动包含在HTTP请求头中。
- 服务器解析请求头中的Session ID,并将Cookie中的Session ID与数据库中的Session进行比较以验证用户的有效性。
- 如果会话ID有效且相关联的身份验证状态信息表明用户已经通过身份验证,则服务器可以允许请求并提供所需的响应。否则,服务器可能会拒绝请求或重定向用户到登录页面以重新验证。
下面是Cookie认证流程图
package com.kjz;
import com.kjz.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* HttpCookie演示
*/
@Slf4j
@RestController
public class SessionController {
//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","kjz")); //设置Cookie/响应Cookie
return Result.success();
}
//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
}
}
return Result.success();
}
}
}
A.访问 c1 接口,设置Cookie ,http://localhost:8080/c1
我们可以看到,设置的cookie,通过响应头Set-Cookie响应给浏览器,并且浏览器会将Cookie,存储在浏览器端。
B. 访问c2接口 http://localhost:8080/c2,此时浏览器会自动的将Cookie携带到服务端,是通过请求头Cookie,携带的
优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的)
缺点:
移动端APP(Android、IOS)中无法使用Cookie
不安全,用户可以自己禁用Cookie
Cookie不能跨域
Session是一种在Web应用程序中跨请求保持用户状态的机制。
Session是一段服务器上的存储区域,用于存储用户信息和状态。当用户第一次访问Web应用程序时,服务器会创建一个Session,并给它分配一个唯一的标识符(session ID),然后将该标识符发送给客户端。客户端收到session ID后,通常会将其存储在cookie中,以便后续请求时将其发送回服务器。服务器通过session ID可以找到对应的Session,并从中读取或修改用户信息和状态。session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中
- session 认证流程:
- 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
- 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
- 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
- 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。
根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。
以下是流程图:
package com.kjz;
import com.kjz.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* HttpSession演示
*/
@Slf4j
@RestController
public class SessionController {
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1: {}", session.hashCode());
session.setAttribute("loginUser", "tom"); //往session中存储数据
return Result.success();
}
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info("HttpSession-s2: {}", session.hashCode());
Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
}
}
A. 访问 s1 接口,http://localhost:8080/s1
请求完成之后,在响应头中,就会看到有一个Set-Cookie的响应头,里面响应回来了一个字符串,就是JSESSIONID,这个就是服务端会话对象 Session 的ID。
Session ID被存储在浏览器的Cookie中。
B. 访问 s2 接口,http://localhost:8080/s2
接下来,在后续的每次请求时,都会将Cookie的值,携带到服务端,那服务端呢,接收到Cookie之后,会自动的根据JSESSIONID的值,找到对应的会话对象Session。
两次请求,获取到的Session会话对象的hashcode是一样的,就说明是同一个会话对象。而且,第一次请求时,往Session会话对象中存储的值,第二次请求时,也获取到了。 那这样,我们就可以通过Session会话对象,在同一个会话的多次请求之间来进行数据共享了。
优点:Session是存储在服务端的,安全
缺点:
服务器集群环境下无法直接使用Session
移动端APP(Android、IOS)中无法使用Cookie
用户可以自己禁用Cookie
Cookie不能跨域
- 安全性: Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。
- 存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
- 有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。
- 数据存储:
- Cookie:Cookie是以键值对的形式存储在客户端的文本文件中。每个Cookie都有名称、值、过期时间、域名等属性。Cookie的数据容量通常有限制,一般为几KB。
- Session:Session将用户状态数据存储在服务器端的内存或数据库中。因此,Session可以存储更大量的数据,并且不受Cookie容量限制。
- 生命周期:
- Cookie:可以设置Cookie的过期时间,在过期时间之前,Cookie会一直保留在客户端,除非被删除或过期。
- Session:Session的生命周期由服务器控制,通常在用户关闭浏览器或一段时间不活动后过期。过期后,服务器会清理掉相应的Session数据。
Cookie和Session通常是结合使用的。服务器使用Session来管理用户的状态和敏感信息,而将Session ID存储在Cookie中发送给客户端。客户端在后续的请求中会自动携带Cookie中的Session ID,服务器通过Session ID来识别和恢复对应的Session,从而实现跨请求的状态保持。
作为计算机术语时,是“令牌”的意思。Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
token其实说的更通俗点可以叫暗号,在一些数据传输之前,要先进行暗号的核对,不同的暗号被授权不同的数据操作。说白了token是一个身份卡,有权限的作用。例如在USB1.1协议中定义了4类数据包:token包、data包、handshake包和special包。主机和USB设备之间连续数据的交换可以分为三个阶段,第一个阶段由主机发送token包,不同的token包内容不一样(暗号不一样)可以告诉设备做不同的工作,第二个阶段发送data包,第三个阶段由设备返回一个handshake包。
在Web应用程序中,有各种类型的Token用于身份验证和授权。以下是一些常见的Token类型:
访问令牌(Access Token):用于访问受保护的资源,通常用于OAuth 2.0授权框架中。当用户通过认证服务器获得授权后,会收到一个访问令牌,该令牌可以被发送到资源服务器以获取受保护资源。
刷新令牌(Refresh Token):用于获取新的访问令牌,通常也用于OAuth 2.0授权框架中。刷新令牌可以用来获取新的访问令牌,以避免用户需要重新进行认证流程。
JSON Web Token(JWT):一种开放标准(RFC 7519),用于在各方之间安全地传输信息。JWT通常被用作访问令牌,它包含了被加密的用户信息和其他元数据,可以被用于身份验证和授权。
ID令牌(ID Token):也通常用于OAuth 2.0认证框架中,用于向客户端提供已认证用户的身份信息。ID令牌通常被用于OpenID Connect协议中。
CSRF令牌(Cross-Site Request Forgery Token):用于防止跨站请求伪造攻击。CSRF令牌通常是一个随机生成的值,它会被包含在表单提交中,服务器会验证该令牌以确定请求是否合法。
身份令牌(Identity Token):用于表示用户的身份信息,通常在身份验证过程中使用。
下面详细介绍一下Access Token和Refresh Token这两者常见的令牌
Access Token(访问令牌)是OAuth 2.0授权框架中的一种令牌类型,用于标识和验证客户端的访问权限。在OAuth 2.0认证流程中,用户通过认证服务器进行身份验证后,会获得一个Access Token,然后将该Token用于访问受保护的资源服务器。
Token认证流程:
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端
- 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 token
- 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据
- 每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里
- 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
- token 完全由应用管理,所以它可以避开同源策略
Refresh Token(刷新令牌)是OAuth 2.0授权框架中的一种令牌类型,用于获取新的Access Token(访问令牌),以避免用户需要重新进行认证流程。在OAuth 2.0授权流程中,Refresh Token通常是在用户通过认证服务器进行身份验证后,一并返回给客户端的。
特点:
生命周期较长:相对于Access Token而言,Refresh Token的生命周期较长,可以用来获取多个Access Token,以延长用户的认证状态。
不包含用户信息:Refresh Token通常不包含任何用户信息,只包含用于获取新Access Token的信息,如客户端ID、密钥、过期时间等。
需要安全存储:由于Refresh Token可以用来获取新的Access Token,因此需要被妥善保管,并且在传输过程中需要采用加密等安全措施。
工作流程:
用户在客户端进行OAuth 2.0授权流程,通过认证服务器进行身份验证,获取Access Token和Refresh Token。
客户端在后续的请求中使用Access Token进行API访问,当Access Token过期时,客户端会使用Refresh Token向认证服务器请求新的Access Token。
认证服务器收到Refresh Token,并验证其有效性和权限,如果验证通过,则生成并返回新的Access Token。
客户端使用新的Access Token进行后续的API访问,直到新的Access Token过期或者再次使用Refresh Token获取新的Access Token。
使用注意点
Refresh Token可以避免用户频繁进行认证流程,提高用户体验。但是它也需要被妥善保管,以避免被恶意利用。在使用Refresh Token时,需要注意以下几点:
安全存储:Refresh Token应该被安全地存储,通常建议使用加密等技术保护其安全性。
过期时间控制:Refresh Token应该具有一定的过期时间,并且客户端需要及时更新Refresh Token,以避免过期而导致无法获取新的Access Token。
权限控制:Refresh Token应该和Access Token一样,具有权限控制机制,确保只有拥有合法Refresh Token的客户端才能获取新的Access Token。
下面是工作流程图:
优点:
支持PC端、移动端
解决集群环境下的认证问题
减轻服务器的存储压力(无需在服务器端存储)
服务端无状态化、可扩展性好
缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验)
前面介绍了基于令牌技术来实现会话追踪。这里所提到的令牌就是用户身份的标识,其本质就是一个字符串。令牌的形式有很多,接下来介绍一下一种实现方案:功能强大的 JWT令牌。
JWT全称:JSON Web Token (官网:JSON Web Tokens - jwt.io)
定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
简洁:是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。
自包含:指的是jwt令牌,看似是一个随机的字符串,但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如:可以直接在jwt令牌中存储用户的相关信息。
简单来讲,jwt就是将原始的json数据格式进行了安全的封装,这样就可以直接基于jwt在通信双方安全的进行信息传输了。
JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)
第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}
第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}
第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
签名的目的就是为了防jwt令牌被篡改,而正是因为jwt令牌最后一个部分数字签名的存在,所以整个jwt 令牌是非常安全可靠的。一旦jwt令牌当中任何一个部分、任何一个字符被篡改了,整个令牌在校验的时候都会失败,所以它是非常安全可靠的。
JWT是如何将原始的JSON格式数据,转变为字符串的呢?
其实在生成JWT令牌时,会对JSON格式的数据进行一次编码:进行base64编码
Base64:是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码,那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号。
需要注意的是Base64是编码方式,而不是加密方式。
下面说一下基于java代码如何生成和校验JWT令牌
首先我们先来实现JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依赖:
io.jsonwebtoken
jjwt
0.9.1
然后通过调用工具包中提供的API来完成JWT令牌的生成和校验
工具类:JWTUtils
public class JWTUtils {
private static String signKey = "kjzshicaibi";//签名密钥
private static Long expire = 43200000L; //有效时间
/**
* 生成JWT令牌
* @param claims JWT第二部分负载 payload 中存储的内容
* @return
*/
public static String generateJwt(Map claims){
String jwt = Jwts.builder()
.addClaims(claims)//自定义信息(有效载荷)
.signWith(SignatureAlgorithm.HS256, signKey)//签名算法(头部)
.setExpiration(new Date(System.currentTimeMillis() + expire))//过期时间
.compact();
return jwt;
}
/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)//指定签名密钥
.parseClaimsJws(jwt)//指定令牌Token
.getBody();
return claims;
}
}
通过测试类进行测试
JWT生成:
/**
* 生成JWT
*/
@Test
public void testGenJwt(){
Map claims = new HashMap<>();
claims.put("id",1);
claims.put("name","tom");
String jwt = JwtUtils.generateJwt(claims);
System.out.println(jwt);
}
输出结果为
输出的结果就是生成的JWT令牌,,通过英文的点分割对三个部分进行分割,我们可以将生成的令牌复制一下,然后打开JWT的官网,将生成的令牌直接放在Encoded位置,此时就会自动的将令牌解析出来。
第一部分解析出来,看到JSON格式的原始数据,所使用的签名算法为HS256。
第二个部分是我自定义的数据,之前我们自定义的数据就是id,还有一个exp代表的是我所设置的过期时间。
由于前两个部分是base64编码,所以是可以直接解码出来。但最后一个部分并不是base64编码,是经过签名算法计算出来的,所以最后一个部分是不会解析的。
另外在这里插一嘴,我调试的过程中出现了以下的bug
产生错误原因:生成JWT的密钥signKey字符串过短引起的异常。
解决方法:尽量将signKey字符串设置长。
之前设置signKey字符串仅仅3个字母字符串 "kjz",现在修改为"kjzshicaibi"
解析JWT:
@Test
public void testParseJwt(){
Claims claims = JwtUtils.parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTcwMDgzMzkyN30.tF3EmoKNR5PE-vVZcv0VKgpiaaVgp-mYtkqpBVRbtF4");
System.out.println(claims);
}
输出结果
- 用户向服务器发送凭据(例如用户名和密码)以进行身份验证。
- 服务器验证用户提供的凭据。如果凭据有效,则服务器生成一个JWT,并将其作为响应返回给客户端。
- 客户端收到JWT后,将其存储在本地,通常是在浏览器的Cookie或本地存储中。
- 客户端在后续请求中将JWT作为身份验证凭据包含在HTTP请求头中,键值对如下(Authorization: Bearer
) - 服务器接收到带有JWT的请求后,会对JWT进行验证,确保它是由服务器签发的有效令牌。
- 如果JWT验证成功且未过期,服务器将处理请求并提供所需的响应。服务器可以从JWT中获取附加的声明信息,如用户ID、角色等,以进行授权和个性化操作。
- 如果JWT验证失败或已过期,服务器会拒绝请求或要求客户端重新进行身份验证。
流程图如下
下面是JWT运用到登录场景的一个例子
1.引入JWT工具类
这里的工具类与上面提到的一致就不重复引用了。
2.登录完成后,调用工具类生成JWT令牌并返回
@RestController
@Slf4j
public class LoginController {
//依赖业务层对象
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
//调用业务层:登录功能
Emp loginEmp = empService.login(emp);
//判断:登录用户是否存在
if(loginEmp !=null ){
//自定义信息
Map claims = new HashMap<>();
claims.put("id", loginEmp.getId());
claims.put("username",loginEmp.getUsername());
claims.put("name",loginEmp.getName());
//使用JWT工具类,生成身份令牌
String token = JwtUtils.generateJwt(claims);
return Result.success(token);
}
return Result.error("用户名或密码错误");
}
}
利用开发者工具,抓取一下网络请求
登录请求完成后,可以看到JWT令牌已经响应给了前端,此时前端就会将JWT令牌存储在浏览器本地。
服务器响应的JWT令牌存储在本地浏览器哪里了呢?
在当前案例中,JWT令牌存储在浏览器的本地存储空间Local Storage中了。 Local Storage是浏览器的本地存储,在移动端也是支持的。
之后再发起一个查询数据的请求,此时可以看到在请求头中包含一个token(JWT令牌),后续的每一次请求当中,都会将这个令牌携带到服务端。
注意此处我是在前端发请求时自定义了一个名为token的键值对,将JWT放在这个键值对中。一般情况下,默认都是把token放在Authorization的键值对中。
写到此处我的心中又产生了一个问题,为什么要把JWT放在请求头中而不是请求行、请求体中呢
放在请求头中的原因:
安全性:
将JWT放在请求头中可以避免敏感信息被记录在URL中或者浏览器历史记录中。这是因为,请求头中的内容不会自动包含在浏览器历史记录中,也不会自动写入Web服务器的访问日志文件中,从而增加了身份验证信息的安全性。
标准化:
将JWT放在请求头中符合HTTP协议规范。HTTP协议定义了请求行(包括方法、URI和协议版本)和请求头两个部分,其中请求头用于传递一些附加的信息,比如身份验证信息、请求正文类型等。因此,将JWT放在请求头中符合HTTP协议的设计理念,而将它放在请求行或请求体中可能会导致与HTTP协议不兼容的问题。
将JWT放在请求头中也方便了API开发和使用的标准化。HTTP请求头具有标准的格式和语法,这使得API的开发和使用更加规范和易于实现。相比之下,将JWT放在请求行或者请求体中,可能会需要开发人员自己定义格式和语法,这会增加开发和使用的成本和风险
不放在请求行中的原因
安全性问题:
请求行通常会被记录在服务器的访问日志中,也可能会被缓存或代理服务器记录下来。如果将JWT放在请求行中,就会将敏感的身份验证信息暴露在这些记录中,增加了被恶意利用的风险。
长度限制:
请求行的长度是有限制的,根据HTTP协议规范,请求行的总长度(包括方法、URI和协议版本等)应该不能超过几千个字节(不同的浏览器配置不同)。如果将JWT放在请求行中,可能会导致JWT太长而无法满足长度限制,导致请求失败或被截断。
处理复杂:
请求行的解析和处理通常由HTTP服务器或客户端库自动完成。将JWT放在请求行中可能会导致服务器或库无法正确解析请求行,需要进行额外的处理和解析逻辑,增加了开发和维护的复杂性
不放在请求体中的原因
安全性问题:
请求体通常会被缓存、记录在服务器的日志中、传输到代理服务器中等。将JWT放在请求体中可能会将敏感信息暴露在这些记录和传输中,增加安全风险。
兼容性问题:
有些浏览器或客户端可能无法正确处理带有请求体的HTTP请求,或者在处理请求体时出现问题。这可能导致请求失败或出现其他错误。