当前位置:首页 » 《我的小黑屋》 » 正文

Web身份验证详解:Cookie、Session、Base64与Token的应用与区别

17 人参与  2024年09月13日 14:40  分类 : 《我的小黑屋》  评论

点击全文阅读


在这里插入图片描述

?个人主页:时间会证明一切.

目录

会话技术Cookie服务器向客户端发送 Cookie客户端携带 Cookie 向服务器发送请求Cookie 使用细节使用 Cookie 时存在的问题跨域问题 SessionSession 使用流程Session 使用细节Cookie 和 Session 区别 Base64算法Base64是什么?为什么叫Base64?Base64解决什么问题?使用场景Base64算法Base64字符串末尾的`=`是什么Base64 DataURI格式 Token一、跨域认证的问题二、JWT 的原理三、JWT 的数据结构3.1 Header3.2 Payload3.3 Signature3.4 Base64URL 四、JWT 的使用方式五、JWT 的几个特点 JWT 在 java 中的使用创建 JWT 和 解析 JWT 工具类登录向前端发送 JWT token前端接收 token客户端再次发送请求携带 token 参考

会话技术

会话:一次会话中包含多次请求和响应。

一次会话:浏览器第一次给服务器资源发送请求,会话建立,直到有一方断开为止功能:在一次会话的范围内的多次请求间,共享数据方式: 客户端会话技术:Cookie服务器端会话技术:Session

Cookie

Cookie:客户端会话技术,将数据保存到客户端,以后每次请求都携带 Cookie 数据进行访问,基于HTTP协议实现

当用户第一次访问并登陆一个网站的时候,Cookie的设置以及发送会经历以下4个阶段:

客户端发送一个请求到服务器服务器返回一个 httpResponse 响应到客户端,其中包含 Set-Cookie 的响应头客户端收到服务器返回的 Set-Cookie 响应头里的数据,将其保存 Cookie,之后向服务器发送请求时,httpRequest 请求中会包含一个 Cookie 的请求头服务器可以通过检查该 Cookie 获取信息,返回响应数据

img

服务器向客户端发送 Cookie

Cookie 基本使用

创建 Cookie 对象,设置数据

Cookie cookie = new Cookie("key","value");

发送 Cookie 到客户端 使用 response 对象

response.addCookie(cookie)

Set-Cookie是通过HTTP响应头部的Set-Cookie字段来设置的。当服务器要向客户端设置一个新的Cookie时,会在HTTP响应中包含这个Set-Cookie字段。

例如,下面是一个包含Set-Cookie头部的HTTP响应示例:

HTTP/1.1 200 OKContent-Type: text/htmlSet-Cookie: key=value

在这个示例中,服务器通过Set-Cookie头部将一个名为key的Cookie发送给客户端。该Cookie的值为value

当客户端收到这个Set-Cookie头部时,会解析其中的信息,并存储这个Cookie。在后续的请求中,客户端会将这个Cookie自动包含在请求头部的Cookie字段中发送给服务器。服务器接收到请求后,可以根据这个Cookie的值进行相应的处理。

客户端携带 Cookie 向服务器发送请求

在HTTP请求中,Cookie是通过包含在请求头部的"Cookie"字段中发送的。具体来说,Cookie会以键值对的形式表示,多个键值对之间使用分号(;)进行分隔。例如:

GET /example HTTP/1.1Host: example.comCookie: key1=value1; key2=value2; key3=value3

在这个示例中,请求头部的"Cookie"字段携带了三个Cookie,即key1=value1、key2=value2和key3=value3。

服务器收到请求后,可以解析请求头部中的"Cookie"字段,以获取客户端发送的Cookie数据,并根据需要进行处理或响应

Cookie 使用细节

Cookie存活时间

