使用Spring AI中的RAG技术,实现私有业务领域的大模型系统

news2024/11/14 20:05:48

前言

在上一篇文章《使用SpringAI快速实现离线/本地大模型应用》中,记录了如何使用SpringAI来调用我们的本地大模型,如何快速搭建一个本地大模型系统,并演示本地大模型的智能对话、图片理解、文生图等功能。

但在前文中,我们把SpringAI只是当作了一个大模型的搬运工,它并没有和我们的具体业务系统联系起来。
也就是,它并没有解决前文中提到的以下问题:“然而在许多领域,由于大模型的数据没有采集到更细化的信息,亦或者出于安全原因某些数据不能对外公开,这时使用离线大模型来实现信息的生成与检索则变得非常重要。”

在本文中,将介绍如何使用Spring AI中的RAG技术,让大模型(LLM)为我们的私有业务系统体现出具体价值。

目标:我们的大模型=(公有大模型+我们的私有业务系统知识)

RAG介绍

RAG,即Retrieval-augmented Generation,检索增强生成。它是大模型信息检索技术的结合技术。

百度百科上的解释为:当模型需要生成文本或者回答问题时,它会先从一个庞大的文档集合中检索出相关的信息,然后利用这些检索到的信息来指导文本的生成,从而提高预测的质量和准确性。
按成功人士的话来讲,则为:“让大模型为行业赋能”

RAG的工作流程可参考SpringAI中的这张图:
RAG
即:在用户向大模型检索某个信息时,会携带关于所需要检索的背景知识,这样以让大模型可以参考已有知识来进行应答。

SpringAI中实现RAG

spring-ai-rag

通过前面的介绍,我们已经对RAG有了一定的了解,通过它可以让大模型更加智能。RAG它没有具体的实现方式,在SpringAI中实现一个RAG我们一般遵循SpringAI的规范实现它就行。

此部分可参考:SpringAI-AI/Concepts/Retrieval Augmented Generation

The approach involves a batch processing style programming model, where the job reads unstructured data from your documents, transforms it, and then writes it into a vector database. At a high level, this is an ETL (Extract, Transform and Load) pipeline. The vector database is used in the retrieval part of RAG technique.

如上所述,RAG的实现流程更像是一个ETL工作流,并运用向量数据库实现对业务数据的存储与检索。下面用具体例子来说明。

ETL Pipeline(数据预加载)

etl-pipeline
ETL——Extract, Transform, and Load,数据的提取、转换、加载流。

在SpringAI中,它对ETL的具体实现进行了一系列的封装,并对E、T、L中的每个阶段依次提供了接口类,分别为DocumentReader、DocumentTransformer、DocumentWriter。同时还提供了相关的依赖包,我们实现时根据需要按需导入。

下面以加载项目中一个名为1.pdf的PDF文档为例,看一下SpringAI中实现ETL的具体代码:

@Component
public class RagWorker {

    private final VectorStore vectorStore;
    private final Resource pdfResource;

    public RagWorker(VectorStore vectorStore,
                     @Value("classpath:/rag/1.pdf") Resource pdfResource) {
        this.vectorStore = vectorStore;
        this.pdfResource = pdfResource;
    }

    @PostConstruct
    public void init() {
        //Extract
        TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(pdfResource);
        TokenTextSplitter tokenTextSplitter = new TokenTextSplitter();
        //Transform, and Load
        vectorStore.add(tokenTextSplitter.apply(tikaDocumentReader.get()));
    }
}

以上代码中实现了从对PDF文档的读取,到TOKEN的切分与转换为向量,再到最终存入向量数据库的全部过程。

其中的TokenTextSplitter为Transformer的一个具体实现,类图如下:

textSplitter

其中涉及到的依赖为:

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-tika-document-reader</artifactId>
        </dependency>

向量数据库与embedding模型

SpringAI中提供了VectorStore接口类,它代表各向量数据库的抽象接口。SimpleVectorStore为SpringAI所提供的内存map向量数据库实现,仅在数据量极少和代码验证时适用。

