概述
会话从字面理解就是"两方交流",那问题就来了,HTTP(超文本传输协议)里面的"传输"不就包含了"两方交流"的意思吗?为什么要多此一举提出会话技术呢?
谈到这个,我们就需要先了解 HTTP 协议。HTTP 协议是一中无状态的协议,它的每次请求都是相互独立的,那就产生了一个问题,"交流"的另一方,也就是服务器,面对这么多请求,如何判断每一个请求都是再和谁"交流"?所以这就衍生出了会话技术。
说到这里,那大家都明白了。会话技术就是用来解决"交流"过程中,身份识别的问题。解决了这个问题,才能够跟踪和管理用户状态信息、提供个性化服务等等。
会话技术发展到现在,主流的是三种方案
- Cookie
- Session
- Token
下面我们依次讲解这三个会话跟踪方案
1.Cookie
首先我们要解决什么 Cookie 这个问题?
Cookie 是一种客户端会话跟踪方案
,本质就是浏览器端的一种"键值对"存储机制,相当于客户端和服务器之间传输的一串字符串。Cookie 存储在客户端浏览器中。当我们使用 Cookie 来跟踪会话时,服务器 会在 HTTP 响应头部的set-cookie
字段中设置 Cookie。当客户端发送后续请求时,会将该 Cookie 信息包含在 HTTP 请求头中发送给服务器。
那解决信息不共享的问题,我们是不是就可以通过 Cookie 进行解决了。最简单的实现方式,是不是只需要将身份信息当作 Cookie 就可以了。说到这里相信你你已经很清楚 Cookie 的作用了。
那为了方便在请求中携带 Cookie,我们还需要遵循 HTP 协议。Cookie 也是 HTP 协议当中所支持的技术,各大浏览器厂商都支持该标准。所以浏览器接受到响应回来的 Cookie 时,会自动 的将 Cookie 存储在浏览器中,后续的每一次请求中,浏览器会自动的将本地存储的 Cookie 携带到服务端中。服务端接收到请求中,可以通过解释 Cookie,判断瀛湖的信息了,这样就解决了在不同的请求中进行身份信息共享的问题。

接下来我们继续学习如何具体的使用 Cookie 进行身份认证交流
Cookie 认证流程通常包括以下步骤:
- 用户访问需要身份验证的网站。如果用户未经过身份验证,则服务器将重定向用户到登录页面
- 这部分内容需要另一个技术:统一拦截技术,不是我们本次讨论的重点
- 用户输入用户名和密码,发送 Http 请求传递给服务器进行验证。
- 服务器验证用户的凭据,并创建一个会话来保存用户的身份验证状态。在这个过程中,服务器生成一个 Cookie,包含了用户的唯一标识信息
- 服务器将 Cookie 设置在响应头中的 set-cookie 字段中,然后发送响应
- 当用户发送后续请求时,浏览器会将 Cookie 设置请求头中的 cookie 字段中
- 服务器解析请求头中的 Cookie,并解析 Cookie 进行身份的校验工作

代码实现
java
@Slf4j
@RestController
public class SessionController {
// 设置 Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
// 设置 Cookie
response.addCookie(new Cookie("username","AirMan"));
return Result.success();
}
// 获取 Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
// 校验 Cookie
for (Cookie cookie : cookies) {
if(cookie.getName().equals("username")){
// 输出 name 为 username 的 cookie
System.out.println("username: " + cookie.getValue());
}
}
return Result.success();
}
}
为什么有了 Cookie 还有 Session,Token 技术的出现呢?那是不是就说明了 Cookie 有一些缺点?
Cookie 最主要的缺点就是移动端 APP(Android、IOS) 中无法使用 Cookie ;其次就是 Cookie 不能跨域 ;还有就是不安全 ,客户端可以随意修改 Cookie 内容,服务端很难保证数据真实性。
什么是跨域?
只要协议不同(http,https),IP 不同,端口不同,满足其中之一都叫做跨域。
因为现在的项目都是前后端分离的,前端部署在服务器 192.168.150.200
上,端口 80
,后端部署在 192.168.150.100
上,端口 8080
,所以因为跨域的原因,服务器设置的 Cookie 将无法使用。

2.Session
Session 解决了 Cookie 的什么问题?
Session 主要解决 Cookie 不安全的问题。如果将用户的手机号,密码等敏感信息存放在 Cookie 中,那么风险对于双方都是很大的。对于浏览器端,容易发生泄露问题。对于服务器端,因为客户端可以随意修改 Cookie 内容,服务端很难保证数据真实性。
那下面我们来讲它是如何解决的
Session 的底层就是基于我们刚才所介绍的 Cookie 来实现的。
它是服务器端会话跟踪技术
,所以它是存储在服务器端的。当用户第一次访问 Web 应用程序时,服务器会创建一个 Session,并给它分配一个唯一的标识符(session ID),然后将该标识符发送给客户端。客户端收到 session ID 后,通常会将其存储在 cookie 中,以便后续请求时将其发送回服务器。服务器通过 session ID 可以找到对应的 Session,并从中读取或修改用户信息和状态。session 存储在服务器端,sessionId 会被存储到客户端的 cookie 中。