默认情况下,Cookie 存储在浏览器内存中,当浏览器关闭,内存释放,则 Cookie 被销毁setMaxAge(int seconds):设置 Cookie 存活时间 正数:将 Cookie 写入浏览器所在电脑的硬盘,持久化存储。到时间自动删除负数:默认值,Cookie 在当前浏览器内存中,当浏览器关闭,则 Cookie 被销毁零: 删除对应 Cookie

Cookie 存储中文

Cookie不能直接存储中文 在tomcat 8 之前 cookie中不能直接存储中文数据在tomcat 8 之后,cookie支持中文数据。特殊字符还是不支持,建议使用URL编码存储,URL解码解析如需要存储,则需要进行转码:URL编码

使用 Cookie 时存在的问题

不安全,不要存储敏感数据,比如用户密码,账户余额,因为存储在客户端,容易被客户端篡改能存储的数据量不能超过 4KB有数量限制,一个浏览器针对一个网站最多存 20 个 Cookie,浏览器一般只允许存放 300 个 Cookie;移动端对 Cookie 的支持不是很好,而 Session 需要基于 Cookie 实现,所以移动端常用的是 token;Cookie 为不可跨域的:每个 Cookie 都会绑定单一的域名,无法在别的域名下获取使用;

跨域问题

跨域的由来不得不提到 XSS 攻击。

全名:Cross-site scripting(跨站脚本攻击)。这是一种安全漏洞,攻击者可以利用这种漏洞在网站上注入恶意的客户端代码。若受害者运行这些恶意代码,攻击者就可以突破网站的访问限制并冒充受害者。简单地说,就是我可以在你的网站偷偷上运行我的代码,那有多危险。

为了应对这种情况,便有了浏览器的同源策略。

同源策源

这个策略是由浏览器去实现的,其目的在于限制请求方如何与另一个源的资源进行交互。

“源”你就理解为地址,但它的完整格式由协议、域名、端口组成,如:

https://store.company.com:8000

另一个客户端请求上面的地址,如果协议、域名、端口其中有一个不同,这两个就算两个源,客户端的行为要受到限制。

下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:

URL结果原因
http://store.company.com/dir2/other.html同源只有路径不同
http://store.company.com/dir/inner/another.html同源只有路径不同
https://store.company.com/secure.html失败协议不同
http://store.company.com:81/dir/etc.html失败端口不同 ( http:// 默认端口是 80)
http://news.company.com/dir/other.html失败主机不同

不同源的请求,就被认为是跨域,是不被允许的。

Cookie跨域问题

Cookie是不可以跨域名的,隐私安全机制禁止网站非法获取其他网站的Cookie。
正常情况下,同一个以及域名下的两个二级域名也不能交互Cookie,比如test1.jianshu.com与test2.jianshu.com,因为二者简书的域名不完全相同,如果想要jianshu.com名下的二级域名都可以使用该Cookie,需要设置Cookie的domain参数为jianshu.com,这样使用test1.jianshu.com和test2.jianshu.com就能访问同一个域名了。

设置的过程,服务器设置cookie的时候,需要指定cookie的domain,当domain与当前host的匹配不上的时候,responseHeader里的set-cookie不会设置成功。这也就是cookie不支持跨域问题。

Session

服务器端会话技术,在一次会话的多次请求间共享数据,将数据保存在服务器端的对象中

session 是另一种记录服务器和客户端会话状态的机制

session 是基于 cookie 实现的

session 存储在服务器端,可以理解为一个状态列表,他拥有一个唯一识别符号JSESSIONID,通常存放于 cookie 中

在一次会话的多次请求间获取的session是同一个

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MXOSt1SF-1688566791979)(Cookie、Session 和 Token.assets/image-20230703141620004.png)]

Session 使用流程