vectorStore.add方法会调用嵌入模型的embed方法,将数据转换为向量。
代码片段如下:

	@Override
	public void add(List<Document> documents) {

		VectorStoreObservationContext observationContext = this
			.createObservationContextBuilder(VectorStoreObservationContext.Operation.ADD.value())
			.build();

		VectorStoreObservationDocumentation.AI_VECTOR_STORE
			.observation(this.customObservationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
					this.observationRegistry)
			.observe(() -> this.doAdd(documents));
	}

	@Override
	public void doAdd(List<Document> documents) {
		for (Document document : documents) {
			logger.info("Calling EmbeddingModel for document id = {}", document.getId());
			float[] embedding = this.embeddingModel.embed(document);
			document.setEmbedding(embedding);
			this.store.put(document.getId(), document);
		}
	}

SpringAI中的默认ollama嵌入模型为mxbai-embed-large,使用前记得先进行拉取

ollama pull mxbai-embed-large

如需要修改SpringAI中调用的ollama嵌入模型,可通过项目的配置文件进行修改,application.properties

spring.ai.ollama.embedding.model=rjmalagon/gte-qwen2-1.5b-instruct-embed-f16:latest

如这里修改为了中文支持度更好的qwen嵌入模型

ollama pull rjmalagon/gte-qwen2-1.5b-instruct-embed-f16

写段JAVA代码试下从向量数据库中查询某个关键字

    @GetMapping("/ai/vectorStoreSearch")
    public Map<String, String> getSystem(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        List<Document> similarDocuments = this.vectorStore.similaritySearch(message);
        String documents = similarDocuments.stream().map(Document::getContent)
                .collect(Collectors.joining(System.lineSeparator()));
        return Map.of("result", documents);
    }

调用试一下:

http://127.0.0.1:8080/ai/vectorStoreSearch?message=%E8%92%B2%E6%B5%B7%E6%B4%8B

vectorStore

其中\r\n为代码中的System.lineSeparator(),它默认会返回与被搜索关键字最相似的4段文本

RAG实现示例

如上所述,在SpringAI项目中实现一个完整的RAG功能需要包含完整的ETL流程,并携带prompt的上下文向大模型提交问题,这些步骤看似冗长但好在有SpingAI的完美封装,实现起来非常简单。

在项目中新建一个字符串模板文件,如service.st,内容如下:

你是一个信息查询助手,你的名字为"haiyangAI"。您的回复将简短明了,但仍然具有信息量和帮助性。

今天日期是{current_date}。

参考信息:
{documents}

对话时的主要代码如下:

    private static void submitPromptListener(String chatId, MessageInput.SubmitEvent e, ChatService chatService, VerticalLayout messageList) {
        var question = e.getValue();
        var userMessage = new MarkdownMessage(question, "You", Color.AVATAR_PRESETS[6]);
        var assistantMessage = new MarkdownMessage("haiyangAI", Color.AVATAR_PRESETS[0]);
        messageList.add(userMessage, assistantMessage);
        chatService.chat(chatId, question)
                .map(res -> Optional.ofNullable(res.getResult().getOutput().getContent()).orElse(""))
                .subscribe(assistantMessage::appendMarkdownAsync);
    }

    public Flux<ChatResponse> chat(String chatId, String message) {
        Message systemMessage = promptManagementService.getSystemMessage(chatId, message);
        UserMessage userMessage = new UserMessage(message);
        promptManagementService.addMessage(chatId, userMessage);
        logger.debug("Chatting with chatId: {} and message: {}", chatId, message);
        Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
        logger.info("prompt: {}", prompt);
        return chatModel.stream(prompt);
    }

    public PromptManagementService(@Value("classpath:/rag/service.st") Resource systemPrompt, VectorStore vectorStore) {
        this.systemPrompt = systemPrompt;
        this.vectorStore = vectorStore;
        this.messageAggregations = new ConcurrentHashMap<>();
    }

    public Message getSystemMessage(String chatId, String message) {
        // Retrieve related documents to query
        List<Document> similarDocuments = this.vectorStore.similaritySearch(SearchRequest.query(message).withTopK(2));
        String documents = similarDocuments.stream().map(Document::getContent)
                .collect(Collectors.joining(System.lineSeparator()));

        Map<String, Object> prepareHistory = Map.of(
                "documents", documents,
                "current_date", java.time.LocalDate.now()
        );
        return new SystemPromptTemplate(this.systemPrompt).createMessage(prepareHistory);
    }

