0


前端必备技能:全面解析 Token 在 Web 开发中的应用

对于许多人来说,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)支持刷新失败的处理,保证系统稳定性。

标签: 前端 token js

本文转载自: https://blog.csdn.net/qq_53123067/article/details/143867846
版权归原作者 亦世凡华、 所有, 如有侵权,请联系我们删除。

“前端必备技能:全面解析 Token 在 Web 开发中的应用”的评论:

还没有评论