当前位置: 首页 > news >正文

无感刷新 token

在这里插入图片描述

文章目录

    • 背景
    • 基本思路
    • 需解决的问题
      • 请求进入死循环
      • 标记刷新 token 请求避免请求拦截覆盖 refresh token
      • 并发刷新 token
    • 完整代码
    • 注意:拦截器注册顺序
    • 另一种方案:事件驱动刷新

前景提要:

  • ts 简易封装 axios,统一 API

  • 实现在 config 中配置开关拦截器

  • axios 实现请求 loading 效果

背景

无感刷新 token 一般指的是使用 refresh token 无感刷新 access token。

基本思路

设置全局请求拦截器,从 localstorage 或其他地方获取 token 放在请求头中携带。在响应拦截器中,判断响应结果中是否有 token,有就存下来放在 localstorage 或其他地方。

一句话总结就是本地有就带,响应有就存。

实现自动刷新,就是在响应拦截之前的基础上再加一个判断,如果 access token 过期了,就携带 refresh token 去请求认证中心的接口。拿到新的 access token 后,再次对业务接口发起请求。

  • 注意别忘了要让新业务请求携带最新的 access token。

在axios拦截器中重新发起请求,就是拿到业务请求的 config,用axios实例发起请求。所以也可以说,一个请求的本质就是它的config配置对象。

src\api\http\token.ts

import { AxiosResponse, InternalAxiosRequestConfig } from "axios";
import httpRequest from "..";
import { refreshAccessToken } from "../modules/refreshToken";export const ACCESS_TOKEN_KEY = "access_token";
export const REFRESH_TOKEN_KEY = "refresh_token";
export const UNAUTHORIZED_STATUS_CODE = 401;// token 工具函数/*** 获取token* @param key token的key* @returns {string}accessToken*/
export function getToken(key: string) {return localStorage.getItem(key);
}/*** 存储token到本地* @param key token的key* @param token token的值*/
export function setToken(key: string, token: string) {localStorage.setItem(key, token);
}// 拦截器/*** 请求拦截器:只要本地有access token,所有请求的请求头就携带上它。(对于没有采用单点登录方案的系统,access token就是普通 token)* @param {InternalAxiosRequestConfig}config* @returns {InternalAxiosRequestConfig}config*/
export function setAccessTokenRequestInterceptor(config: InternalAxiosRequestConfig) {if (config.headers) config.headers.authorization = `Bearer ${getToken("accessToken")}`;return config;
}/*** 响应拦截器:只要服务器响应了 token,就保存下来到本地,无论是 access token 还是 refresh token。* 当接口因权限拒绝或者本地计算 access token 过期,则就去拿 refresh token 无感刷新 access token。* @param {AxiosResponse}res* @returns {AxiosResponse}res*/
export async function getTokenResponseInterceptor(res: AxiosResponse) {// 假设服务器将token放在响应头中返回// 保存授权 token,也就是 access token 或者普通的 tokenif (res.headers.authorization) {const token = res.headers.authorization.repalce("Bearer ", "");setToken(ACCESS_TOKEN_KEY, token);}// 保存 refresh tokenif (res.headers.refreshtoken) {const refreshToken = res.headers.refreshtoken.repalce("Bearer ", "");setToken(REFRESH_TOKEN_KEY, refreshToken);}// 请求业务接口没有权限,说明 access token 过期,需要刷新 tokenif (res.data.code === UNAUTHORIZED_STATUS_CODE) {// 请求服务器获取最新access token,当前拦截器递归保存tokenconst isRefreshSuccess = await refreshAccessToken();if (isRefreshSuccess) {// 刷新 access token 成功,装配新 access token 后拿到 axios 实例重新发起请求res.config.headers.Authorization = `Bearer ${getToken(ACCESS_TOKEN_KEY)}`;const response = await httpRequest.getInstance().request(res.config);return response;} else {// 刷新失败,refresh token 过期,跳转登录页面重新登录window.location.hash = "/login";// window.location.href = "/login";}}return res;
}