其主要流程为:

  1. 根据用户的输入prompt从向量数据库(vectorStore)中查询出与prompt最接近的本地文本,topK(2)代表返回2条
  2. 拼接classpath:/rag/service.st本地向量数据库返回的topK文档到模板文件中,形成SystemPrompt
  3. 将SystemPrompt和用户输入的UserMessage作为消息列表传递给大模型,以让大模型根据SystemPrompt中的内容为背景知识来应答用户的输入

之后再用测试界面试一下效果,提问:蒲海洋在2022年7月申请了什么专利?
rag-demo

自定义的AI根据上下文背景准确进行了回复,其参考的信息来源于1.pdf文档
1pdf

上面代码写得有点长,也可以使用SpringAI提供的Advisors API进一步简化,写个http示例接口:

    @GetMapping(value = "/ai/generateStream2", produces = MediaType.TEXT_EVENT_STREAM_VALUE + ";charset=UTF-8")
    public Flux<ServerSentEvent<String>> generateStream2(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        String promptContext = """
                你是一个信息查询助手,你的名字为"haiyangAI"。您的回复将简短明了,但仍然具有信息量和帮助性。
                今天日期是{current_date}。
                参考信息:{question_answer_context}
                """;
        return ChatClient.create(chatModel)
                .prompt()
                .user(message)
                .advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults().withTopK(2), promptContext))
                .user(b -> b.param("current_date", LocalDate.now().toString()))
                .stream()
                .content()
                .map(resp -> ServerSentEvent.builder(resp)
                        .event("generation").build());
    }

其中的question_answer_context占位符为QuestionAnswerAdvisor查询向量数据库自动填充的内容。
可见源码org.springframework.ai.chat.client.advisor#before方法:

    private AdvisedRequest before(AdvisedRequest request) {
        HashMap<String, Object> context = new HashMap(request.adviseContext());
        String var10000 = request.userText();
        String advisedUserText = var10000 + System.lineSeparator() + this.userTextAdvise;
        String query = (new PromptTemplate(request.userText(), request.userParams())).render();
        SearchRequest searchRequestToUse = SearchRequest.from(this.searchRequest).withQuery(query).withFilterExpression(this.doGetFilterExpression(context));
        List<Document> documents = this.vectorStore.similaritySearch(searchRequestToUse);
        context.put("qa_retrieved_documents", documents);
        String documentContext = (String)documents.stream().map(Content::getContent).collect(Collectors.joining(System.lineSeparator()));
        Map<String, Object> advisedUserParams = new HashMap(request.userParams());
        advisedUserParams.put("question_answer_context", documentContext);
        AdvisedRequest advisedRequest = AdvisedRequest.from(request).withUserText(advisedUserText).withUserParams(advisedUserParams).withAdviseContext(context).build();
        return advisedRequest;
    }

验证一下:
http://127.0.0.1:8080/ai/generateStream2?message=蒲海洋在2022年7月申请了什么专利
api-rag

可以看出应答效果与前面手动代码效果一样,即:QuestionAnswerAdvisor可以简化手动查库填充上下文的代码,强得可怕。

Function Calling实时查询

SpringAI中除了可以使用向量数据库进行上下文知识提供外,还提供了另外一种与RAG类似的功能——Function Calling API

Function Calling工作流程

functionCalling

Function Calling的工作流程如上图,它的工作流程可以理解为是一个回调

举个问天气预报的例子。
问题:“今天成都天气怎么样?”
其中的 “今天”“成都” 分别代表两个变量:时间地点
SpringAI框架中要实现这类部分变量的特定问题,则可以使用function calling来实现。

