对于许多人来说,Token依然是一个模糊的概念:它是如何工作的?为什么它如此重要?它和JWT(JSON Web Token)又有什么关系?如果你也曾对这些问题产生过好奇,那这篇文章将带你深入了解前端开发中的 Token 机制,解锁身份认证的背后秘密。
初识Token
**token是什么**:token的意思是”令牌“,是服务端生成的一串字符串作为客户端进行请求的一个标识,当用户第一次登录后,服务器生成一个token并将此token返回给客户端,以后客户端只需带上整个token前来请求数据即可,无需再次带上用户名和密码,简单的token由以下三部分组成:
1)uid:用户唯一的身份标识
2)time:当前时间的时间戳
3)sign:签名—将请求URL、时间戳、uid进行一定的算法加密
token进行身份验证的流程大致如下:
**token的优势**:与传统的cookie和session认证方式相比,token具有一系列显著的优势,以下是token相较于cookie和session的主要优势:
1)无状态:
token是无状态的,意味着服务器不需要存储任何关于用户会话的信息,每次请求都携带自包含的token且其内部包含了用户的所有认证信息。 cookie/session通常需要服务器存储用户的会话状态,服务器必须维持一个会话存储,这种状态管理增加了服务器的负担。
2)跨域支持:
token不依赖浏览器的Same-Origin Policy,可以跨域传递。 cookie/session通常受限于同源策略,跨域请求时浏览器需要做额外的配置才能传递cookie,如果没有正确设置可能会导致跨域问题。
3)适合移动端与多平台:
token适用于各种客户端,不仅限于浏览器还包括移动端应用或桌面应用等。 cookie/session主要依赖浏览器的存储机制,在非浏览器环境中使用会受限,通常需要额外的处理。
4)避免跨站请求伪造(CSRF)攻击:
token是作为HTTP请求头中的Authorization字段传递的,而不是通过cookie自动附加到请求中的,因此它不容易受到 CSRF(跨站请求伪造)攻击。
5)灵活的过期和刷新机制:
token通常会嵌入过期时间允许对token的生命周期进行精确控制,一旦过期客户端可以通过刷新机制来获得新的认证令牌,而不需要重新登录。 cookie/session通常依赖于服务器的会话过期时间,客户端和服务器之间需要保持会话状态,若过期用户通常需要重新登录,刷新机制不像token那么灵活。
6)安全性:
token可以被加密或签名,保证传输过程中数据的完整性和安全性,即使被截获,只要没有解密或签名密钥攻击者也无法篡改其中的数据。 cookie/session虽然可以设置HttpOnly和Secure属性来增强安全性,但其本身依赖于浏览器的安全机制可能受到 XSS(跨站脚本攻击)的影响。
**token登录流程**:前端使用token进行登录的流程通常如下:
1)用户提交登录请求:用户在前端页面上输入用户名和密码,并提交登录表单。
// 例如使用 fetch 发起请求
fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: 'user',
password: 'password',
}),
})
.then(response => response.json())
.then(data => {
// 后续处理登录成功的情况
});
2)后端验证用户信息:后端收到登录请求后验证用户名和密码是否正确,如果验证成功后端生成一个token并将其返回给前端,该token通常包含了用户的信息(如用户 ID、角色等),并且在一定时间内有效。
// 后端伪代码,生成 JWT Token
const jwt = require('jsonwebtoken');
const secretKey = 'yourSecretKey';
const token = jwt.sign(
{ userId: user.id, role: user.role }, // 用户数据
secretKey, // 密钥
{ expiresIn: '1h' } // 过期时间
);
res.json({ token }); // 将 Token 返回给前端
3)前端保存token:前端接收到后端返回的token后通常会将其保存在浏览器的存储中,常见的存储方式有:LocalStorage、SessionStorage和Cookie。
// 登录成功后保存 token
localStorage.setItem('token', data.token);
// 如果后端返回的是包含 HttpOnly 的 cookie,浏览器会自动处理
document.cookie = `token=${data.token}; path=/; secure; HttpOnly`;
4)使用token访问受保护资源:在后续的请求中前端会将token附加到HTTP请求的Authorization头部,以便后端可以验证该请求是否授权。
// 获取存储的 token
const token = localStorage.getItem('token');
fetch('/api/protected-resource', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`, // 在 Authorization 头中发送 Token
},
})
.then(response => response.json())
.then(data => {
// 处理受保护资源的数据
});
5)后端验证token:后端在接收到带有token的请求后会验证该token的有效性,常见的验证方式包括:检查token是否存在、是否已经过期、是否合法。
const jwt = require('jsonwebtoken');
const secretKey = 'yourSecretKey';
const token = req.headers['authorization']?.split(' ')[1]; // 获取 Token
if (!token) {
return res.status(401).json({ message: 'Token not provided' });
}
jwt.verify(token, secretKey, (err, decoded) => {
if (err) {
return res.status(401).json({ message: 'Invalid or expired token' });
}
// Token 验证成功,decoded 是 Token 中的用户信息
req.user = decoded;
next(); // 继续执行后续操作
});
6)token过期与刷新:如果token过期后端会返回一个错误提示(如 401 Unauthorized),前端可以通过以下方式处理,提供刷新token的机制,在token过期时前端可以发送Refresh Token请求来获取新的Access Token;如果没有使用刷新token,用户需要重新登录以获得新的token。
// 使用 Refresh Token 刷新 Access Token
fetch('/api/refresh-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
refreshToken: localStorage.getItem('refreshToken'),
}),
})
.then(response => response.json())
.then(data => {
// 存储新的 Token
localStorage.setItem('token', data.newToken);
});
7)登出:当用户登出时,前端需要清除存储的 Token。
// 清除本地存储的 token
localStorage.removeItem('token');
初识JWT
**JWT**(JSON Web Token):是一种特定格式的token,它是基于json的开放标准用于在网络应用环境中安全地传输声明(如用户身份、权限等信息),token是一个抽象的概念,可以指代任何形式的认证令牌,而JWT是一种特定类型的token,它采用json格式,并且具有标准化的结构,可以用于身份验证和数据传输,其主要包含以下三部分:
1)Header(头部):描述token的类型(通常是JWT)和使用的签名算法(如HMAC SHA256或RSA)。
2)Payload(负载):存放声明通常是用户身份信息、过期时间等,JWT可以通过此部分传递一些必要的信息。
3)Signature(签名):用于验证JWT的来源和数据完整性,它是Header和Payload加密后的结果,确保token没有被篡改。
**JWT工作原理**:用户的信息通过token字符串的形式保存在客户端浏览器当中,服务器通过还原token字符串的形式来认证用户的身份,如下图所示:
**JWT使用方式**:客户端收到服务器返回的JWT之后通常会将它存储在localStorage或sessionStorage中,此后客户端每次与服务器通信都要带上这个JWT字符串,从而进行身份认证,推荐的做法就是把JWT放在HTTP请求的Authorization字段中,格式如下:
Authorization: Bearer <token>
这里我们可以借助node中的express框架来简单实现以下,终端执行如下命令安装JWT相关的包:
1)jsonwebtoken:用于生成JWT字符串
2)express-jwt:用于将JWT字符串解析还原成JSON对象
npm install jsonwebtoken express-jwt
// 01导入用于生成JWT字符串的包
const jwt = require("jsonwebtoken")
// 02导入用于生成客户端发送过来的JWT字符串,解析还原成JSON对象的包
const expressJWT = require('express-jwt')
为了保证JWT字符串的安全性防止JWT字符串在网络传输过程中被别人破解,我们需要专门定义一个用于加密和解密的secret密钥:
1)当生成JWT字符串的时候,需要使用secret密钥对用户的信息进行加密,最终得到加密好的JWT字符串
2)当把JWT字符串解析还原成JSON对象的时候,需要使用secret密码进行解密
// secrect 密钥的本质:就相当于一个字符串
const secret = "secret 010 020"
调用jsonwebtoken包提供的sign()方法,将用户的信息加密成JWT字符串响应给客户端:
// 登录接口
app.post('/api/login', (req, res) => {
// 用户登录成功之后,生成JWT字符串,通过token属性响应给客户端
res.send({ status: 200, token: jwt.sign({ name: 'admin' }, secret, { expireIn: '30s' })})
})
客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的Authorization字段将token字符串发送到服务器进行身份认证,此时服务器可以通过express-jwt这个中间件,自动将客户端发送过来的Token 解析还原成JSON 对象:
// 使用app.use()来注册中间件
// expressJWT({ secret: secret })就是用来解析toke的中间件
// .unless({ path: [/^\/api\/login/, /^\/api\//] })是用来排除不需要token验证的接口
app.use(expressJWT({ secret: secrect }).unless({ path: [/^\/api\/login/, /^\/api\//] }))
当express-jwt这个中间件配置成功之后,即可在那些有权限的接口中使用req.user对象来访问从JWT字符串中解析出来的用户信息了,示例代码如下:
// 这是一个有权限的接口
app.get('/api/admin', (req, res) => {
res.send({ status: 200, message: 'admin', data: req.user })
})
当使用express-jwt解析token字符串时,如果客户端发送过来的token字符串过期或不合法,会产生一个解析失败的错误影响项目的正常运行,我们可以通过express的错误中间件,捕获这个错误并进行相关的处理,示例代码如下:
app.use((err, req, res, next) => {
// token解析失败导致的错误
if (err.name === 'UnauthorizedError') {
res.status(401).send({ status: 401, message: 'token解析失败' })
} else {
res.status(500).send({ status: 500, message: '服务器内部错误' })
}
})
无感刷新
接下来开始实现token的无感刷新,下面这段代码的核心目的是自动刷新Access Token,从而确保在token过期或失效时用户能够继续进行API请求,而无需人工干预,如下所示:
let isRefreshing = false;
let requests = [];
axiosInstance.interceptors.response.use(
(res) => res,
async (err) => {
// 只处理 401 错误
if (err.response && err.response.status === 401) {
const originalRequest = err.config;
// 如果正在刷新 Token,其他请求排队等待
if (isRefreshing) {
return new Promise((resolve) => {
requests.push((token) => {
originalRequest.headers['Authorization'] = `Bearer ${token}`;
resolve(axiosInstance(originalRequest));
});
});
}
// 开始刷新 Token
isRefreshing = true;
try {
const { access_token } = await refreshToken();
if (access_token) {
// 更新全局请求头
axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;
// 处理等待中的请求
requests.forEach((cb) => cb(access_token));
requests = []; // 清空请求队列
// 重试原始请求
originalRequest.headers['Authorization'] = `Bearer ${access_token}`;
return axiosInstance(originalRequest);
}
// 如果没有获取到新的 token,抛出原错误
throw err;
} catch (e) {
// 刷新失败,处理失败
return Promise.reject(e);
} finally {
// 无论刷新成功与否,标记刷新操作结束
isRefreshing = false;
}
}
// 其他错误直接抛出
return Promise.reject(err);
}
);
这种设计非常适合使用JWT或类似认证机制的应用程序,尤其是在前后端分离的Web应用中确保用户在token过期后仍能无缝访问接口,其主要特点是:
1)自动检测401错误,并自动处理token刷新。
2)同时处理多个请求,避免重复刷新token。
3)支持刷新失败的处理,保证系统稳定性。
版权归原作者 亦世凡华、 所有, 如有侵权,请联系我们删除。