开启生长之旅!这是我参与「日新计划 12 月更文挑战」的第10天,点击检查活动详情

前言:下周轮到我做技能共享,寻思共享点啥能配的上我滴身份,嗯?我是什么身份?
———— JSON Web Token(缩写 JWT)是目前最流行的跨域认证处理计划,本文介绍它的原理和用法。
———— 下面说下春野的JWT鉴权之路。

Koa2 + Vue【JWT鉴权之路】

一、JWT

1.跨域认证的问题

互联网服务离不开用户认证。一般流程是下面这样:

  • 用户向服务器发送用户名和密码。
  • 服务器验证经过后,在当时对话(session)里边保存相关数据,比如用户人物、登录时刻等等。
  • 服务器向用户回来一个 session_id,写入用户的 Cookie。
  • 用户随后的每一次恳求,都会经过 Cookie,将 session_id 传回服务器。
  • 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

这种形式的问题在于,扩展性(scaling)不好。单机当然没有问题,假如是服务器集群,或许是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。

举例来说,A 网站和 B 网站是同一家公司的相关服务。现在要求,用户只要在其中一个网站登录,再拜访另一个网站就会自动登录,请问怎样完成?

一种处理计划是 session 数据持久化,写入数据库或其他持久层。各种服务收到恳求后,都向持久层恳求数据。这种计划的优点是架构明晰,缺陷是工程量比较大。另外,持久层万一挂了,就会单点失利。

另一种计划是服务器索性不保存 session 数据了,一切数据都保存在客户端,每次恳求都发回服务器。JWT 便是这种计划的一个代表。

2.JWT 的原理

JWT 的原理是,服务器认证今后,生成一个 JSON 目标,发回给用户,就像下面这样。

{
  "姓名": "张三",
  "人物": "管理员",
  "到期时刻": "2018年7月1日0点0分"
}

今后,用户与服务端通讯的时分,都要发回这个 JSON 目标。服务器完全只靠这个目标确定用户身份。为了避免用户篡改数据,服务器在生成这个目标的时分,会加上签名(详见后文)。

服务器就不保存任何 session 数据了,也便是说,服务器变成无状况了,然后比较简单完成扩展。

3.瞅瞅JWT

从前有一天,维基百科说:JSON Web Token(JWT,读作 [/dt/]),是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。

它由三部分组成: 头信息(header), 音讯体(payload)和签名(signature)

Koa2 + Vue【JWT鉴权之路】
它是一个很长的字符串,中心用点(.)分隔成三个部分。留意,JWT 内部是没有换行的,这儿仅仅为了便于展示,将它写成了几行。

写成一行,便是下面的姿态。

Header.Payload.Signature

1.1 Header

Header 部分是一个 JSON 目标,描绘 JWT 的元数据,通常是下面的姿态。

// HS256 表明运用了 HMAC-SHA256 来生成签名。
{
  "alg": "HS256",
  "typ": "JWT"
}

上面代码中,alg特色表明签名的算法(algorithm),默许是 HMAC SHA256(写成 HS256);typ特色表明这个令牌(token)的类型(type),JWT 令牌统一写为JWT。 最后,将上面的 JSON 目标运用 Base64URL 算法(详见后文)转成字符串。

1.2 Payload

Payload 部分也是一个 JSON 目标,用来寄存实际需求传递的数据。JWT 规矩了7个官方字段,供选用。

  • iss (issuer):签发人
  • exp (expiration time):过期时刻
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):收效时刻
  • iat (Issued At):签发时刻
  • jti (JWT ID):编号

除了官方字段,你还能够在这个部分界说私有字段,下面便是一个例子。

{
  "sub": "KaiHui",
  "name": "Chunye",
  "id": "156",
  "admin": true,
  "iat": "7200000"
}

留意,JWT 默许是不加密的,任何人都能够读到,所以不要把隐秘信息放在这个部分。

这个 JSON 目标也要运用 Base64URL 算法转成字符串

1.3 Signature

Signature 部分是对前两部分的签名,避免数据篡改。

首要,需求指定一个密钥(secret)。这个密钥只有服务器才知道,不能走漏给用户。然后,运用 Header 里边指定的签名算法(默许是 HMAC SHA256),依照下面的公式产生签名。

key = 'secretkey'
unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload)  
signature = HMAC-SHA256(key, unsignedToken)
1.未签名的令牌由base64url编码的头信息和音讯体拼接而成(运用"."分隔)
2.签名则经过私有的key核算而成
3.在未签名的令牌尾部拼接上base64url编码的签名(相同运用"."分隔)便是JWT了:
token = HMACSHA256(base64UrlEncode(header) + 
		'.' + base64UrlEncode(payload) ,
		secret)