具体工作流程如下:

  1. 首先定义一个description,设置问题描述(“获取某个地方某天的天气”),并绑定一个回调方法用来查询某个地方的某天的天气信息
  2. 用户的输入被提交到大模型后,大模型判断用户的输入是否符合是否符合descprition所描述的问题,
    如满足,则由大模型提取出关键参数:“今天”“成都”,并调用这个description的回调函数
  3. 由本地的业务系统得到今天成都这两个实参,去查询本地业务系统中的天气信息,并将对应的天气信息再次提交给大模型
  4. 大模型再次收到业务系统关于 “成都今天的天气信息” 后,组织语言返回最终的分析结果

Function Calling快速实现

这里以ollama本地大模型(参考ollama-chat-functions),写段代码理解下。

假设要实现的行业为 “AI+地铁”,要做一个地铁AI问答系统,以实现某站点线路查询的场景,解决类似:“天府三街有哪些地铁线路?” 这类的问题。

写个工具类,用来处理地铁站问题的回调,示例代码:

@Configuration
public class SubwayTools {
    private static final Logger logger = LoggerFactory.getLogger(SubwayTools.class);

    public record SubwayStationDetailsRequest(String stationName) {
    }

    @JsonInclude(JsonInclude.Include.NON_NULL)
    public record SubwayStationDetails(String stationName,
                                       Integer totalSubwayLine,
                                       List<String> subwayLineNameList) {
    }

    @Bean
    @Description("获取所在位置的地铁线路")
    public Function<SubwayStationDetailsRequest, SubwayStationDetails> getSubwayStationDetails() {
        return request -> {
            try {
                return findSubwayStationDetails(request.stationName());
            } catch (Exception e) {
                logger.warn("Booking details: {}", NestedExceptionUtils.getMostSpecificCause(e).getMessage());
                return new SubwayStationDetails(request.stationName(), 0, null);
            }
        };
    }

    /**
     * 获取某个地点的地铁线路
     *
     * @param stationName stationName
     */
    private SubwayStationDetails findSubwayStationDetails(String stationName) {
        //mock数据
        if (stationName.contains("天府三街")) {
            return new SubwayStationDetails(stationName, 1, List.of("地铁1号线"));
        } else if (stationName.contains("三岔")) {
            return new SubwayStationDetails(stationName, 2, List.of("地铁18号线", "地铁19号线"));
        } else if (stationName.contains("西博城")) {
            return new SubwayStationDetails(stationName, 3, List.of("地铁1号线", "地铁6号线", "地铁18号线"));
        } else if (stationName.contains("海洋公园")) {
            return new SubwayStationDetails(stationName, 3, List.of("地铁1号线", "地铁18号线", "地铁16号线"));
        } else {
            return null;
        }
    }
}

在上面的代码中使用Description注解的方式向Spring框架中注入了一个名为getSubwayStationDetailsfunction call,并定义了用来接收大模型解析参数的Request类SubwayStationDetailsRequest,用来回复大模型的Response(上下文)类SubwayStationDetails,并使用mock的方式模拟了几个地点的地铁线路。

有了上面@Configuration注解的自动装配后,我们就可以调用上面的Function了。写段代码验证下:

    public Flux<ChatResponse> chatWithCallback(String chatId, String message) {
        UserMessage userMessage = new UserMessage(message);
        ChatResponse response = chatModel.call(new Prompt(userMessage, OllamaOptions.builder().withFunction("getSubwayStationDetails").build()));
        return Flux.just(response);
    }

使用call调用时传入function的名称为getSubwayStationDetails,测试效果如下:
fc-demo

注意看,最后一个示例中我们问: “海洋公园有哪些地铁线路?”,它回答到: “海洋公园有3条地铁线路,分别是地铁1号线、地铁18号线和地铁16号线。”
虽然答案不符合实际,但它这样回答是正确的,因为在function函数中我们mock的数据就是这样的。

fc-note

另外,需要注意的是:如果使用ollama作为大模型,要使用function calling功能,需要使用支持Tools的模型。

如我这里使用的是国内的千问2.5

ollama run qwen2.5

spring.ai.ollama.chat.options.model=qwen2.5:latest

总结

使用SpringAI内置的RAGFunction Calling功能可以快速实现一个针对私有业务领域的本地大模型系统。

  • RAG主要涉及到ETL Pipeline、embedding model、向量数据库的知识点,处理过程有点像装饰器设计模式,在向大模型提交prompt前修饰原问题的上下文背景
  • Function Calling对于实时信息查询的场景比较适用,通过对大模型的2次调用业务系统api的反馈让大模型获取出上下文背景