首先,客户端会发送一个http请求到服务器端。服务端接收请求后,创建对应的 session ,而每一个session是有一个唯一标识id的,tomcat自动将该session的id当作一个Cookie并发送一个http响应到客户端,这个响应头,其中就包含 Set-Cookie 头部。该头部包含了key = JSESSIONID,值=session唯一标识。比如:set-cookie: JSESSIONID=10(每个session的唯一标识)浏览器接收到服务端返回的 JSESSIONID=10后,将此信息存入 Cookie 中,同时 Cookie 记录此 JSESSIONID 属于哪个域名。当用户第二次发起请求时,请求会自动判断此域名下是否存在 Cookie 信息,如果存在浏览器会自动在请求头中添加 Cookie 发送到服务端服务端接收请求,会从 Cookie 中获取 JSESSIONID,再根据 JSESSIONID查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

Session 使用细节

Session 钝化、活化

服务器重启后,Session 中的数据是否还在? 钝化:在服务器正常关闭后,Tomcat 会自动将 Session 数据写入硬盘的文件中活化:再次启动服务器后,从文件中加载数据到 Session 中

Session销毁:

默认情况下,无操作,30分钟自动销毁

<session-config><session-timeout>30</session-timeout></session-config>

调用 Session 对象的invalidate()方法,Session 自动销毁

Cookie 和 Session 区别

Cookie 和 Session都是来完成一次会话内多次请求间数据共享的区别: 存储位置:Cookie是将数据存储在客户端,Session将数据存储在服务端安全性:Cookie不安全,Session安全数据大小:Cookie最大4KB,Session无大小限制存储时间:Cookie可以长期存储,Session默认30分钟服务器性能:Cookie不占服务器资源,Session占用服务器资源

Base64算法

Base64是什么?

Base64 是一种二进制到文本的编码方式。如果要更具体一点的话,可以认为它是一种byte数组编码为字符串的方法,而且编码出的字符串只包含 ASCII 基础字符。

例如字符串ShuSheng007对应的 Base64 为U2h1U2hlbmcwMDc=。其中那个=比较特殊,是填充符,一会再说。

值得注意的是 Base64 不是加密算法,其仅仅是一种编码方式,算法也是公开的,所以不能依赖它进行加密。

为什么叫Base64?

因为它是基于(Base)64个字符的一种编码方式。使用其编码后的文本只包含64个ASCII码字符(偶尔加一个填充字符=),如下所示:

Base64使用到的64个 字符:

A-Z 26个a-z 26个0-9 10个+ 1个/ 1个

下图是Base64码表,可以看到从0到63的每个数字都对应一个上面的一个字符。

img

Base64解决什么问题?

为什么各系统以及传输协议中二进制不兼容

导致各系统和传输协议中二进制不兼容的原因可能有以下几点:

数据表示方式:不同系统或协议可能使用不同的数据表示方式,例如大端字节序和小端字节序的区别。如果在数据交换过程中没有正确处理字节序,就会导致数据解析错误。

数据类型和长度:不同系统或协议可能对数据类型和长度的定义存在差异。例如,一个系统使用32位整数表示某个字段,而另一个系统使用64位整数表示相同的字段。在数据交换时,如果没有对数据类型和长度进行适当的转换和匹配,就会导致数据解析错误或数据截断。

数据编码方式:不同系统或协议可能使用不同的数据编码方式,如ASCII、UTF-8、UTF-16等。如果在数据交换时没有正确指定和处理编码方式,就会导致数据解析错误或乱码。

数据格式和协议规范:不同系统或协议可能使用不同的数据格式和协议规范。例如,一个系统使用JSON格式进行数据交换,而另一个系统使用XML格式。如果在数据交换时没有按照正确的格式和规范进行解析和处理,就会导致数据解析错误或通信失败。

加密和压缩算法:不同系统或协议可能使用不同的加密和压缩算法。如果在数据交换时没有正确处理加密和压缩算法,就无法正确解析和还原数据。

为了解决这些二进制不兼容性问题,通常需要进行数据格式转换、字节序转换、数据类型转换、编码转换等操作,以确保数据在不同系统和协议之间正确解析和传输。

Base64 就是为了解决各系统以及传输协议中二进制不兼容的问题而生的

