前言

  • 为什么要用 token
HTTP 是一种无状态的协议,也就是 HTTP 没法保存客户端的信息,没办法区分每次请求的不同。

设想这样的场景,A 和 B 同时修改个人的文章,服务器同时收到两个 post 请求,但浏览器并不知道哪个请求是 A 哪个请求是 B,需要一个标识符(token)来标记一串信息并且在请求的时候带上

  • token 是什么
Token 是服务器生成的一串字符,作为客户端请求的令牌。当第一次登陆后,服务器会分发 Tonken 字符串给客户端。后续的请求,客户端只需带上这个 Token,服务器即可知道是该用户的访问。

个人理解就是一串被服务器加密过的个人信息,比如下面这个:

acess_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1ZWRhMTBjOTI0NThmNDAwMmFjZDEyMTAiLCJuaWNrX25hbWUiOiLlsI_osaoxOTk2IiwiY3JlYXRlZF90aW1lIjoiMjAyMC0wNi0wNVQwOTozMDo0OS42MThaIiwidXBkYXRlZF90aW1lIjoiMjAyMC0wNi0wNVQwOToyOToyMC4wNzlaIiwiaWF0IjoxNTkxMzQ5NDY4LCJleHAiOjE1OTE5NTQyNjh9.GmUJRXHed7M1xJyPaFFgaQKJoS-w8-l3N_PQFPiwwTE

服务器通过秘钥解密从而获得当前请求者的信息

技术栈

  • 前端:vue + ssr
  • 后端:egg(一个 node 框架) + ts
  • 数据库:redis + mongo
  • 部署:Docker
  • 构建:Jenkins

blog 已经基本完成,并且用上了 ssr 渲染

本文主要是讲 token 验证,其他不再累述

token 验证设计

我的 blog 对 get 类型的请求不做 token 验证,其他会修改资源的请求如 POST、PUT 会做 token 验证

可拆解为下面 3 个步骤

  • 客户端用户登录,服务端根据用户信息生成 token 并在客户端持久化存储
  • 客户端请求,带上 token
  • 服务端验证 token,若失败则直接返回错误状态

1.前端(vue.js)

使用 axios 库,并且在 request 拦截器中把 token 塞到请求头 header ,在 response 拦截器中统一对错误状态码进行全局提示

登录成功之后把 token 写入 浏览器缓存 中,每次请求都带上

import Vue from 'vue';import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';axios.interceptors.request.use(  async (config: AxiosRequestConfig) => {    const acess_token = await Vue.prototype.$getCacheData('acess_token'); # 缓存中读取token    if (acess_token) {      config.headers.acess_token = acess_token;    }    return config;  },  (err: any) => Promise.reject(err),);axios.interceptors.response.use(  (response: AxiosResponse) => {    if (response.data.ret === 200) {      return response;    } else {      Vue.prototype.$global_fail(response.data.content);      return Promise.reject(response);    }  },  (err: any) => {    console.log(err);    if (err.code === 'ECONNABORTED' && err.message.indexOf('timeout') !== -1) {      Vue.prototype.$global_error('请求超时,请联系管理员');    }    if (err.response) {      Vue.prototype.$global_error(decodeURI(err.response.data.msg || err.response.data.message));    }    return Promise.reject(err);  },);

关于前端缓存,这里我推荐一个 localForage 库,很实用

localForage 是一个 JavaScript 库,能存储多种类型的数据,而不仅仅是字符串。localForage 有一个优雅降级策略,若浏览器不支持 IndexedDB 或 WebSQL,则使用 localStorage。

但注意,它的操作都是异步的,可以自己封装一层把它改成同步的

import Vue from 'vue';import localForage from 'localforage';Vue.prototype.$setCacheData = async (key: string, data: any): Promise<void> => await localForage.setItem(key, data);Vue.prototype.$getCacheData = async (key: string): Promise<string | null> => await localForage.getItem(key) || null;Vue.prototype.$clearCache = () => localForage.clear();

2.后端(egg.js)

  • 生成 token

用户登录,使用 jsonwebtoken 生成 token

jsonwebtoken 的详情请点击: node-jsonwebtoken

加密解密使用也比较简单,直接给出 UserService 方法,其中 secret 为秘钥,且可以设置 token 过期时间

# /app/service/user.tsimport * as jwt from 'jsonwebtoken';export default class UserService extends Service {  private secret = 'Hello__World'; # 秘钥  async createToken(user: User): Promise<string> {    const payload = {      _id: user._id,      nick_name: user.nick_name,      created_time: user.created_time,      updated_time: user.updated_time,    };    return jwt.sign(payload, this.secret, { expiresIn: '7d' }); # 过期时间  }  checkToken(token: string): User {    try {      # 根据秘钥解密token      return jwt.verify(token, this.secret);    } catch (e) {      throw '无效的token';    }  }}

在中间件中进行 token 验证,若失败直接返回

  • 中间件验证

开启 verify 中间件,并只对特定的 POST 请求进行验证:

开启中间件

# /config/config.default.tsconfig.middleware = ['verify'];config.verify = {  enable: true,  # 只对POST请求做验证  match(ctx) {    return ctx.request.method === 'POST';  },};

在 verify 调用 checkToken 方法验证 token

# /app/middleware/verify.tsmodule.exports = () => {  return async (ctx, next) => {    if (ctx.path.startsWith('/api/user/login') || ctx.path.startsWith('/api/user/sendCode') || ctx.path.startsWith('/api/user/register')) {      return await next();    }    try {      const acess_token: string = ctx.request.header.acess_token;      if (!acess_token) {        throw '请登录';      } else {        await ctx.service.user.checkToken(acess_token); # 验证token        return await next();      }    } catch (e) {      # token验证失败会走到这里,返回自定义状态码      console.log(e);      ctx.body = {        ret: 304,        content: `${e}`,      };    }  };};

至此 token 验证就完了,如有不足,欢迎指出


END