开启生长之旅!这是我参与「日新计划 12 月更文挑战」的第10天,点击检查活动详情
前言:下周轮到我做技能共享,寻思共享点啥能配的上我滴身份,嗯?我是什么身份?
———— JSON Web Token(缩写 JWT)是目前最流行的跨域认证处理计划,本文介绍它的原理和用法。
———— 下面说下春野的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)。
它是一个很长的字符串,中心用点(.)分隔成三个部分。留意,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
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()],
});
后台详解
- 根本装备
- 新建文件夹jwt-server
- npm init -y (初始化项目,生成package.json文件)
- npm install koa @koa/router koa-bodyparser basic-auth jsonwebtoken –save 装置第三方依靠
- 新建app.js进口文件 而且装备
- 装备发动办法并发动项目
- 发动成功
验证用户名密码
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;
第一阶段作用
生成Token
- token.js新增获取token代码(generateToken头部引进)
新增创立东西办法文件
// 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,
};
第二阶段作用
验证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,
};
第三阶段作用
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,
};
第四阶段作用图
人物权限校验
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写入接口权限等级
token.js 里边获取用户生成token时的权限
第五阶段作用图
总结
反常处理最好写成全局的,要不然逻辑太冗余了。 像JSONWebToken这样好玩的东西咱们都理解了,那今后还不是海阔任鱼跃,天高任鸟飞了…咳 下班!
往期精彩文章链接
Token【JWT与传统认证流程】
相关资料
- jwt.io/introductio…
- www.ruanyifeng.com/blog/2018/0…
- www.bilibili.com/video/BV1S3…
- 本文源码:github.com/JinChunYe2/…
水平有限,还不能写到尽善尽美,期望我们多多沟通,跟春野一同前进!!!