身份验证是现代 Web 应用程序中至关重要的一部分。它确保只有经过授权的用户才能访问敏感数据和功能。而 JSON Web Token(JWT) 作为一种流行的身份验证机制,成为了目前最炙手可热的跨域认证解决方案之一。它利用令牌在客户端和服务器之间进行交互,从而验证用户的身份。本文将介绍 JWT 认证的相关知识以及利用 Node.js 和 Vue2 实现前后端之间的令牌交互的方法。

1.JWT的组成

JWT是一种用于身份验证和授权的开放标准,它由三部分组成:头信息(header)、消息体(payload)和签名(signature)。它以长字符串的形式存在,中间由点号(.)分隔成三个部分。

      头信息包含了关于生成 JW T的算法和类型等元数据,通常包括两个字段:alg(算法)和 type(类型),它们都是 Base64Url 编码后的 JSON 对象。
      消息体包含与 JWT 相关的声明信息,比如用户 ID、角色、过期时间等。这些声明都是 JSON 对象,同样采用 Base64Url 编码。可以在消息体中添加自定义的声明信息,但要注意不要泄露敏感信息。
      签名则是使用 Secret Key 或 RSA 等公私钥对头信息和消息体进行加密,以确保 JWT 的完整性和真实性。签名使用头信息中指定的算法进行计算,并将结果附加到 JWT 的末尾,同样采用 Base64Url 编码。

当客户端发送请求时,服务器会验证 JWT 的合法性,确认 JWT 中的声明信息是否真实可信。为了验证 JWT 的合法性,服务器需要先解码 JWT,获取其中的头信息和消息体,并重新计算签名,以验证其有效性。如果签名验证通过,服务器便可进一步根据 JWT 中的声明信息完成对用户的认证和授权。

2.JWT特点

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

3.JWT认证流程

      1.服务端在登录接口中将用户信息等进行加密成 JWT 发送给客户端。
      2.客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
      3.此后,客户端每次与服务器通信,都要带上这个 JWT。可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。
      4.服务端使用自己保存的 key 解密 JWT,计算、验证签名以判断该JWT 是否可信。

4.代码实现

(1)后端生成token

jsonwebtoken模块:用于 token 的生成和验证。jsonwebtoken文档

import jwt from 'jsonwebtoken';
let secretkey="填写用于加密的key", 
let expiresIn=60 * 60 * 2 //填写token的过期时间 单位秒 这里是2小时 
// info 为加密的信息  尽量不要包含敏感信息
export const getToken=async (ctx:Context,info:object,time:number=expiresIn):Promise<string>=>{
	try{
		const token = jwt.sign({info}, secretkey, {expiresIn:time})
		return token;
	}catch(e){
		throw new ErrorPlus('生成token失败');  // 抛出错误  这里是我自己定义的异常类
	}
}

在用户登录成功会后调用函数生成 token 并将 token 返回给前端。

(2)前端处理token

前端在收到 token 后首先将 token 存储在 localstorage 或 cookies 中:

localStorage.setItem("token", token)

配置 axios 请求拦截器和响应拦截器:

import axios from 'axios'
// 创建实例时配置默认值
const instance = axios.create({
    baseURL,          // 请求的基地址
    timeout: 10000    //超时时间
})
 
// http request 拦截器:是在ajax请求发出之前的操作
// 给每个请求头加上token 用于后端验证
instance.interceptors.request.use(
    config => {
        let token = localStorage.getItem('mx_token');
        // 判断是否存在token 如果存在则在每个 http header都加上token
        if (token) {
            config.headers.Authorization = `token ${token}`;
        }
        //必须return回去
        return config;
    },
    err => {
        return Promise.reject(err);
    }
)
 
//axios响应拦截器
// 如果我们使用中需要统一处理所有 http 请求和响应, 就需要使用 axios 拦截器。
// 通过配置 http response inteceptor, 如果后端接口返回 401 Unauthorized(说明该用户未授权), 用户需重新登录。
instance.interceptors.response.use(
    response => {
        return response.data;
    },
    err => {
        if (err.response) {
            switch (err.response.status) {
                case 401:
                    //返回401清除token信息并跳转到登录界面
		    localStorage.removeItem('mx_token');
                    router.replace({
                        path: '/login'
                    })
            }
        }
        return Promise.reject(err.response.data);
    }
)

(3)后端编写中间件验证token

解密函数:从请求头中取出token,并验证token的正确性,如果发现没有token,或token过期,token错误等抛出401异常。

// 解密token
export const deToken = async (token: string): Promise<object> => {
  try {
	  if(!token){
		  throw new ErrorPlus('Token为空',401);
	  }

	  let token_arr=token.split(' ');
	  if(token_arr[0]!=='token'){
		  throw new ErrorPlus('Token格式错误',401);
	  }
	  
    // 进行解密,解密为uid和scope
    const res = jwt.verify(token_arr[1], secretkey) as any;
    return res;
  } catch (error) {
    if ((error as any).name === 'TokenExpiredError') {
	    throw new ErrorPlus('Token已过期',401);
    } else {
        throw error;
    }
  }
};

编写身份认证的 node 中间件,即在每一个请求处理之前,首先验证 token 的正确性。如果验证通过则继续向下执行,否则直接抛出异常。

const checkAuth=async (ctx:Context,next:Next)=>{
	try{
		let token = ctx.req.headers['authorization'] as string;
		const res=await deToken(token) as any;
		ctx.userid=res.info;
		await next();
	}catch(e){
		throw e;
	}
}

在路由中使用中间件:

import checkAuth from '../middleware/jwt';

// 全局使用
app.use(checkAuth);

// 单个路由使用
router.post('/update_info',checkAuth,update_info);