为啥 Base64 大家就兼容了呢?因为 Base64 满足了各方的需求,各方说了,我们只保证支持 ASIIC 中那些基础字符,其他的我们不能保证,于是 Base64 就去从那些基础字符里挑了64个,所以大家都高兴了。

Base64编码是一种将二进制数据转换为可打印字符的编码方式。它通过将原始二进制数据转换为由64个字符组成的可打印字符集,从而解决了各系统和传输协议中二进制不兼容的问题。以下是Base64可以解决二进制不兼容性的原因:

可打印字符集:Base64编码使用64个可打印字符(A-Z,a-z,0-9和"+“,”/")表示二进制数据,这些字符在大多数系统和协议中都是兼容的。通过将二进制数据转换为可打印字符集,可以确保数据能够在不同系统和协议之间正确解析和传输。

字符集一致性:Base64编码使用的字符集在大多数系统中是标准的,无论是ASCII字符集还是Unicode字符集。这种一致性确保了数据在不同系统和协议中的兼容性。

无格式限制:Base64编码只关注数据的二进制表示,而不关注数据的具体格式。这意味着可以对任意类型的二进制数据进行Base64编码,无论是图像、音频、文本还是其他类型的数据。因此,通过Base64编码,可以将任何类型的二进制数据转换为字符串形式,以便在各种系统和协议之间传输

需要注意的是,Base64编码会导致数据稍微增加约33%的大小,因为每3个字节的二进制数据会编码成4个字符。因此,在使用Base64编码传输数据时,需要考虑到数据大小的增加。另外,Base64编码只是解决二进制数据在传输和解析过程中的兼容性问题,而不会解决二进制数据在语义和语法上的兼容性。对于更复杂的数据兼容性问题,可能需要进行更多的数据转换和处理操作。

使用场景

证书电子邮件的附件,因为附件往往有不可见字符xml 中如果像嵌入另外一个 xml 文件,直接嵌入,往往 xml 标签就乱套了, 不容易解析,因此,需要把 xml 编译成字节数组的字符串,编译成可见字符。网页中的一些小图片,可以直接以 Base64 编码的方式嵌入,不用再链接请求消耗网络资源。较老的纯文本协议 SMTP ,这些文本偶尔传输一个文件时,需要用 Base64

Base64算法

Base64算法是一种将二进制数据转换为可打印字符的编码算法。下面是Base64编码的基本步骤:

我们将使用二进制数据 “Hello World!” 进行编码。

将 “Hello World!” 转换为对应的ASCII码: ASCII码:72 101 108 108 111 32 87 111 114 108 100 33 将ASCII码转换为二进制: 01001000 01100101 01101100 01101100 01101111 00100000 01010111 01101111 01110010 01101100 01100100 00100001 将二进制数据按照每3个字节分组: 01001000 01100101 01101100 01101100 01101111 00100000 01010111 01101111 01110010 01101100 01100100 00100001 将每个字节转换为8位二进制数,并拼接在一起: 010010000110010101101100 011011000110111100100000 010101110110111101110010 011011000110010000100001 将24位的二进制数划分为4个6位的小组: 010010 000110 010101 101100 011011 000110 111100 100000 010101 110110 111101 110010 011011 000110 010000 100001 将每个6位的二进制数转换为十进制数,对应于Base64字符集中的索引: 18 6 21 44 27 6 60 32 21 54 61 50 27 6 16 33 使用Base64字符集中对应索引的字符替代每个6位二进制数: S G V k b G 8 g V u Z + y G B Q 得到最终的Base64编码字符串: “SGVsbG8gV29ybGQh”

所以,将二进制数据 “Hello World!” 使用Base64编码后得到的Base64编码字符串是 “SGVsbG8gV29ybGQh”。

解码时,可以按照相反的步骤进行操作:

将Base64编码字符串按照每4个字符进行分组。

将每个Base64字符转换为对应的6位二进制数。

将这4个6位的二进制数拼接在一起,得到一个24位的二进制数。