看到这里相比你就明白了,Session 技术无非就是请求头中的 cookie 字段保存的是 sessionID,响应头的 set-cookie 字段保存的也是 sessionID,而具体的用户信息则保存在服务端。sessionID 只起到一个标识的作用。
所以 Cookie 更像是一个"身份证",每次访问网站时出示,表明"我是谁,我的身份证号是多少"。
而Session 更像是"指纹认证",每次访问网站时出示,不需要表明"我是谁..."。指纹所对于的信息全保存在服务端
Session 的流程和 Cookie 类似,就不再赘述,代码实现如下,下面的代码和 Cookie 很相似,但千万不要混稀了。Session 是在服务端的,loginUser 的内容不会存放到 Cookie 中。Cookie 只存放一个 SessionID,浏览器端是没有 loginUser 这个信息的。
java
@Slf4j
@RestController
public class SessionController {
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1: {}", session.hashCode());
// 往 session 中存储数据,不是往 Cookie 中存放内容!
session.setAttribute("loginUser", "AirMan");
return Result.success();
}
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info("HttpSession-s2: {}", session.hashCode());
// 从 session 中获取数据,也就是从服务器中取内容
Object loginUser = session.getAttribute("loginUser");
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
}
}
为什么有了 Session 还有 Token 技术的出现呢?那是不是就说明了 Session 有一些缺点?
Session 的底层就是基于 Cookie 来实现的。Session 除了解决 Cookie 不安全的问题,其他问题都继承过来了:
- 服务器集群环境下无法直接使用 Session
- 现在的项目基本都是集群部署,Session 保存在一个服务器中,如果这个服务器宕机了,其他服务器是没有 SessionID 等信息的。早期解决方案是通过服务器实时同步解决,但这样每台服务器都要存放,开销就很大。更好的解决方案是通过** Redis 解决共享 Session 的问题**,这里不过多讨论
- 移动端 APP(Android、IOS) 中无法使用 Cookie
- Cookie 不能跨域
那下面我们就要解决
3.Token
Token 解决了 Session 和 Cookie 的什么问题?
它解决了移动端无法使用的问题,也解决了集群环境无法使用 Cookie 的问题
那下面我们来讲 Token 的实现思路
Token 是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个 Token 便将此 Token 返回给客户端,客户端接收到令牌之后,就需要将这个令牌存储起来。这个存储可以存储在 cookie 当中,也可以存储在其他的存储空间(比如:Authorizatrion 字段)当中。在后续的每一次请求当中,都需要将令牌携带到服务端。携带到服务端之后,接下来我们就需要来校验令牌的有效性。如果令牌是有效的,就说明用户已经执行了登录操作,如果令牌是无效的,就说明用户之前并未执行登录操作
所以 Token 就成为了"交流"的暗号,暗号正确了,我们才能继续交流。那么暗号的生成一定要安全。
所以 Token 的优点显而易见
- 支持PC端、移动端
- 解决集群环境下的认证问题
- 减轻服务器的存储压力(无需在服务器端存储)
唯一的缺点就是需要自己实现 Token 的生成(包括令牌的生成、令牌的传递、令牌的校验)
Token 的类型有很多,企业开发中常使用的是 JSON Web Token(JWT),JWT 是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。JWT 通常被用作**访问令牌**,它包含了被加密的用户信息和其他元数据,可以被用于身份验证和授权,其本质就是一个字符串。
所以 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是编码方式,而不是加密方式)
利用 JWT 进行会话维持之后,整个流程如下
- 在浏览器发起请求来执行登录操作,此时会访问登录的接口,如果登录成功之后,我们需要生成一个jwt令牌,将生成的 jwt令牌返回给前端。
- 前端拿到jwt令牌之后,会将jwt令牌存储起来。在后续的每一次请求中都会将jwt令牌携带到服务端。
- 服务端统一拦截请求之后,先来判断一下这次请求有没有把令牌带过来,如果没有带过来,直接拒绝访问,如果带过来了,还要校验一下令牌是否是有效。如果有效,就直接放行进行请求的处理。
具体的代码实现:
首先需要引入 JWT 的依赖
xml
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
java
@Test
public void genJwt(){
Map<String,Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("username","Tom");
String jwt = Jwts.builder()
.setClaims(claims) // 自定义内容(载荷)
.signWith(SignatureAlgorithm.HS256, "AirMan") // 设置签名算法和签名使用的秘钥
.setExpiration(new Date(System.currentTimeMillis() + 24*3600*1000)) // 设置过期时间
.compact();
System.out.println(jwt);
}
java
public static Claims parseJWT(String secretKey, String token) {
// 得到 DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的 jwt
.parseClaimsJws(token)
.getBody();
return claims;
}
通过上述代码即可生成一个 JWT 令牌并进行解析,需要注意的是密钥一定尽可能的复杂
另外为了让浏览器请求的时候自动携带 Token ,所以当登录请求完成后,前端就会将 JWT 令牌存储在浏览器本地。
服务器响应的JWT令牌存储在本地浏览器哪里了呢?
在当前案例中,JWT令牌存储在浏览器的本地存储空间Local Storage中了。 Local Storage是浏览器的本地存储。JWT 令牌存储在浏览器的本地存储空间Local Storage
中了。Local Storage
是浏览器的本地存储,在移动端也是支持的

之后再发起一个查询数据的请求,此时可以看到在请求头中包含一个 token(JWT令牌),后续的每一次请求当中,都会将这个令牌携带到服务端。一般情况下,默认都是把 token 放在 Authorization 的键值对中。当然也可以自定义请求头中的键值对,如下

最终提供给大家一个 JWT 的工具类
java
public class JwtUtil {
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘钥
*
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息
* @return
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);
// 设置jwt的body
String jwtStr = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp)
.compact();
return jwtStr;
}
/**
* Token解密
*
* @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
* @param token 加密后的token
* @return
*/
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token)
.getBody();
return claims;
}
}