文章目录
一、Cookie 与 Session
在我们访问页面时,通常是通过 HTTP 协议的,该协议的特点之一是** 无状态**。当我们通过HTTP协议去访问某一个网站,此时就会向网站服务端发送请求。HTTP 协议确保每一个请求都是独立的,于是服务端无法确定当前请求的身份,而 Cookie 和 Session可以解决这个问题。
1.1 Cookie
Cookie 存储在客户端,比如访问网站时的浏览器,每一次请求,Cookie都会自动的发送给服务端,当然这是可以手动禁用的。
Cookie 主要的属性
属性 | 描述 |
---|---|
key=value | 键值对,必须都为字符串类型。值为 Unicode字符时,使用字符编码。值为二进制数据时,使用 Base64编码 |
domain | 指定 cookie 所属的域名,默认是当前域名 |
path | 指定 cookie 在哪个路径(即路由)下生效,默认为 ‘/’。若指定为/user,那么只有 /user 下的路由可以访问到该 cookie,如:/user/find |
maxAge | cookie 失效时间,单位:秒, maxAge < 0 则关闭浏览器就失效,maxAge = 0 ,删除,默认情况maxAge = -1 |
expires | icookie 过期时间,在设置的某个时间后 该 cookie失效。没有 maxAge 好用 |
secure | true / false,表示 cookie 是否使用安全协议(如 HTTPS,SSL)传输,默认为 false。设置为 ture 时, cookie 在HTTP传输中无效,而在 HTTPS 中会有效 |
httpOnly | 禁止通过 JS脚本获取 cookie,但可以通过浏览器调试工具获取到 Application 里的 cookie,能防一定的XSS攻击 |
1.2 Session
Session 是另一种记录服务端和客户端会话状态的机制,Session 是基于 Cookie 实现的,Session 存储在服务端,当发送请求后,服务端会存储当前请求的sessionId (区分不同的客户端)到 客户端的 Cookie 中。
1.3 Cookie 与 Session 比较
Cookie | Session | |
---|---|---|
安全性 | 不安全,存储在客户端,可自行修改 | 安全,存储在服务端 |
存储类型 | key为字符串value也为字符串 | key为字符串,value 可为能序列化的任何数据 |
有效期 | 长时间保持 | 时间较短,默认为30秒,且在客户端关闭就失效。 |
存储量 | 单个Cookie不能超过4K,即4096 Byte | 没有固定限制,存储在内存,但是过大会占资源 |
二、Token
Token 即令牌,分为 Access Token 和 Refresh Token 两种,本次我们用的是 JWT,它是基于Token的,这里以了解为主。
2.1 Access Token
Access Token 是访问资源接口(API)时所需的资源凭证
主要组成: uid + time + sign
- uid ,用户唯一的身份标识
- time,当前时间戳
- sign 签名
特点:
- 服务端无状态化,可扩展性强
- 支持移动端设备,现APP里基本上用的都是 Token认证
- 安全性高
- 支持跨应用,比如在CSDN中使用微信登录账号。
Access Token 的身份认证流程:
以使用 Token 进行登录的身份认证为例
使用 Access Token 认证的特点:
- 认证通过后,每次请求都需携带 Token 字符串,通常是放在 HTTP 请求的 Header 请求头
- 基于 Token 实现的用户认证属于 无状态的认证方式,服务端不用存放 Token 数据。
- 用解析 Token 的计算时间 换取 Session 的存储与处理时间,减轻服务端压力,减少数据库交互。
- Token 由客户端管理,成功避开同源策略,即可跨应用进行用户认证
2.2 Refresh Token
Refresh Token 的作用是刷新 Access Token 的 token,若没有 Refresh Token, 同样可以刷新 Access Token,但每次刷新都需要用户输入用户名和密码,带来更多麻烦。
Refresh Token + Access Token 的 认证流程:
使用 Refresh Token 结合 Access Token 的特点:
- Access Token 有效期比较短,当过期失效时, 使用 Refresh Token 就可以获取到新的 Key,若 RefshToken中也失效,那么用户只能重新登录。
- Refresh Token 包括其过期时间都存储在服务器的数据库中,只有在申请新的 Acess Token 时才会验证,不会对接口响应时间造成影响,无需像 Session那样保存在内存。
2.3 Token 与 Session 相比
Session | Token | |
---|---|---|
服务端状态 | 有状态化,记录会话信息 | 无状态化,不存储会话信息 |
安全性 | 安全性比Cookie高,但没有Token高,因为SessionId仍然是记录在客户端的 Cookie 里,可进行篡改 | 更安全,有带加密算法的签名机制 |
认证机制 | 支持认证,不可跨应用 | 支持认证,可以跨应用 |
三、JWT
JWT,是一种基于 Token 的认证授权机制,是 JSON Web Token 的缩写,是目前最流行的跨域认证采用的技术。
JWT是为了在网络应用环境之间传递声明而执行的一种基于JSON的公开标准(https://datatracker.ietf.org/doc/html/rfc7519)
关于 JWT 的 参考教程:http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
3.1 JWT 认证流程
JWT实现原理,以用户登录为例:
JWT 认证流程:
- 用户携带个人的用户名和密码发送请求,服务端认证成功后,返回客户端一个JWT字符串
- 客户端将 Token 保存到本地(浏览器的话通常是 localStorage 或 Cookie)
- 当用户要访问受保护的路由或资源时,需设置 Header 请求头的 Authorization 的值,使用 Bearer 模式 添加 JWT 字符串,比如
Authorization: Bearer <token>
- 服务端的保护路由会检查 Header 种 Authorization 中的 JWT 信息,若合法则运行用户行为。
使用 JWT 的特点:
- 内部包含一些会话信息,除了验证、签名生成 JWT外,其他情况无需数据库交互
- JWT 并不一定依赖于 Cookie,所以可以跨域访问,即支持向第三方应用进行认证与授权
- 由于用户状态并不存储在服务端的内存,故是一种无状态的认证机制
3.2 JWT 与 Token 相比
JWT 是 Tokne 的拓展,它们之间的 相同点 有:
- 都是访问资源的令牌
- 都可以记录用户信息
- 都使服务端无状态化
- 都必须通过验证,客户端才能访问服务端受保护的资源
不同点:
Token,服务端 必须 查询数据库获取用户信息,然后再验证 Token 是否有效,
JWT 是将 Token 和 Payload 加密存储在客户端,服务端只需使用密钥解密进行验证,无需查询数据库
3.3 JWT的主要组成
JWT 主要由三部分组成,分别是 header(请求头)、payload(有效负载)、signature(签名),JWT由这三个字符串以句点 . 的方式连接,JWT 通常显示为:xxx.yyy.zzz
,即 Header.Payload.Signature
3.3.1 Header
Header 标头通常由两部分组成:
- 令牌的类型(即 JWT )
- 签名算法
如 HMAC SHA256 或 RSA,它会用 Base64 编码组成 JWT 结构的第一部分
注:Base64 是一种编码,即是可以进行解码的,并不是一种加密过程。
{
"alg" : "HS256",
"typ" : "JWT
}
3.3.2 Payload
令牌的第二部分是 Payload (有效负载),主要存储一些键值对
和 Header 部分一样,Payload 会采用 Base64 编码,不过是作为 JWT 结构的第二部分
例如:
{
"sub": "1234567890",
"name": "Uni",
"admin": true
}
3.3.3 Signature
前面两部分都是用 Base64 进行编码的,即前端后端都可以解析里面的信息,Signature 签名需要使用编码后的header 和 payload 以及我们提供的一个字符串密钥,然后使用 header 中指定的签名算法,例如 HS256 进行签名。签名的作用是保证 JWT 没有被篡改过,如:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret);
3.3.4 签名目的
像上述那样,签名的时候同时使用了 header 和 payload 字符串的内容进行组合,
这最后一步的签名过程,实际上是对 Header 头部以及 Payload 负载内容进行签名,防止内容被篡改。若有人对Header 以及 Payload 的内容解码之后再进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务端就会判断出新的头部和负载形成的签名和 JWT 附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
签名的策略有许多,这里只是简单举例,比如我们可以用 payload 里存储的用户ID 和 服务器固定的密钥来进行组合加密,都是可以的。
3.3.5 信息安全问题
Base64 是一种编码方式,是可逆的,那么信息就容易暴露。在JWT中,不应该在 payload 负载里加入任何 敏感 的数据,例如用户的个人密码,不适合放到 JWT。
JWT 经常用于设计用户认证和授权系统,或者实现 Web 应用的单点登录。
四、Cookie Session Token JWT 使用的常见问题
4.1 Cookie
使用 Cookie 需注意的问题:
- 存储在客户端,容易被篡改,使用前需验证是否合法
- 尽量不存敏感数据,如用户密码,账号余额
- 使用 httpOnly ,提高安全性
- 存储的数据量不能过大,单个Cookie最多只能存储 4kb 的数据
- 设置正确的 domain 域 和 path,尽量减少数据传输
- 无法跨域
- 浏览器对单个网站最多存 20个 Cookie,而浏览器一般指允许存放 300个Cookie
- 移动端不适合用 Cookie ,而 Session 又是基于 Cookie 实现,故移动端常用 Token
4.2 Session
使用 Session 需注意的问题:
- 当用户在线过多时,会占据更多内存,需在服务端定期处理过期的 Session
- 集群环境下,服务端需解决多台 web 服务器共享session的问题,否则同一个用户无法访问服务端不同的web服务器。
- 共享session 会遇到跨域问题,服务端需在各个应用间解决 cookie 跨域 问题
- sessionId 存储在客户端的 cookie中,如果浏览器禁止 cookie 或不支持 cookie,可以把 sessionId 写在 URL 参数后面,即 session不一定必须基于 cookie 实现
4.3 Token
使用 Token 需注意的问题:
- 数据库存储 Token,若导致查询时间长,可选择存放在内存,比如使用 Redis 数据库
- Token 由应用管理,避免了同源策略的限制。
- Token 不依赖 Cookie, 可避免 CSRF(跨站域请求伪造)攻击
4.4 JWT
使用 JWT 需注意的问题:
- JWT 不依赖 Cookie,,支持 CORS 跨域资源共享
- JWT 默认不加密,不过可设置加密,生成原始Toekn后,再用指定的加密算法加密一次
- JWT 不加密时不能存放敏感数据,否则有安全隐患
- JWT 除了认证以外,还可以用来交换信息,降低了服务端的数据库交互
- JWT 最大优势是服务端无需依赖 Session,方便服务端的认证与授权业务的拓展,同时也有一定局限,无法在使用过程中废弃某个 Token 或 更改 Token 的权限,除非使用更多的逻辑进行处理
- JWT 本身包含了认证信息,故最好将JWT有效期设置短一些,对于比较重要的权限,使用时应再次验证用户身份
- JWT 适合一次性的命令认证,颁发有效期很短的JWT,降低泄露的危险
- 为了降低盗用概率,JWT 应使用 HTTPS 安全协议传输
4.5 整合JWT
JWT 本质上就是一个由点分隔的 Base64-URL 字符串,可以在 HTML 和 HTTP 环境中轻松传递这些字符串,与基于XML的标准(例如SAML)相比,它更紧凑。可通过URL,POST参数或者在HTTP Header 发送,因为数据量小,传输速度快。
五、SpringBoot 整合 JWT
5.1 前后端不分离版
5.1.1 简单版
后端不使用拦截器,前端只通过 HTML ,不写 JavaScript,接下来以最这种最简单的方式来实现 JWT 认证,前后端不分离,前端用 Thymeleaf 模板渲染引擎。
注:这种方式只是用来测试 JWT 功能的,实际开发中很少用
1. 引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.bbs.uni</groupId>
<artifactId>uni</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>uni</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</