将这个24位的二进制数划分为3个8位的字节。

将每个8位二进制数转换为十进制数,得到原始的二进制数据。

如果Base64编码字符串中存在填充字符’=',则去除填充字符。

需要注意的是,Base64编码是一种用于数据传输和存储的编码方式,并不是加密算法。它可以将二进制数据转换为可打印字符,但不能提供数据的保密性。

下图是维基百科上面的一个例子

假如我们的原文为Man,那么下图演示了如何按照上面的步骤将其编码为Base64字符串

img

可以发现Man对应的Base64为TWFu.现在大家应该明白为什么只有64个字符了吧?因为算法将将8bit分割成6bit了,而6bit的取值范围为0~63

Base64字符串末尾的=是什么

有时我们会在Base64字符末尾会看到=,有时1个,有时2个,这是为啥?

通过上面的我们知道了Base64编码过程是3个字符一组的进行,如果原文长度不是3的倍数怎么办呢? 例如我们的原文为Ma,它不够3个,那么只能在编码后的字符串中补=了。缺一个字符补一个,缺两个补两个即可,所以有时候你会看见base64字符串结尾有1个或者2个=

img

Base64 DataURI格式

有时你会发现web页面传给你的base64字符串前面有类似下面的东东。

data:image/jpeg;base64,    /9j/4AA...

这是DataURI,大部分浏览器支持直接打开这类二进制数据,但是我们要格外注意,如果你只是想要真实的Base64内容就需要取,后边的内容

Token

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,本文介绍它的原理和用法。

img

一、跨域认证的问题

互联网服务离不开用户认证。一般流程是下面这样。

用户向服务器发送用户名和密码。服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。服务器向用户返回一个 session_id,写入用户的 Cookie。用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

存在的问题

扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。

举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?

解决方案

一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。

另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

二、JWT 的原理

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

{  "姓名": "张三",  "角色": "管理员",  "到期时间": "2023年7月1日0点0分"}

以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。

服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

三、JWT 的数据结构

实际的 JWT 大概就像下面这样。

img

它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。

JWT 的三个部分依次如下。

Header(头部)Payload(负载)Signature(签名)

写成一行,就是下面的样子。

Header.Payload.Signature

img

下面依次介绍这三个部分。

3.1 Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

{  "alg": "HS256",  "typ": "JWT"}

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT

最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。

3.2 Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