相信通过前文《使用SpringAI快速实现离线/本地大模型应用》的内容,再结合本文中介绍的RAGFunction Calling技术,我们可以很方便地用SpringAI来实现一个面向自己的业务领域的本地(离线)大模型系统了。

本文源码:https://github.com/puhaiyang/springai-demo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2240379.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据分析-系统认识数据分析

目录 数据分析的全貌 观测 实验 应用 数据分析的全貌 观测 实验 应用

4. 查看并更新langgraph节点

导入必要的库和设置工具 首先&#xff0c;我们需要导入一些必要的库&#xff0c;并设置我们的工具。这些工具将用于在Spotify和Apple Music上播放歌曲。 from langchain_openai import ChatOpenAI from langchain_core.tools import tool from langgraph.graph import Messag…

使用Java绘制图片边框,解决微信小程序map组件中marker与label层级关系问题,label增加外边框后显示不能置与marker上面

今天上线的时候发现系统不同显示好像不一样&#xff0c;苹果手机打开的时候是正常的&#xff0c;但是一旦用安卓手机打开就会出现label不置顶的情况。尝试了很多种办法&#xff0c;也在官方查看了map相关的文档&#xff0c;发现并没有给label设置zIndex的属性&#xff0c;只看到…

【专题】计算机网络之网络层

1. 网络层的几个重要概念 1.1 网络层提供的两种服务 (1) 让网络负责可靠交付 计算机网络模仿电信网络&#xff0c;使用面向连接的通信方式。 通信之前先建立虚电路 VC (Virtual Circuit) (即连接)&#xff0c;以保证双方通信所需的一切网络资源。 如果再使用可靠传输的网络…

vTESTstudio系列15--vTESTstudio-Doors的需求和测试用例的管理

最近有朋友在咨询vTESTstudio中怎么去跟Doors里面的需求去做好管理这方面的问题&#xff0c;临时加两篇文章介绍一下,Lets Go!!! 目录 1.Doors的配置&#xff1a; 1.1 安装Doors AddIn for vTESTstudio&#xff1a; 1.2 更新XML脚本&#xff1a; 1.3 导出需求的Trace Item…

波动中的金钥匙:趋势震荡指标——源码公布,仅供学习

趋势与震荡&#xff0c;两者在市场运行中紧密相连&#xff0c;相互影响。趋势往往是震荡累积后的自然延伸&#xff0c;而震荡则常常是趋势形成与调整的前奏。在各类行情与不同时间周期中&#xff0c;当前的震荡不过是更大周期趋势中的一个组成部分&#xff1b;相应的&#xff0…

面试_ABtest原理简介

01 什么是ABtest ABtest来源于假设检验&#xff0c;现有两个随机均匀的有样本组A、B&#xff0c;对其中一个组A做出某种改动&#xff0c;实验结束后分析两组用户行为数据&#xff0c;通过显著性检验&#xff0c;判断这个改动对于我们所关注的核心指标是否有显著的影响&#xf…

‘nodemon‘ 不是内部或外部命令,也不是可运行的程序

解决方法&#xff1a;使用 npx 临时运行 nodemon 如果你不想全局安装 nodemon&#xff0c;你可以使用 npx&#xff08;npm 5.2 及以上版本自带&#xff09;来临时运行 nodemon&#xff1a; npx nodemon server.jsnodemon正常配置 要在开发过程中实现每次修改 Node.js 代码后…

计算机网络基础(3)_应用层自定义协议与序列化

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 计算机网络基础(3)_应用层自定义协议与序列化 收录于专栏【计算机网络】 本专栏旨在分享学习计算机网络的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&a…

E2E、CRC、Checksum、Rollingcounter

文章目录 前言1、E2E2、CRC3、Checksum4、Rollingcounter总结 前言 在专栏文章仿真CAN报文发送的CRC校验算法&#xff08;附CAPL代码&#xff09;和同星TSMaster中如何自定义E2E校验算法中分别给出了CRC算法和E2E校验实现&#xff0c;从中也明白了为什么在测试中需要去做这些仿…