src\api\modules\refreshToken.ts

import httpRequest from "..";
import { REFRESH_TOKEN_KEY, UNAUTHORIZED_STATUS_CODE, getToken } from "../http/token";const REFRESH_TOKEN_API = "/refreshToken";/*** 获取 refresh token 的接口* 这个接口不同于业务接口,它携带的 token 是 refresh token 而不是 access token。* 当请求 access token 回来后,就会启动响应拦截将 access token 保存* 返回一个布尔值,用于判断是否刷新成功,因为 refresh token 也会过期,导致刷新失败。* @returns {boolean} isRefreshSuccess 刷新 access token 是否成功。*/
export async function refreshAccessToken() {const res = await httpRequest.get({url: REFRESH_TOKEN_API,headers: {Authorization: `Bearer ${getToken(REFRESH_TOKEN_KEY)}`}});// 响应状态码不为 401,则表示刷新成功return res.code !== UNAUTHORIZED_STATUS_CODE;
}

需解决的问题

请求进入死循环

假如 refresh token 也过期了,那么携带 refresh token 去刷新 access token时就会被拒绝,refreshAccessToken 请求失败,状态码 401。这时因为是 401,响应拦截器中就又会以为是 access token 过期,又拿着 refresh token 去刷新。至此陷入死循环了。

核心就是当前是否启动无感刷新 access token 的判断条件,需要区分是 access token 过期导致的业务接口拒绝,还是 refresh token 过期导致的授权接口拒绝。

解决办法可以是服务器接口给出过期时间,或者前端自己解析 jwt,拿到过期时间。然后通过判断 access token 过期时间来选择是否要去刷新。

假如通过接口返回 401 来判断。这时可以引入一个变量做标志,表明当前请求是否是刷新 access token 的请求,还是业务请求。
src\api\modules\refreshToken.ts