iss (issuer):签发人exp (expiration time):过期时间sub (subject):主题aud (audience):受众nbf (Not Before):生效时间iat (Issued At):签发时间jti (JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

{  "sub": "1234567890",  "name": "John Doe",  "admin": true}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

这个 JSON 对象也要使用 Base64URL 算法转成字符串。

请注意,对于签名令牌,此信息虽然受到篡改保护,但任何人都可以读取。不要将机密信息放在 JWT 的有效负载或标头元素中,除非它已加密。

3.3 Signature

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(  base64UrlEncode(header) + "." +  base64UrlEncode(payload),  secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

3.4 Base64URL

前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_ 。这就是 Base64URL 算法。

JWT.io Debugger

四、JWT 的使用方式

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

Authorization: Bearer <token>

另一种做法是,跨域的时候,JWT 就放在 POST 请求体里面。

五、JWT 的几个特点

JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。JWT 不加密的情况下,不能将秘密数据写入 JWT。JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

JWT 在 java 中的使用

创建 JWT 和 解析 JWT 工具类

@Componentpublic class JwtUtil {    // 有效期    private static final long JWT_EXPIRE = 3 * 60 * 60 * 1000L;  //三小时 每秒1000毫秒    // 令牌秘钥    private static final String JWT_KEY = "123456";    public String createToken(Object data) {        // 当前时间        long currentTime = System.currentTimeMillis();        // 过期时间        long expTime = currentTime + JWT_EXPIRE;        // 构建jwt        JwtBuilder builder = Jwts.builder()                .setId(UUID.randomUUID() + "")  // 设置JWT的唯一标识符                .setSubject(JSON.toJSONString(data))  // 设置JWT的主题,将data对象转换为JSON字符串                .setIssuer("system")  // 设置JWT的发行者                .setIssuedAt(new Date(currentTime))  // 设置JWT的发行时间为当前时间                .signWith(SignatureAlgorithm.HS256, encodeSecret())  // 使用HS256算法和密钥对JWT进行签名                .setExpiration(new Date(expTime)) // 设置JWT的过期时间                .setHeaderParam("typ","JWT");  // 设置 JWT 的 Header        return builder.compact();  // 将JWT压缩为一个字符串并返回    }    /**     * encodeSecret方法:用于对密钥进行编码,返回一个SecretKey对象。     * @return SecretKey     */    private SecretKey encodeSecret() {        byte[] encode = Base64.getEncoder().encode(JWT_KEY.getBytes());        // 0 它表示密钥的起始位置 由于使用的是 AES 对称加密算法,所以密钥的算法参数被指定为 “AES”。        return new SecretKeySpec(encode, 0, encode.length, "AES");    }    public Claims parseToken(String token) {        return Jwts.parser()                .setSigningKey(encodeSecret())                .parseClaimsJws(token)                .getBody();    }    /**     * 用于解析JWT。它接受一个JWT字符串作为参数     * 并使用encodeSecret方法生成的密钥对JWT进行解析     * 解析后的内容可以通过getBody方法获取,也可以通过parseObject方法将主题转换为指定类型的对象。     */    public <T> T parseToken(String token, Class<T> clazz) {        Claims body = Jwts.parser()                .setSigningKey(encodeSecret())                .parseClaimsJws(token)                .getBody();        return JSON.parseObject(body.getSubject(), clazz);    }}

测试 JWT 工具类

@SpringBootTestpublic class JwtUtilTest {    // 注入工具类    @Resource    private JwtUtil jwtUtil;    // 创建 JWT    @Test    public void testCreateJwt(){        User user = new User();        user.setUsername("zhangsan");        user.setPhone("14523235858");        String token = jwtUtil.createToken(user);        System.out.println(token);    }    // 解析 JWT    @Test    public void testParseJwt(){        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJlYzAwYmViMi0yMjM3LTQ3YzItOTNiMi1hYzMxNjBkYjRhODEiLCJzdWIiOiJ7XCJwaG9uZVwiOlwiMTQ1MjMyMzU4NThcIixcInVzZXJuYW1lXCI6XCJ6aGFuZ3NhblwifSIsImlzcyI6ImFhcm9uIiwiaWF0IjoxNjg4NTIyMTczLCJleHAiOjE2ODg1MzI5NzN9.zjwCTUbaiVD2g8J2SPJPMQwsfjAW9NlnlwVwEDYUMTA";       /**         * 使用 Base64 编码将字符串 “123456” 转换为字节数组时,会将每个字符编码为对应的 Base64 字符。         * 具体转换过程如下:         * 将字符串 “123456” 转换为对应的字节数组 [49, 50, 51, 52, 53, 54]。         * 对字节数组进行 Base64 编码,生成对应的 Base64 字符串。         * 经过 Base64 编码后的结果是 “MTIzNDU2”。         */        byte[] encode = Base64.getEncoder().encode("123456".getBytes());        // 调用 Jwts.parser 对JWT解析        JwsHeader header = Jwts.parser()                .setSigningKey(new SecretKeySpec(encode,0,encode.length,"AES"))                .parseClaimsJws(token)                .getHeader();        Claims body = Jwts.parser()                .setSigningKey(new SecretKeySpec(encode, 0, encode.length, "AES"))                .parseClaimsJws(token)                .getBody();        String signature = Jwts.parser()                .setSigningKey(new SecretKeySpec(encode, 0, encode.length, "AES"))                .parseClaimsJws(token)                .getSignature();        // 使用 base64 解码        String decode = new String(Base64.getDecoder().decode(encode));        System.out.println(decode);        System.out.println(header);        System.out.println(body);        System.out.println(signature);    }}

打印输出

// Base 解码后得到123456// Header{typ=JWT, alg=HS256}// Payload jti是JWT唯一标识 sub用户信息{jti=ec00beb2-2237-47c2-93b2-ac3160db4a81, sub={"phone":"14523235858","username":"zhangsan"}, iss=system, iat=1688522173, exp=1688532973}// SignaturezjwCTUbaiVD2g8J2SPJPMQwsfjAW9NlnlwVwEDYUMTA

登录向前端发送 JWT token

service

public Map<String, Object> login(LoginRequest loginRequest) {    //省略校验登录表单并查询数据库....        // 创建jwt    String token = jwtUtil.createToken(loginUser);    Map<String, Object> data = new HashMap<>();    // 将 token 放入 map 中    data.put("token", token);    return data;}    

controller

@PostMapping("/login")public Result<Map<String, Object>> login(@RequestBody LoginRequest loginRequest) {    Map<String, Object> data = userService.login(loginRequest,request);    if (data != null) {        // 返回 token 给前端        return Result.success(data);    }}

Result

@Datapublic class Result <T>{    private Integer code;    private String message;    private String description;    private T data;    public static <T> Result<T> success(T data) {        return new Result<>(20000,"success",data);    }}

前端接收 token

Login 组件 handleLogin 方法

handleLogin() {    this.$refs.loginForm.validate((valid) => {        if (valid) {            this.loading = true;            this.$store            // 使用user/login作为action的类型可以帮助区分在Vuex的store中有多个文件和模块的情况。                .dispatch("user/login", this.loginForm)                .then(() => {                this.$router.push({ path: this.redirect || "/" });                this.loading = false;            })                .catch(() => {                this.loading = false;            });        } else {            console.log("error submit!!");            return false;        }    });},

auth.js

import Cookies from 'js-cookie'// 存入cookies中的token的keyconst TokenKey = 'login_token'// 获取存入cookies中的tokenexport function getToken() {  return Cookies.get(TokenKey)}// 将token存入cookies中export function setToken(token) {  return Cookies.set(TokenKey, token)}export function removeToken() {  return Cookies.remove(TokenKey)}

api 目录下的 user.js

// 导入request 为这三个请求增加request拦截器import request from '@/utils/request'export function login(data) {  return request({    url: '/user/login',    method: 'post',    data  })}export function getInfo(token) {  return request({    url: '/user/info',    method: 'get',    params: { token }  })}export function logout() {  return request({    url: '/user/logout',    method: 'post'  })}

store目录中的 user.js

// 导入方法import { getToken, setToken, removeToken } from '@/utils/auth'import { login, logout, getInfo } from '@/api/user'import { resetRouter } from '@/router'const actions = {  // user login  // 这里的login实现组件方法的dispatch("user/login", this.loginForm)  login({ commit }, userInfo) {    const { username, password } = userInfo    return new Promise((resolve, reject) => {      // 这里的login是调用api中的login      login({ username: username.trim(), password: password }).then(response => {        // 接收到后端响应过来的 token        const { data } = response        // 调用SET_TOKEN 用户登录成功后的 token 保存到 state 中用户组件共享        commit('SET_TOKEN', data.token)        // 调用 utils 下的 auth.js 中的方法,将token保存在浏览器的 Cookies 中        setToken(data.token)        // 通过调用resolve方法,将Promise对象标记为已完成状态。        resolve()      }).catch(error => {    // 通过调用reject方法,将Promise对象标记为已拒绝状态,并将错误信息作为参数传递给它。        reject(error)      })    })  },}const mutations = {  // 将用户登录成功后的 token 保存到 state 中  SET_TOKEN: (state, token) => {    state.token = token  }}const getDefaultState = () => {  return {    token: getToken(),    name: '',    avatar: '',    menuList: []  }}const state = getDefaultState()export default {  // 开启命名空间,这样才能找到各个模块 使用 user/login  namespaced: true,  state,  mutations,  actions}

客户端再次发送请求携带 token

前端拦截器

前端拦截器的作用

是在发送请求之前对请求进行处理,例如为请求携带 token、设置请求头等。前端拦截器不会拦截请求本身,而是在请求发送之前对其进行修改或者附加额外的处理。这样可以方便地为所有需要授权或者其他共享的请求添加相同的逻辑,使得代码更加简洁和可维护。

对请求真正的拦截

而请求的真正拦截或阻止是由后端来完成的。后端可以根据实际需求对请求进行检查、验证、授权等,如果后端判断请求不符合条件或不被允许,则会拦截该请求,返回相应的错误响应或者执行其他操作。

因此,前端拦截器的作用是在前端对请求进行预处理,而请求的拦截或者放行是由后端来决定的。

utils 下 request.js 的请求拦截器

import axios from 'axios'import store from '@/store'import { MessageBox, Message } from 'element-ui'import { getToken } from '@/utils/auth'// 创建一个 axios 实例const service = axios.create({  // .env.development 文件下的  // VUE_APP_BASE_API = 'http://localhost:80' 后端的地址  baseURL: process.env.VUE_APP_BASE_API, // url = 基础 URL + 请求 URL  // withCredentials: true, // 跨域请求时发送 cookies  timeout: 500000 // 请求超时时间})// 请求拦截器service.interceptors.request.use(  config => {    // 在发送请求之前做一些处理    if (store.getters.token) {      // 让每个请求携带自定义的 token      // ['X-Token'] 是自定义的请求头键名      // 请根据实际情况进行修改 getToken 从cookies中取调用Cookies.get(TokenKey)      config.headers['X-Token'] = getToken()    }    return config  },  error => {    // 对请求错误做一些处理    console.log(error) // 用于调试    return Promise.reject(error)  })

响应拦截器

// 响应拦截器service.interceptors.response.use(  /**   * 如果你想获取响应的 HTTP 信息,比如 headers 或者 status   * 请返回 response => response      * 根据自定义的状态码判断请求状态   * 这里只是一个示例   * 你也可以根据 HTTP 状态码来判断状态   */  response => {    const res = response.data    // 如果自定义的返回码不是 20000,则判断为错误    if (res.code !== 20000) {      Message({        message: res.message || 'Error',        type: 'error',        duration: 5 * 1000      })      // 50008: 非法的 token;50012: 其他客户端登录;50014: Token 已过期;      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {        // 重新登录        MessageBox.confirm('您已注销,您可以取消以停留在此页面,或者重新登录', '确认注销', {          confirmButtonText: '重新登录',          cancelButtonText: '取消',          type: 'warning'        }).then(() => {          store.dispatch('user/resetToken').then(() => {            location.reload()          })        })      }      return Promise.reject(new Error(res.message || 'Error'))    } else {      return res    }  },  error => {    console.log('err' + error) // 调试用    Message({      message: error.message,      type: 'error',      duration: 5 * 1000    })    return Promise.reject(error)  })

参考

五分钟搞懂 Session、Cookie和Token - 掘金 (juejin.cn)cookie机制&跨域问题 - 简书 (jianshu.com)(76条消息) 关于JWT_jwt存储用户信息_F班的小夏同学的博客-CSDN博客(76条消息) Cookie、Session和Token_移动端cookie_oywLearning的博客-CSDN博客06-小结_哔哩哔哩_bilibiliJSON Web Token 入门教程 - 阮一峰的网络日志 (ruanyifeng.com)让你彻底理解Base64算法(Base64是什么,Base64解决什么问题,Base64字符串末尾的=是什么) - 知乎 (zhihu.com)

点击全文阅读


本文链接:http://zhangshiyu.com/post/159343.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1