2021SC@SDUSC
由于HTTP协定是不储存状态的,当我们刚刚透过帐号密码验证一个使用者时,下一个request请求就把刚刚的资料忘了。所以我们的程序就不知道谁是谁,就要再验证一次。所以为了保证系统安全易用,我们就需要验证用户否处于登录状态。
在大多数前后端分离的项目中,JWT是常见的一个认证标准,特别适合用于分布式站点的单点登录场景。JWT全称是JSON web token。是一种为了在网络应用中传递数据而创造的一种开放标准。在JWT之前,大多数的web应用是采用session认证的,前后端分离开发成为主流之后,JWT也就登上了舞台。
在介绍JWT之前,我们先来讲一下传统的认证方式。
众所周知,HTTP请求是一种无状态的协议(stateless)。这意味着,当用户通过验证账号密码的方式进行用户认证后,用户进行下一次业务请求时还需要再次验证身份。因为HTTP协议不记录状态,所以服务器无法知道是哪个用户发出的请求。为了使服务器能够识别出是哪个用户发送的请求,我们只能在服务器端储存一份用户的登录信息。当用户登录成功时,我们将这份登录信息随着响应发送给浏览器,并告知其应当将该信息作为cookie保存。当用户再次发送请求时,其登录信息将作为cookie被请求携带着发送给服务器,从而使服务器能够区分出不同的用户。
但是,在互联网蓬勃发展的当下,各大web应用的使用人数也在指数型增长,若要储存每个用户的登录信息,对服务器来说无疑是个重大的负担。而且由于登录信息是储存于服务器端,这种传统的session认证方式也很难用于分布式系统中。同时,session还有着很大的安全隐患,由于该认证方式基于cookie识别用户,一旦cookie被截获,很容易遭受CSRF攻击。
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
当我们看到一段看似毫无意义的字符串,由三段文本构成,每段文本间以.
来分割开来,且以eyJhbGciOiJ
作为开头的话,那么这段字符串很有可能就是一段JWT字符串。例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
一个JWT由三段字符串构成,第一段我们叫它头部(header),第二部分被称为载荷(payload),最后一部分则叫做签证(signature)。
JWT的头部包含两部分信息:
{
"alg": "HS256",
"typ": "JWT"
}
这个部分是用来存储有效信息的地方,包含有三个部分:
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息。
最后这部分由三部分组成:
这部分是通过加密后的header和加密后的payload使用.
连接成的字符串,再通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
注意:secret是保存在服务器端的,jwt的生成签发也是在服务器端完成的,其应当作为服务器的私钥,任何时候都不应该泄露出去。一旦第三方获得了这个密钥,就意味着第三方可以自行签发jwt。
通过分析SDUOJ源码可知,当用户执行登录操作后,后端会将签发的JWT字符串置于响应体的data
字段。由于token需要长期保存,因此应当使用localStorage
进行储存。
import axios from 'axios';
axios.post(
"/user/login",
{
username: this.userId,
password: this.userPsw,
},
).then((res)=>{
if (res.status === 200) {
localStorage.setItem("token", res.data.data);
this.$message({
title: "登录成功",
message: "登录成功!正在为您跳转页面...",
type: "success",
duration: 1000,
showClose: false,
onClose: () => {
this.$store.dispatch("setNoToken", false);
this.$router.go(-1);
},
});
} else {
this.$alert(
`错误代码${res.data.code}:${res.data.message}`,
"登录失败",
{
type: "error",
}
);
}
});
一般是在请求头里加入Authorization
字段,并加上Bearer
标注。
例如,在SDUOJ项目中,是这样使用的:
import axios from 'axios';
axios.post(
'/test', // url
{num: 1}, // data
{
headers: {
Authorization: `Bearer ${token}`
}
}, // options
).then((res)=>{
console.log(res);
})
服务器会负责接收token并验证,当验证通过后则进行业务处理。整个流程就是这样的:
前面也有提到,JWT的payload
模块可以携带一些业务逻辑所必要的非敏感信息。因此,前端需要能够解析出JWT字符串。
举个例子,在sduoj中,需要判断用户的登录信息是否合法,其中一项评判标准就是JWT是否过期。在后端的服务器代码中存在这一部分逻辑,而在前端的代码中,也包含了这一段逻辑代码。
前端的代码逻辑中,仅在用户首次与sduoj的前端页面建立会话时进行判断。因此,判断JWT是否过期的代码存放在了页面的主组件App.vue
中。具体代码如下:
// 首先判断localStorage中是否存在token字段
if(localStorage.getItem("token") !== null) {
/**
* 注意,由于localStorage是以字符串形式取出的
* 当token字段是空字符串时,javascript会将其转换为布尔值false。
* 因此需要增加!==null进行判断。
*/
// 使用模块加载器将通过yarn安装的jsonwebtoken加载进来
let jwt = require("jsonwebtoken");
// 使用jsonwebtoken解析JWT字符串
const TOKEN = jwt.decode(localStorage.getItem("token"));
/**
* 通过jsonwebtoken解析后将会得到包含payload非敏感部分明文的JSON对象
* 提取jwt的过期时间,过期时间处在exp字段下
* 注意:jwt的时间单位比javascript、java等语言的时间单位要大3个数量级
* 因此,需要给解析出的时间乘上1000
*/
let exp = TOKEN.exp * 1000;
// 判断当前时间是否已超过token的过期时间
if (exp <= new Date().getTime()) {
this.$message({
message: "您的身份认证已过期,请重新登陆",
type: "warning",
duration: 1000,
onClose: () => {
/**
* 消息提示关闭后执行两项任务
* 1.将过期的token字段从localStorage中移除
* 2.为用户跳转至登录界面
*/
localStorage.removeItem("token");
this.$router.push("/login");
},
});
}
} else {
// 当localStorage中不存在token字段时,自动跳转至登录界面
this.$router.push("/login");
}