身份验证是现代 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);