嵌入式硬件杂谈(一)-推挽 开漏 高阻态 上拉电阻

引言&#xff1a;对于嵌入式硬件这个庞大的知识体系而言&#xff0c;太多离散的知识点很容易疏漏&#xff0c;因此对于这些容易忘记甚至不明白的知识点做成一个梳理&#xff0c;供大家参考以及学习&#xff0c;本文主要针对推挽、开漏、高阻态、上拉电阻这些知识点的学习。 目…

二叉树面试题(C 语言)

目录 1. 单值二叉树2. 相同的树3. 对称二叉树4. 二叉树的前序遍历5. 二叉树的中序遍历6. 二叉树的后序遍历7. 另一颗树的子树8. 通过前序遍历返回中序遍历 1. 单值二叉树 题目描述&#xff1a; 如果二叉树每个节点都具有相同的值&#xff0c;那么该二叉树就是单值二叉树。只有…

MFC中Excel的导入以及使用步骤

参考地址 在需要对EXCEL表进行操作的类中添加以下头文件&#xff1a;若出现大量错误将其放入stdafx.h中 #include "resource.h" // 主符号 #include "CWorkbook.h" //单个工作簿 #include "CRange.h" //区域类&#xff0c;对Excel大…

【C++】类中的“默认成员函数“--构造函数、析构函数、拷贝构造、赋值运算符重载

目录 "默认"成员函数 概念引入&#xff1a; 一、构造函数 问题引入&#xff1a; 1&#xff09;构造函数的概念 2&#xff09;构造函数实例 3&#xff09;构造函数的特性 4)关于默认生成的构造函数 (默认构造函数) 默认构造函数未完成初始化工作实例: 二…

LeetCode【0052】N皇后II

本文目录 1 中文题目2 求解方法&#xff1a;位运算回溯法2.1 方法思路2.2 Python代码2.3 复杂度分析 3 题目总结 1 中文题目 n 皇后问题 研究的是如何将 n 个皇后放置在 n n 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。 给你一个整数 n &#xff0c;返回 n 皇后问…

C语言-详细讲解-P1009 [NOIP1998 普及组] 高精度阶乘之和

目录 1.题目要求 2.题目解读 3.代码实现 4.一些小细节 1.数组储存大整数方式 2.memset函数介绍 3.digit与sum的关系 1.题目要求 2.题目解读 这道题本质就是高精度乘法高精度加法的结合&#xff0c;我之前有出过 高精度算法-保姆级讲解 希望详细了解的小伙伴可以去…

浅谈:基于三维场景的视频融合方法

视频融合技术的出现可以追溯到 1996 年 , Paul Debevec等 提出了与视点相关的纹理混合方法 。 也就是说 &#xff0c; 现实的漫游效果不是从摄像机的角度来看 &#xff0c; 但其仍然存在很多困难 。基于三维场景的视频融合 &#xff0c; 因其直观等特效在视频监控等相关领域有着…

Qt_day10_程序打包(完结)

目录 1. 设置图标 2. Debug和Release版本 3. 动态链接库 4. 打包 5. 联系项目要求 Qt开发的程序最终都是要给用户使用的&#xff0c;用户的电脑上不可能装一个Qt的开发环境导入项目使用。因此项目项目开发完成后需要打包——制作成安装包&#xff0c;用户直接下载并安装即可使用…

路径规划——RRT-Connect算法

路径规划——RRT-Connect算法 算法原理 RRT-Connect算法是在RRT算法的基础上进行的扩展&#xff0c;引入了双树生长&#xff0c;分别以起点和目标点为树的根节点同时扩展随机树从而实现对状态空间的快速搜索。在此算法中以两棵随机树建立连接为路径规划成功的条件。并且&…

【项目开发 | 跨域认证】JSON Web Token(JWT)

未经许可,不得转载。 文章目录 JWT设计背景:跨域认证JWT 原理JWT 结构JWT 使用方式注意JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,本文介绍它的原理、结构及用法。 JWT设计背景:跨域认证 互联网服务的用户认证流程是现代应用中的核心组成部分,通常的流程…