算出签名今后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,就能够回来给用户。

1.4 Base64URL

前面说到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法根本相似,但有一些小的不同。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里边有特别含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这便是 Base64URL 算法。

二、JWT 的运用办法

计划一:

1.客户端收到服务器回来的 JWT,能够储存在 Cookie 里边,也能够储存在 localStorage。

2.尔后,客户端每次与服务器通讯,都要带上这个 JWT。你能够把它放在 Cookie 里边自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 恳求的头信息Authorization字段里边。

Authorization: Bearer <token>

3.服务端运用自己保存的key核算、验证签名以判别该JWT是否可信。

计划二:

跨域的时分,JWT 就放在 POST 恳求的数据体里边。

三、JWT 的几个特色

  • JWT 默许是不加密,但也是能够加密的。生成原始 Token 今后,能够用密钥再加密一次。

  • JWT 不加密的情况下,不能将隐秘数据写入 JWT。

  • JWT 不仅能够用于认证,也能够用于交换信息。有用运用 JWT,能够下降服务器查询数据库的次数。

  • JWT 的最大缺陷是,由于服务器不保存 session 状况,因而无法在运用过程中废止某个 token,或许更改 token 的权限。也便是说,一旦 JWT 签发了,在到期之前就会一直有用,除非服务器部署额外的逻辑。

  • JWT 本身包括了认证信息,一旦走漏,任何人都能够获得该令牌的一切权限。为了削减盗用,JWT 的有用期应该设置得比较短。关于一些比较重要的权限,运用时应该再次对用户进行认证。

  • 为了削减盗用,JWT 不应该运用 HTTP 协议明码传输,要运用 HTTPS 协议传输。

四、思路

1.前端登陆校验

  • 首要表单校验,校验成功账号密码发送后台。
  • 后台接收数据,进行数据校验,校验成功运用JWT生成Token回来给前端。
  • 前端恳求成功,前端拿到Token之后把Token存储到Cookie或许localStorage中。
  • 之后部分恳求做token权限验证处理(后续会在一切token中参加token有用认证,假如无效,则从头登陆)

2.权限校验

  • 部分前端恳求时恳求头都会带着Token
  • 后台收到恳求先校验token,假如token合法(token正确、没过期、有权限),则执行next()。
  • 不然直接回来401以及对应的message。

五、上代码

前端项目根本装备

  • Vue3 + Vite + Element-plus

Koa2 + Vue【JWT鉴权之路】

login.vue

// 主要代码 login.vue
<template>
  <div id="login">
    <div class="login-wrapper">
      <div class="title-wrapper">
        <h1 class="title">登录</h1>
      </div>
      <div class="form-wrapper">
        <el-form
          ref="formRef"
          :model="formData"
          :rules="rulesAccount"
          label-width="80px"
        >
          <el-form-item label="用户名" prop="username">
            <el-input v-model="formData.username"></el-input>
          </el-form-item>
          <el-form-item label="密码" prop="password">
            <el-input type="password" v-model="formData.password"></el-input>
          </el-form-item>
          <el-form-item class="btn-item">
            <el-button type="primary" @click="login">登录</el-button>
          </el-form-item>
          <el-form-item class="btn-item">
            <el-button type="primary" @click="verifyToken"
              >验证token有用性</el-button
            >
          </el-form-item>
        </el-form>
      </div>
    </div>
  </div>
</template>
<script setup>
// 引进reactive(界说引证类型)
import { reactive } from "vue";
// 引进路由函数
import { useRouter } from "vue-router";
// 引进axios
import axios from "axios";
// element提示
import { open1, open4 } from "../../hooks/ElMessage.js";
// 引进login校验规矩
import { rulesAccount } from "./config/login-config";
// 界说双向绑定参数
let formData = reactive({
  username: "",
  password: "",
});
// 注册路由
const router = useRouter();
// 登陆验证
const login = async () => {
  const { username, password } = formData;
  console.log(formData, "formData");
  // 向后台发送账号密码,获取回来信息
  const tokenResult = await axios.post("/api/token", {
    username,
    password,
  });
  // 假如回来过错码
  const CTXBODY = tokenResult.data;
  if (CTXBODY.code === 10001) {
    open4(CTXBODY.msg);
    return;
  }
  // 存储token
  localStorage.setItem("token", tokenResult.data.token);
  // 跳转页面
  router.push("/list");
  // 登陆成功提示
  open1(CTXBODY.msg);
};
// 查验token有用性
const verifyToken = async () => {
  const userToken = localStorage.getItem("token");
  console.log(userToken);
  const verifyTokenMsg = await axios.post("/api/token/verify", {
    userToken,
  });
  if (verifyTokenMsg.data.code === 200) {
    open1(verifyTokenMsg.data.msg);
    return;
  }
  open4("token失效!");
};
</script>