export async function refreshAccessToken() {const res = await httpRequest.get({url: REFRESH_TOKEN_API,headers: {Authorization: `Bearer ${getToken(REFRESH_TOKEN_KEY)}`,_isRefreshAccessTokenRequest: true // 标记当前请求为刷新 token 请求}});return res.code !== UNAUTHORIZED_STATUS_CODE;
}

src\api\http\token.ts

// 请求业务接口没有权限,说明 access token 过期,需要刷新 token
if (res.data.code === UNAUTHORIZED_STATUS_CODE && !res.config.headers._isRefreshAccessTokenRequest) {// 请求服务器获取最新access token,当前拦截器递归保存tokenconst isRefreshSuccess = await refreshAccessToken();if (isRefreshSuccess) {// 刷新 access token 成功,装配新 access token 后拿到 axios 实例重新发起请求res.config.headers.Authorization = `Bearer ${getToken(ACCESS_TOKEN_KEY)}`;const response = await httpRequest.getInstance().request(res.config);return response;} else {// 刷新失败,refresh token 过期,跳转登录页面重新登录window.location.hash = "/login";// window.location.href = "/login";}
}

标记刷新 token 请求避免请求拦截覆盖 refresh token

刷新 access token 的请求也是一个请求,它携带的是 refresh token。但是之前我们设置了全局的请求拦截器。又因为无论是 access token 还是 refresh token 都是放在请求头的 authorization 上携带,此时请求拦截设置的 access token 就会覆盖掉 refresh token,导致刷新接口拿不到 refresh token。
因此请求拦截器也要对刷新 token 的请求做额外的区分,过滤掉刷新请求。也可以通过请求头的标记实现。

export function setAccessTokenRequestInterceptor(config: InternalAxiosRequestConfig) {if (config.headers && !config.headers._isRefreshAccessTokenRequest) {config.headers.authorization = `Bearer ${getToken(ACCESS_TOKEN_KEY)}`;}return config;
}

并发刷新 token

如果当前有很多业务请求,然后 access token 刚好过期了。那这些业务请求的响应拦截器中都会拿着 refresh token 去请求刷新接口刷新 access token。前一个刷新请求还没拿到最新的 access token,后一个刷新请求又发出了,这就出现了并发刷新 token ,冗余发送请求的情况。

解决这个问题的核心,无非就是确定上一个刷新 token 的请求是否结束,它没结束后续的刷新请求就得等着。这种观测异步处理的状态,promise 干的就是这个。

定义一个全局的变量,用这个全局的变量保存刷新 token 的请求,也就是保存一个 promise。
变量初始是空的,因为没有刷新请求。当有刷新请求发起,就生成一个 promise 观测该请求,并将该 promise 保存在全局变量中。此时后续想要再次发起刷新请求,就直接返回这个“全局的请求”(promise)给它们,避免了发起冗余请求。并且这样当第一个刷新请求得到结果,后续所有请求就都拿到了结果,因为都是同一个 promise。
promise 有结果后,无论刷新成功与否,都代表了本轮并发刷新 token 请求的结束,需将全局变量重置为 null,以准备下一次并发刷新请求。

import httpRequest from "..";
import { REFRESH_TOKEN_KEY, UNAUTHORIZED_STATUS_CODE, getToken } from "../http/token";const REFRESH_TOKEN_API = "/refreshtoken";let promise: Promise<any> | null = null;/*** 获取 refresh token 的接口* 这个接口不同于业务接口,它携带的 token 是 refresh token 而不是 access token。* 当请求 access token 回来后,就会启动响应拦截将 access token 保存* 返回一个 Promise 布尔值,用于判断是否刷新成功,因为 refresh token 也会过期,导致刷新失败。* @returns {Promise<boolean>} isRefreshSuccess 刷新 access token 是否成功。*/
export function refreshAccessToken() {// 前面已经发送了刷新请求,promise 有值,此时后续请求全都返回最开始的 promiseif (promise) {return promise;}promise = new Promise((resolve, rejects) => {httpRequest.get({url: REFRESH_TOKEN_API,headers: {Authorization: `Bearer ${getToken(REFRESH_TOKEN_KEY)}`,_isRefreshAccessTokenRequest: true}}).then(res => {resolve(res.code !== UNAUTHORIZED_STATUS_CODE);}).catch(() => rejects(false)).finally(() => {promise = null; // 本次并发刷新请求结束,重置变量为 null});});return promise;
}

完整代码

src\api\http\token.ts

import { AxiosResponse, InternalAxiosRequestConfig } from "axios";
import httpRequest from "..";
import { refreshAccessToken } from "../modules/refreshToken";export const ACCESS_TOKEN_KEY = "access_token";
export const REFRESH_TOKEN_KEY = "refresh_token";
export const UNAUTHORIZED_STATUS_CODE = 401;// token 工具函数/*** 获取token* @param key token的key* @returns {string}accessToken*/
export function getToken(key: string) {return localStorage.getItem(key);
}/*** 存储token到本地* @param key token的key* @param token token的值*/
export function setToken(key: string, token: string) {localStorage.setItem(key, token);
}// 拦截器/*** 请求拦截器:只要本地有access token,所有请求的请求头就携带上它。(对于没有采用单点登录方案的系统,access token就是普通 token)* @param {InternalAxiosRequestConfig}config* @returns {InternalAxiosRequestConfig}config*/
export function setAccessTokenRequestInterceptor(config: InternalAxiosRequestConfig) {if (config.headers && !config.headers._isRefreshAccessTokenRequest) {config.headers.authorization = `Bearer ${getToken(ACCESS_TOKEN_KEY)}`;}return config;
}/*** 响应拦截器:只要服务器响应了 token,就保存下来到本地,无论是 access token 还是 refresh token。* 当接口因权限拒绝或者本地计算 access token 过期,则就去拿 refresh token 无感刷新 access token。* @param {AxiosResponse}res* @returns {AxiosResponse}res*/
export async function getTokenResponseInterceptor(res: AxiosResponse) {// 假设服务器将token放在响应体中返回// 保存授权 token,也就是 access token 或者普通的 tokenif (res.data.data?.accessToken) {const token = res.data.data.accessToken.replace("Bearer ", "");setToken(ACCESS_TOKEN_KEY, token);}// 保存 refresh tokenif (res.data.data?.refreshToken) {const refreshToken = res.data.data.refreshToken.replace("Bearer ", "");setToken(REFRESH_TOKEN_KEY, refreshToken);}// 请求业务接口没有权限,说明 access token 过期,需要刷新 tokenif (res.data.code === UNAUTHORIZED_STATUS_CODE && !res.config.headers._isRefreshAccessTokenRequest) {// 请求服务器获取最新access token,当前拦截器递归保存tokenconst isRefreshSuccess = await refreshAccessToken();if (isRefreshSuccess) {// 刷新 access token 成功,装配新 access token 后拿到 axios 实例重新发起请求res.config.headers.Authorization = `Bearer ${getToken(ACCESS_TOKEN_KEY)}`;const response = await httpRequest.getInstance().request(res.config);return response;} else {// 刷新失败,refresh token 过期,跳转登录页面重新登录window.location.hash = "/login";// window.location.href = "/login";}}return res;
}

src\api\modules\refreshToken.ts

import httpRequest from "..";
import { REFRESH_TOKEN_KEY, UNAUTHORIZED_STATUS_CODE, getToken } from "../http/token";const REFRESH_TOKEN_API = "/refreshtoken";let promise: Promise<any> | null = null;/*** 获取 refresh token 的接口* 这个接口不同于业务接口,它携带的 token 是 refresh token 而不是 access token。* 当请求 access token 回来后,就会启动响应拦截将 access token 保存* 返回一个 Promise 布尔值,用于判断是否刷新成功,因为 refresh token 也会过期,导致刷新失败。* @returns {Promise<boolean>} isRefreshSuccess 刷新 access token 是否成功。*/
export function refreshAccessToken() {// 前面已经发送了刷新请求,promise 有值,此时后续请求全都返回最开始的 promiseif (promise) {return promise;}promise = new Promise((resolve, rejects) => {httpRequest.get({url: REFRESH_TOKEN_API,headers: {Authorization: `Bearer ${getToken(REFRESH_TOKEN_KEY)}`,_isRefreshAccessTokenRequest: true}}).then(res => {resolve(res.code !== UNAUTHORIZED_STATUS_CODE);}).catch(() => rejects(false)).finally(() => {promise = null; // 本次并发刷新请求结束,重置变量为 null});});return promise;
}

测试代码

<template><div><h2>测试无感刷新 token</h2><el-button type="primary" round @click="handleClickLogin">登录</el-button><el-button type="primary" round @click="handleClickGetProtectData">请求受保护资源</el-button></div>
</template><script setup lang="ts">import { login } from "@/api/modules/login";import { fetchUsersList } from "@/api/modules/user";const handleClickLogin = () => {login({ username: "admin" }).then(res => {console.log(res);});};const handleClickGetProtectData = async () => {const res = await fetchUsersList();console.log("res", res);};
</script><style scoped></style>

注意:拦截器注册顺序

注册无感刷新的响应拦截器要在防抖拦截器的后面。
如这样:

// debounceRequest
httpRequest.getInstance().interceptors.request.use(compareUrl);
httpRequest.getInstance().interceptors.response.use(filterFulfilledUrl);// token
httpRequest.getInstance().interceptors.request.use(setAccessTokenRequestInterceptor);
httpRequest.getInstance().interceptors.response.use(getTokenResponseInterceptor);

请求防抖是通过比较请求 url 来实现的。在请求拦截器中保存当前请求的url到数组中,后续的请求都需要判断一下,当前请求的url是否已经在数组中。当响应拦截器启动,说明请求完毕,就从数组中清除此url。

此时问题就来了,假如 token 的响应拦截定义在防抖响应拦截器的前面。(axios响应拦截,越晚定义越晚执行)
当 access token 过期,token 响应拦截器中会去刷新 token,并对业务接口重新发起请求。注意,此时仍然处于上一次被拒绝请求的拦截器中。那后续的防抖响应拦截器肯定还没执行,也就是还没有清除数组中被拒绝请求的url。此时又重新发送了请求,防抖的请求拦截中就会发挥防抖功能抛出错误“请求频繁"。
因此防抖响应拦截器和无感刷新 token 的响应拦截器有注册顺序,token 拦截要后注册。

另一种方案:事件驱动刷新

这种方式以某个页面事件触发刷新,而不是在拦截器中判断所有请求的结果。

用户登陆返回accesToken,refreshToken 还有accesToken有效时间戳。
每次加载到home页面,直接判断accesToken是否过期,过期了直接用refreshToken请求刷新accesToken接口,返回新的accesToken,新的refreshToken,accesToken的有效时间戳。

accesToken的有效时间戳并不是accesToken真正失效时间,一般会比真的失效时间点会提前的。

至于其他request和这套机制完全独立的。只是每次请求带上accessToken罢了。不用考虑accesToken过期啥的。

相关文章:

无感刷新 token

文章目录 背景基本思路需解决的问题请求进入死循环标记刷新 token 请求避免请求拦截覆盖 refresh token并发刷新 token 完整代码注意&#xff1a;拦截器注册顺序另一种方案&#xff1a;事件驱动刷新 前景提要&#xff1a; ts 简易封装 axios&#xff0c;统一 API 实现在 confi…...

【MISRA C 2012】Rule 2.6 函数不应该包含未使用的标签声明

1. 规则1.1 原文1.2 分类 2. 关键描述3. 代码实例 1. 规则 1.1 原文 Rule 2.6 A function should not contain unused label declarations Category Advisory Analysis Decidable, Single Translation Unit Applies to C90, C99 1.2 分类 规则2.6&#xff1a;函数不应该包含…...

Ubuntu:使用apache2部署Vue开发的网站

作者:CSDN @ _乐多_ 本文记录了Vue项目打包到部署到ubuntu系统的全部步骤。 文章目录 一、代码打包二、安装 Apache2三、开启/关闭apache23.1 开启3.2 关闭四、部署Vue应用到Apache24.1 首次部署4.2 更新部署五、全部操作截图一、代码打包 首先,确保您已经在本地开发环境中…...

使用IO完成端口实现简单回显服务器

说明 使用IO完成端口实现简单回显服务器&#xff0c;因为是测试用的&#xff0c;所以代码很粗糙。 提醒 使用的是ReadFile、WriteFile来实现Overlapped IO&#xff0c;正式场合应该用WSARecv、WSASend&#xff0c;原因&#xff1a;来自《Windows网络编程技术》 8.2.5节 在这里…...

【ROS】Nav2源码之nav2_behavior_tree详解

1、简介 nav2_bt_navigator实现ROS2节点以行为树的方式来处理。 nav2_behavior_tree模块提供如下功能: 一个c模板类&#xff0c;可以轻松地将ROS2 动作(actions)和服务(services)集成到行为树(Behavior Trees)中。特定于导航的行为树节点。通用的BehaviorTreeEngine类&#…...

SpringBoot---myBatis数据库操作

一&#xff0c;分页查询 现在controller中设置参数&#xff0c;RequestParam(defaultValue "1") 设置默认值 GetMapping public Result page(RequestParam(defaultValue "1") Integer page,RequestParam(defaultValue "10") Integer pageSize…...

力扣541.反转字符串II

原题链接&#xff1a;力扣541.反转字符串II 思路&#xff1a; 其实在遍历字符串的过程中&#xff0c;只要让 i (2 * k)&#xff0c;i 每次移动 2 * k 就可以了&#xff0c;然后判断是否需要有反转的区间。 因为要找的也就是每2 * k 区间的起点&#xff0c;这样写&#xff0c…...

撕掉Hadoop标签,Cloudera未来可期吗?

Cloudera&#xff1a;大数据的弄潮儿 1、Cloudera发展史2、透过Cloudera看清大数据时代的转变3、参考文章 1、Cloudera发展史 说起Cloudera&#xff0c;就不得不提起Hadoop&#xff0c;Cloudera的过去就是Hadoop时代中的一个缩影。作为全球第一家也是最大一家Hadoop商业化公司&…...

排序算法(1)

这里写目录标题 排序插入排序直接插入排序希尔排序 选择排序直接选择排序堆排序向下调整堆排序 交换排序冒泡排序 排序 插入排序 直接插入排序 直接插入排序是O&#xff08;N^2&#xff09;的排序算法 从0下标开始往后排 void InsertSort(int* a,int n)//直接插入排序 {fo…...

Top 5 Cutting-edge technology examples 2023

文章目录 Top 5 Cutting-edge technology examples 20231、Computer Vision2、Natural Language Processing3、Virtual Reality & Augmented Reality4、Deep Machine Learning5、Neuralink Top 5 Cutting-edge technology examples 2023 Cutting-edge technology in 2023 …...

【算法|滑动窗口No.3】leetcode3. 无重复字符的最长子串

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】【LeetCode】 &#x1f354;本专栏旨在提高自己算法能力的同时&#xff0c;记录一下自己的学习过程&#xff0c;希望…...

元素的水平居中和垂直几种方案

总结一下各种元素的水平居中和垂直居中方案。 水平居中&#xff1a; 1.行内元素水平居中 text-align: center 定义行内内容&#xff08;例如文字&#xff09;如何相对它的块父元素对齐;不仅可以让文字水平居中&#xff0c;还可以让行内元素水平居中 注意&#xff1a;给行内…...

JS和JQuery的区别

JS和jQuery都是用于前端开发的工具&#xff0c;但是它们有一些重要的区别。主要区别如下&#xff1a; JS是一种编程语言&#xff0c;而jQuery是一个JS库。JS可以与其他语言一起使用&#xff08;如PHP、Python等&#xff09;&#xff0c;而jQuery是JS的一个扩展&#xff0c;只能…...

延时摄影视频制作工具 LRTimelapse mac中文版特点介绍

lrTimelapse mac是一款适用于 Windows 和 macOS 系统的延时摄影视频制作软件&#xff0c;可以帮助用户创建高质量的延时摄影视频。该软件提供了直观的界面和丰富的功能&#xff0c;支持多种时间轴摄影工具和文件格式&#xff0c;并具有高度的可定制性和扩展性。 lrTimelapse ma…...

Mac电脑怎么运行 Office 办公软件

虽然 Office 软件也有 Mac 版本的&#xff0c;但是有蛮多小伙伴用起来还是感觉不得劲&#xff0c;毕竟接触了太久的 Windows&#xff0c;所以想要使用 Windows 版本的 Office 软件。 今天就给大家介绍一下怎么在 Mac 电脑中运行 Windows 版本的办公软件&#xff0c;在这里就需…...

FPGA 如何 固化程序到 FLASH中

1、导出Hardware 2、导出bit文件 3、打开SDK 4、 点击Ok 5、创建工程 6、 输入工程名称&#xff1a;guhua 7、选择 Zynq FSBL 8、单击 guhua、然后点击 build 点击&#xff1a;build all 9、 右键之后&#xff0c;点击&#xff1a;Creat Boot Image 10、点击 Cr…...

电源管理(PMIC)MAX20428ATIA/VY、MAX20428ATIC/VY、MAX20428ATIE/VY适合汽车ADAS应用的开关稳压器

一、概述 MAX20428是一款高效率、八路输出、低压PMIC。OUT1将输入电源升压至5V&#xff0c;电流高达500mA&#xff0c;而三个同步降压转换器的输入电压范围为3.0V至4.2V&#xff0c;输出电压范围为0.8V至3.9875V&#xff0c;峰值电流分别高达1.3A、1.3A和3.5A。三个300mA pMOS…...

十年JAVA搬砖路——Linux搭建Ldap服务器。

1.安装命令 yum -y install openldap compat-openldap openldap-clients openldap-servers openldap-servers-sql openldap-devel2.启动ldap systemctl start slapd systemctl enable slapd3.修改密码 slappasswd Aa123456获得返回的密码加密密码串&#xff1a; {SSHA}DkSw0…...

论文 辅助笔记:t2vec train.py

1 train 1.1 加载training和validation数据 def train(args):logging.basicConfig(filenameos.path.join(args.data, "training.log"), levellogging.INFO)设置了日志的基本配置。将日志信息保存到名为 "training.log" 的文件中日志的级别被设置为 INFO&…...

同时标注分割、检测、多分类属性的工具

1、 https://blog.csdn.net/minstyrain/article/details/82385580/ 2、 https://zhuanlan.zhihu.com/p/656703406...

8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂

蛋白质结合剂&#xff08;如抗体、抑制肽&#xff09;在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上&#xff0c;高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术&#xff0c;但这类方法普遍面临资源消耗巨大、研发周期冗长…...

使用分级同态加密防御梯度泄漏

抽象 联邦学习 &#xff08;FL&#xff09; 支持跨分布式客户端进行协作模型训练&#xff0c;而无需共享原始数据&#xff0c;这使其成为在互联和自动驾驶汽车 &#xff08;CAV&#xff09; 等领域保护隐私的机器学习的一种很有前途的方法。然而&#xff0c;最近的研究表明&…...

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...

c++ 面试题(1)-----深度优先搜索(DFS)实现

操作系统&#xff1a;ubuntu22.04 IDE:Visual Studio Code 编程语言&#xff1a;C11 题目描述 地上有一个 m 行 n 列的方格&#xff0c;从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子&#xff0c;但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放

简介 前面两期文章我们介绍了I2S的读取和写入&#xff0c;一个是通过INMP441麦克风模块采集音频&#xff0c;一个是通过PCM5102A模块播放音频&#xff0c;那如果我们将两者结合起来&#xff0c;将麦克风采集到的音频通过PCM5102A播放&#xff0c;是不是就可以做一个扩音器了呢…...

AspectJ 在 Android 中的完整使用指南

一、环境配置&#xff08;Gradle 7.0 适配&#xff09; 1. 项目级 build.gradle // 注意&#xff1a;沪江插件已停更&#xff0c;推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...

Linux --进程控制

本文从以下五个方面来初步认识进程控制&#xff1a; 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程&#xff0c;创建出来的进程就是子进程&#xff0c;原来的进程为父进程。…...

关键领域软件测试的突围之路:如何破解安全与效率的平衡难题

在数字化浪潮席卷全球的今天&#xff0c;软件系统已成为国家关键领域的核心战斗力。不同于普通商业软件&#xff0c;这些承载着国家安全使命的软件系统面临着前所未有的质量挑战——如何在确保绝对安全的前提下&#xff0c;实现高效测试与快速迭代&#xff1f;这一命题正考验着…...

智能AI电话机器人系统的识别能力现状与发展水平

一、引言 随着人工智能技术的飞速发展&#xff0c;AI电话机器人系统已经从简单的自动应答工具演变为具备复杂交互能力的智能助手。这类系统结合了语音识别、自然语言处理、情感计算和机器学习等多项前沿技术&#xff0c;在客户服务、营销推广、信息查询等领域发挥着越来越重要…...

GitFlow 工作模式(详解)

今天再学项目的过程中遇到使用gitflow模式管理代码&#xff0c;因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存&#xff0c;无论是github还是gittee&#xff0c;都是一种基于git去保存代码的形式&#xff0c;这样保存代码…...