list.vue

// 主要代码
<template>
  <div id="list">
    <el-button type="primary" @click="getContent">获取文章内容</el-button>
    <el-button type="primary" @click="addContent">新增文章内容</el-button>
    <h1>{{ resultDate }}</h1>
  </div>
</template>
<script setup>
import axios from "axios";
import { Base64 } from "js-base64";
import { ref } from "vue";
// 初始化响应值
let resultDate = ref();
// 一般恳求,不带着token
const getContent = async () => {
  const result = await axios.get("/api/content");
  resultDate.value = result.data;
  console.log(resultDate.value);
};
// 需鉴权恳求,带着token
const addContent = async () => {
  const result = await axios({
    url: "/api/content",
    method: "post",
    headers: { Authorization: _encode() },
  });
  resultDate.value = result.data;
};
// 生成加密token
const _encode = () => {
  const token = localStorage.getItem("token");
  const encoded = Base64.encode(token + ":");
  return `Basic ${encoded}`;
};
</script>

vite.config.js

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
// 处理跨域
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ""),
      },
    },
  },
  plugins: [vue()],
});

后台详解

  • 根本装备

Koa2 + Vue【JWT鉴权之路】

  • 新建文件夹jwt-server
  • npm init -y (初始化项目,生成package.json文件)
  • npm install koa @koa/router koa-bodyparser basic-auth jsonwebtoken –save 装置第三方依靠
  • 新建app.js进口文件 而且装备
    Koa2 + Vue【JWT鉴权之路】
  • 装备发动办法并发动项目
    Koa2 + Vue【JWT鉴权之路】
  • 发动成功
    Koa2 + Vue【JWT鉴权之路】

验证用户名密码

users.js

// app/data/users.js
// 模仿数据库信息
const users = [
  {
    id: 1,
    username: "zhangsan",
    password: 123456,
    nickname: "张三",
  },
  {
    id: 2,
    username: "李四",
    password: 123456,
    nickname: "李四",
  },
];
module.exports = users;

app.js

const Koa = require("koa");
const bodyParser = require("koa-bodyparser");
// 引进token相关路由
const tokenRouter = require("./app/api/token");
const app = new Koa();
// 解析前端发送过来的数据, ctx.request.body
app.use(bodyParser());
// 注册 token 相关路由
app.use(tokenRouter.routes());
// 端口号装备
app.listen(3000);

token.js

// app/api/token.js
const Router = require("@koa/router");
const users = require("../data/users");
const tokenRouter = new Router({
  // 设置路由前缀 /token
  prefix: "/token",
});
tokenRouter.post("/", async (ctx) => {
  // 解构读取信息
  const { username, password } = ctx.request.body;
  // 验证账号密码是否正确
  const result = verifyUsernamePassword(username, parseInt(password));
  // 回来成果
  ctx.body = {
    ...result,
  };
});
function verifyUsernamePassword(username, password) {
  const index = users.findIndex((item) => {
    return item.username === username && item.password === password;
  });
  // users是否包括回来的数据,假如包括回来下标(为true),不然回来undefined
  const user = users[index];
  // 界说回来值
  const codeMssage = {
    code: 200,
    msg: "登陆成功~",
  };
  // 验证过错,回来过错参数
  if (!user) {
    codeMssage.code = 10001;
    codeMssage.msg = "账号密码过错~";
    return codeMssage;
  }
  // 验证成功
  return codeMssage;
}
// 导出
module.exports = tokenRouter;

第一阶段作用

Koa2 + Vue【JWT鉴权之路】

生成Token

  • token.js新增获取token代码(generateToken头部引进)
    Koa2 + Vue【JWT鉴权之路】

新增创立东西办法文件

// core/util.js
const jwt = require("jsonwebtoken");
const { secretKey, expiresIn } = require("../config/config");
// 运用jwt生成token,传入用户id和权限
function generateToken(uid, scope) {
  const token = jwt.sign(
    {
      uid,
      scope,
    },
    secretKey,
    {
      expiresIn,
    }
  );
  return token;
}
module.exports = { generateToken };

新增JWT装备文件

config/config.js
module.exports = {
  // 密钥
  secretKey: "Jwt%._CYweb#",
  // 有用时刻
  expiresIn: 24 * 60 * 60,
};

第二阶段作用

Koa2 + Vue【JWT鉴权之路】

验证token有用性

在平常我们写项目的时分,能够直接在axios里封装恳求带着token,(持久保持Token有用性)

前端login.vue中新增代码

// 查验token有用性
const verifyToken = async () => {
  const userToken = localStorage.getItem("token");
  const verifyTokenMsg = await axios.post("/api/token/verify", {
    userToken,
  });
  if (verifyTokenMsg.data.code === 200) {
    open1(verifyTokenMsg.data.msg);
    return;
  }
  open4("token失效!");
};

token校验

token.js 中引进auth办法进行token校验

// token.js 新增代码 (头部引进Auth)
// 验证token有用性的接口
tokenRouter.post("/verify", async (ctx) => {
  const token = ctx.request.body.userToken;
  const isValid = await Auth.verifyToken(token);
  if (isValid) {
    ctx.body = {
      code: 200,
      msg: "token验证成功",
    };
    return;
  }
  ctx.body = {
    code: 401,
    msg: "token失效",
  };
});

新增auth文件,对token校验

// middlewares/auth.js
const jwt = require("jsonwebtoken");
const { secretKey } = require("../config/config");
class Auth {
  // 界说静态办法verifyToken,验证token
  static async verifyToken(token) {
    try {
      jwt.verify(token, secretKey);
      return true;
    } catch (e) {
      return false;
    }
  }
}
module.exports = {
  Auth,
};

第三阶段作用

Koa2 + Vue【JWT鉴权之路】

token权限校验

新增content.js路由

// app/api/content.js 需在app.js里注册下路由
const Router = require("@koa/router");
const { Auth } = require("../../middlewares/auth");
const contentRouter = new Router({
  prefix: "/content",
});
// 获取文章内容,不需求鉴权
contentRouter.get("/", async (ctx) => {
  ctx.body = "获取文章内容成功";
});
// 新增这种post恳求,需求验证token是否有用合法,添加中心件鉴权
contentRouter.post("/", new Auth().middleware, async (ctx) => {
  ctx.body = "新增文章内容成功";
});
module.exports = contentRouter;

Auth添加权限校验规矩

const jwt = require("jsonwebtoken");
const { secretKey } = require("../config/config");
// 引进basic-auth,用作encode解密
const basicAuth = require("basic-auth");
class Auth {
  // 回来中心件函数
  get middleware() {
    return async (ctx, next) => {
      // 书写权限逻辑
      // 解析恳求头的authorization
      const token = basicAuth(ctx.request);
      // 查验token是否为空
      let msg = "恳求不合法";
      if (!token || token.name === null) {
        console.log(token.name, 11);
        ctx.body = {
          code: 10005,
          msg: msg,
        };
        return;
      }
      //   判别token是否正确(过期或不合法)
      try {
        var decoded = jwt.verify(token.name, secretKey);
      } catch (e) {
        // 1.token不合法
        // 2.token合法但过期 e.name TokenExpiredError
        if (e.name == "TokenExpiredError") {
          msg = "token已过期";
        }
        console.log(msg);
        ctx.body = {
          code: 10005,
          msg: msg,
        };
        return;
      }
      await next();
    };
  }
  // 界说静态办法verifyToken,验证token
  static async verifyToken(token) {
    try {
      jwt.verify(token, secretKey);
      return true;
    } catch (e) {
      return false;
    }
  }
}
module.exports = {
  Auth,
};

第四阶段作用图

Koa2 + Vue【JWT鉴权之路】

人物权限校验

auth.js新增权限校验特色

// 权限数字控制api拜访权限
  constructor(level) {
    // 用户
    Auth.USER = 2;
    // 管理员
    Auth.ADMIN = 8;
    // 传进来的用户权限
    this.level = level;
  }
  // 对权限进行对比,假如用户权限小于当时接口可操作权限,回来权限不足
  if (decoded.scope  <  this.level) {
        ctx.body = {
          code: 401,
          msg: "权限不足",
          request: `${ctx.method}`,
        };
        return;
  }
  await next();

content.js写入接口权限等级

Koa2 + Vue【JWT鉴权之路】

token.js 里边获取用户生成token时的权限

Koa2 + Vue【JWT鉴权之路】

第五阶段作用图

Koa2 + Vue【JWT鉴权之路】

总结

反常处理最好写成全局的,要不然逻辑太冗余了。 像JSONWebToken这样好玩的东西咱们都理解了,那今后还不是海阔任鱼跃,天高任鸟飞了…咳 下班!

往期精彩文章链接

Token【JWT与传统认证流程】

相关资料


  • jwt.io/introductio…
  • www.ruanyifeng.com/blog/2018/0…
  • www.bilibili.com/video/BV1S3…
  • 本文源码:github.com/JinChunYe2/…

水平有限,还不能写到尽善尽美,期望我们多多沟通,跟春野一同前进!!!