SpringBoot 项目使用 Sa-Token 完成登录认证
一、设计思路
对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验:
- 如果校验通过,则:正常返回数据。
- 如果校验未通过,则:抛出异常,告知其需要先进行登录。
那么,判断会话是否登录的依据是什么?我们先来简单分析一下登录访问流程:
- 用户提交
name
+password
参数,调用登录接口。 - 登录成功,返回这个用户的 Token 会话凭证。
- 用户后续的每次请求,都携带上这个 Token。
- 服务器根据 Token 判断此会话是否登录成功。
所谓登录认证,指的就是服务器校验账号密码,为用户颁发 Token 会话凭证的过程,这个 Token 也是我们后续判断会话是否登录的关键所在。
动态图演示:
接下来,我们将介绍在 SpringBoot 中如何使用 Sa-Token 完成登录认证操作。
Sa-Token 是一个 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
Gitee 开源地址:https://gitee.com/dromara/sa-token
首先在项目中引入 Sa-Token 依赖:
<!-- Sa-Token 权限认证 -->
<dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot-starter</artifactId><version>1.34.0</version>
</dependency>
注:如果你使用的是 SpringBoot 3.x
,只需要将 sa-token-spring-boot-starter
修改为 sa-token-spring-boot3-starter
即可。
二、登录与注销
根据以上思路,我们需要一个会话登录的函数:
// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
StpUtil.login(Object id);
只此一句代码,便可以使会话登录成功,实际上,Sa-Token 在背后做了大量的工作,包括但不限于:
- 检查此账号是否之前已有登录
- 为账号生成
Token
凭证与Session
会话 - 通知全局侦听器,xx 账号登录成功
- 将
Token
注入到请求上下文 - 等等其它工作……
你暂时不需要完整的了解整个登录过程,你只需要记住关键一点:Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端
。
所以一般情况下,我们的登录接口代码,会大致类似如下:
// 会话登录接口
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {// 第一步:比对前端提交的账号名称、密码if("zhang".equals(name) && "123456".equals(pwd)) {// 第二步:根据账号id,进行登录 StpUtil.login(10001);return SaResult.ok("登录成功");}return SaResult.error("登录失败");
}
如果你对以上代码阅读没有压力,你可能会注意到略显奇怪的一点:此处仅仅做了会话登录,但并没有主动向前端返回 Token 信息。
是因为不需要吗?严格来讲是需要的,只不过 StpUtil.login(id)
方法利用了 Cookie 自动注入的特性,省略了你手写返回 Token 的代码。
如果你对 Cookie 功能还不太了解,也不用担心,我们会在之后的 [ 前后端分离 ] 章节中详细的阐述 Cookie 功能,现在你只需要了解最基本的两点:
- Cookie 可以从后端控制往浏览器中写入 Token 值。
- Cookie 会在前端每次发起请求时自动提交 Token 值。
因此,在 Cookie 功能的加持下,我们可以仅靠 StpUtil.login(id)
一句代码就完成登录认证。
除了登录方法,我们还需要:
// 当前会话注销登录
StpUtil.logout();// 获取当前会话是否已经登录,返回true=已登录,false=未登录
StpUtil.isLogin();// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin();
异常 NotLoginException
代表当前会话暂未登录,可能的原因有很多:
前端没有提交 Token、前端提交的 Token 是无效的、前端提交的 Token 已经过期 …… 等等。
Sa-Token 未登录场景值参照表:
场景值 | 对应常量 | 含义说明 |
---|---|---|
-1 | NotLoginException.NOT_TOKEN | 未能从请求中读取到 Token |
-2 | NotLoginException.INVALID_TOKEN | 已读取到 Token,但是 Token无效 |
-3 | NotLoginException.TOKEN_TIMEOUT | 已读取到 Token,但是 Token已经过期 |
-4 | NotLoginException.BE_REPLACED | 已读取到 Token,但是 Token 已被顶下线 |
-5 | NotLoginException.KICK_OUT | 已读取到 Token,但是 Token 已被踢下线 |
那么,如何获取场景值呢?废话少说直接上代码:
// 全局异常拦截(拦截项目中的NotLoginException异常)
@ExceptionHandler(NotLoginException.class)
public SaResult handlerNotLoginException(NotLoginException nle)throws Exception {// 打印堆栈,以供调试nle.printStackTrace(); // 判断场景值,定制化异常信息 String message = "";if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {message = "未提供token";}else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {message = "token无效";}else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {message = "token已过期";}else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {message = "token已被顶下线";}else if(nle.getType().equals(NotLoginException.KICK_OUT)) {message = "token已被踢下线";}else {message = "当前会话未登录";}// 返回给前端return SaResult.error(message);
}
注意:以上代码并非处理逻辑的最佳方式,只为以最简单的代码演示出场景值的获取与应用,大家可以根据自己的项目需求来定制化处理
三、会话查询
// 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.getLoginId();// 类似查询API还有:
StpUtil.getLoginIdAsString(); // 获取当前会话账号id, 并转化为`String`类型
StpUtil.getLoginIdAsInt(); // 获取当前会话账号id, 并转化为`int`类型
StpUtil.getLoginIdAsLong(); // 获取当前会话账号id, 并转化为`long`类型// ---------- 指定未登录情形下返回的默认值 ----------// 获取当前会话账号id, 如果未登录,则返回null
StpUtil.getLoginIdDefaultNull();// 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)
StpUtil.getLoginId(T defaultValue);
四、Token 查询
// 获取当前会话的token值
StpUtil.getTokenValue();// 获取当前`StpLogic`的token名称
StpUtil.getTokenName();// 获取指定token对应的账号id,如果未登录,则返回 null
StpUtil.getLoginIdByToken(String tokenValue);// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
StpUtil.getTokenTimeout();// 获取当前会话的token信息参数
StpUtil.getTokenInfo();
TokenInfo
是 Token 信息 Model,用来描述一个 Token 的常用参数:
{"tokenName": "satoken", // token名称"tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633", // token值"isLogin": true, // 此token是否已经登录"loginId": "10001", // 此token对应的LoginId,未登录时为null"loginType": "login", // 账号类型标识"tokenTimeout": 2591977, // token剩余有效期 (单位: 秒)"sessionTimeout": 2591977, // User-Session剩余有效时间 (单位: 秒)"tokenSessionTimeout": -2, // Token-Session剩余有效时间 (单位: 秒) (-2表示系统中不存在这个缓存)"tokenActivityTimeout": -1, // token剩余无操作有效时间 (单位: 秒)"loginDevice": "default-device" // 登录设备类型
}
五、来个小测试,加深一下理解
新建 LoginAuthController
,复制以下代码
package com.pj.cases.use;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;/*** Sa-Token 登录认证示例 * * @author kong* @since 2022-10-13*/
@RestController
@RequestMapping("/acc/")
public class LoginAuthController {// 会话登录接口 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456@RequestMapping("doLogin")public SaResult doLogin(String name, String pwd) {// 第一步:比对前端提交的 账号名称 & 密码 是否正确,比对成功后开始登录 // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("zhang".equals(name) && "123456".equals(pwd)) {// 第二步:根据账号id,进行登录 // 此处填入的参数应该保持用户表唯一,比如用户id,不可以直接填入整个 User 对象 StpUtil.login(10001);// SaResult 是 Sa-Token 中对返回结果的简单封装,下面的示例将不再赘述 return SaResult.ok("登录成功");}return SaResult.error("登录失败");}// 查询当前登录状态 ---- http://localhost:8081/acc/isLogin@RequestMapping("isLogin")public SaResult isLogin() {// StpUtil.isLogin() 查询当前客户端是否登录,返回 true 或 false boolean isLogin = StpUtil.isLogin();return SaResult.ok("当前客户端是否登录:" + isLogin);}// 校验当前登录状态 ---- http://localhost:8081/acc/checkLogin@RequestMapping("checkLogin")public SaResult checkLogin() {// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`StpUtil.checkLogin();// 抛出异常后,代码将走入全局异常处理(GlobalException.java),如果没有抛出异常,则代表通过了登录校验,返回下面信息 return SaResult.ok("校验登录成功,这行字符串是只有登录后才会返回的信息");}// 获取当前登录的账号是谁 ---- http://localhost:8081/acc/getLoginId@RequestMapping("getLoginId")public SaResult getLoginId() {// 需要注意的是,StpUtil.getLoginId() 自带登录校验效果// 也就是说如果在未登录的情况下调用这句代码,框架就会抛出 `NotLoginException` 异常,效果和 StpUtil.checkLogin() 是一样的 Object userId = StpUtil.getLoginId();System.out.println("当前登录的账号id是:" + userId);// 如果不希望 StpUtil.getLoginId() 触发登录校验效果,可以填入一个默认值// 如果会话未登录,则返回这个默认值,如果会话已登录,将正常返回登录的账号id Object userId2 = StpUtil.getLoginId(0);System.out.println("当前登录的账号id是:" + userId2);// 或者使其在未登录的时候返回 null Object userId3 = StpUtil.getLoginIdDefaultNull();System.out.println("当前登录的账号id是:" + userId3);// 类型转换:// StpUtil.getLoginId() 返回的是 Object 类型,你可以使用以下方法指定其返回的类型 int userId4 = StpUtil.getLoginIdAsInt(); // 将返回值转换为 int 类型 long userId5 = StpUtil.getLoginIdAsLong(); // 将返回值转换为 long 类型 String userId6 = StpUtil.getLoginIdAsString(); // 将返回值转换为 String 类型 // 疑问:数据基本类型不是有八个吗,为什么只封装以上三种类型的转换?// 因为大多数项目都是拿 int、long 或 String 声明 UserId 的类型的,实在没见过哪个项目用 double、float、boolean 之类来声明 UserId System.out.println("当前登录的账号id是:" + userId4 + " --- " + userId5 + " --- " + userId6);// 返回给前端 return SaResult.ok("当前客户端登录的账号id是:" + userId);}// 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo@RequestMapping("tokenInfo")public SaResult tokenInfo() {// TokenName 是 Token 名称的意思,此值也决定了前端提交 Token 时应该使用的参数名称 String tokenName = StpUtil.getTokenName();System.out.println("前端提交 Token 时应该使用的参数名称:" + tokenName);// 使用 StpUtil.getTokenValue() 获取前端提交的 Token 值 // 框架默认前端可以从以下三个途径中提交 Token:// Cookie (浏览器自动提交)// Header头 (代码手动提交)// Query 参数 (代码手动提交) 例如: /user/getInfo?satoken=xxxx-xxxx-xxxx-xxxx // 读取顺序为: Query 参数 --> Header头 -- > Cookie // 以上三个地方都读取不到 Token 信息的话,则视为前端没有提交 Token String tokenValue = StpUtil.getTokenValue();System.out.println("前端提交的Token值为:" + tokenValue);// TokenInfo 包含了此 Token 的大多数信息 SaTokenInfo info = StpUtil.getTokenInfo();System.out.println("Token 名称:" + info.getTokenName());System.out.println("Token 值:" + info.getTokenValue());System.out.println("当前是否登录:" + info.getIsLogin());System.out.println("当前登录的账号id:" + info.getLoginId());System.out.println("当前登录账号的类型:" + info.getLoginType());System.out.println("当前登录客户端的设备类型:" + info.getLoginDevice());System.out.println("当前 Token 的剩余有效期:" + info.getTokenTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在System.out.println("当前 Token 的剩余临时有效期:" + info.getTokenActivityTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在System.out.println("当前 User-Session 的剩余有效期" + info.getSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在System.out.println("当前 Token-Session 的剩余有效期" + info.getTokenSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在// 返回给前端 return SaResult.data(StpUtil.getTokenInfo());}// 会话注销 ---- http://localhost:8081/acc/logout@RequestMapping("logout")public SaResult logout() {// 退出登录会清除三个地方的数据:// 1、Redis中保存的 Token 信息// 2、当前请求上下文中保存的 Token 信息 // 3、Cookie 中保存的 Token 信息(如果未使用Cookie模式则不会清除)StpUtil.logout();// StpUtil.logout() 在未登录时也是可以调用成功的,// 也就是说,无论客户端有没有登录,执行完 StpUtil.logout() 后,都会处于未登录状态 System.out.println("当前是否处于登录状态:" + StpUtil.isLogin());// 返回给前端 return SaResult.ok("退出登录成功");}}
代码注释已针对每一步操作做出详细解释,大家可根据可参照注释中的访问链接进行逐步测试。
本示例代码已上传至 Gitee,可参考:
Sa-Token 登录认证示例
参考资料
- Gitee 仓库地址:https://gitee.com/dromara/sa-token
- GitHub 仓库地址:https://github.com/dromara/sa-token
- Sa-Token 在线文档:https://sa-token.dev33.cn/
相关文章:

SpringBoot 项目使用 Sa-Token 完成登录认证
一、设计思路 对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验: 如果校验通过,则:正常返回数据。如果校验未通过,则:抛出异…...
javaScript 蓝桥杯----梅楼封的一天
目录一、介绍二、目标1.函数入参要求:2.出参要求:三、输出示例1.示例一2.示例二3.示例三4.示例四5.示例五四、待完善代码五、知识点1.正则表达式2.split方法3.test方法4.match方法5.matchAll方法6.slice方法7.replace() 方法8.repeat方法六、答案一、介绍…...

谷粒商城笔记+踩坑(18)——购物车
目录 一、环境搭建 1.1、购物车模块初始化 1.2、动静资源处理 1.3、页面跳转配置 二、数据模型分析 2.1、购物车需求 2.1.1、离线购物车和在线购物车需求、数据库选择redis 2.1.2、购物车数据结构 2.2、模型类抽取,Cart和CartItem 2.3、Redis依赖和配置、…...

进阶C语言:指针笔试题
在学习完进阶C指针之后,可以来做一些笔试题来进行提升、巩固,小编在这里给大家分享几道比较有意思的笔试题 目录 一、笔试题1: 二、笔试题2 三、笔试题3: 四、笔试题4: 五、笔试题5: 编辑 六、笔试题…...

基于SSM(jsp)的宿舍管理系统
带项目源码和数据库文件 MySQL 导入即可用 可作为毕设参考 框架:SSM 1.1功能需求 本系统的设计目的是为了让老师可以随时随地的关注学生的在校情情况,方便老师对学生在校情况的添加、修改、删除和查询,实时关注学生的安全问题。该系统能…...
Java Web应用开发——作业四
一.单项选择题(共3题,30.0分) 1 如何获取一个 Cookie[]( ) A、 request.getCookies() B、 request.getCookie() C、 response.getCookies() D、 response.getCookie() 正确答案: A 2 ( )隐…...

基于ASP的反垃圾邮件管理系统的设计与实现
随着Internet的迅速普及,电子邮件以其快捷、方便、低成本的特点逐渐成为人们进行信息交流的主要媒介之一,但是随之而来的垃圾邮件也越来越泛滥。垃圾邮件占用了有限的存储、计算和网络资源,耗费了用户大量的处理时间,影响和干扰了…...

2010年9月计算机二级JAVA笔试试题及答案
一、选择题(每小题2分,共70分) 下列各题A)、B)、C)、D)四个选项中,只有一个选项是正确的。请将正确选项填涂在答题卡相应位置上,答在试卷上不得分。 (1&…...

博客让谷歌或是百度收录
参考以下大佬的博客教程 Hexo框架(六):SEO优化及站点被搜索引擎收录设置 | 你真是一个美好的人类 第一步 安装百度和 Google 的站点地图生成插件: npm install hexo-generator-baidu-sitemap --save npm install hexo-generator-sitemap --save 然后来…...

机器学习分类算法评价指标
一. 分类评价指标 对机器学习算法的性能进行评估时,不仅需要有效可行的实验估计方法,还需要有衡量模型泛化能力的评价指标,这就是算法评价指标。分类算法的评价指标一般有准确率,精确率,召回率,F1-score&a…...

Socks5代理服务器示例详解
Go语言中变量的声明和JavaScript很像,使用var关键字,变量的声明、定义有好几种形式 变量和常量 // 声明并初始化一个变量 var m int 10 // 声明初始化多个变量 var i, j, k 1, 2, 3 // 多个变量的声明(注意小括号的使用) var(no intname string ) //…...
使用 Docker 和 Nginx 反向代理访问 ChatGPT API
在许多应用程序中,我们需要从一个外部服务中获取数据。然而,由于种种原因(例如跨域问题、API 访问限制等),我们可能无法直接从客户端访问这些服务。这时,反向代理可以成为我们的救星。在这篇文章中…...
[前端笔记038]vue2之vueRouter、elementUI
前言 本笔记参考视频,尚硅谷:BV1Zy4y1K7SH p117 - p135 相关理解 vue 的一个插件库,专门用来实现SPA 应用单页 Web 应用(single page web application,SPA),整个应用只有一个完整的页面点击页面中的导航…...
ChatGPT使用案例之操作Excel
ChatGPT使用案例之操作Excel 微软已经通过其官网宣布,正在将其基于GPT-4的人工智能(AI)技术植入到其Office办公软件当中,该功能名为“Microsoft 365 Copilot”。微软称其是基于大语言模型的下一代AI生产力技术,目前已经向部分商业用户开放。 Microsoft 365 负责人Jared …...

【算法基础】(二)数据结构 --- 单链表
✨个人主页:bit me ✨当前专栏:算法基础 🔥专栏简介:该专栏主要更新一些基础算法题,有参加蓝桥杯等算法题竞赛或者正在刷题的铁汁们可以关注一下,互相监督打卡学习 🌹 🌹 dz…...
STL容器之<multiset>
文章目录测试环境multiset介绍头文件模块类定义对象构造初始化元素访问元素插入和删除元素查找容器大小迭代器其他函数测试环境 系统:ubuntu 22.04.2 LTS 64位 gcc版本:11.3.0 编辑器:vsCode 1.76.2 multiset介绍 关联式容器。元素是唯一的…...
python实战应用讲解-【numpy专题篇】numpy常见函数使用示例(三)(附python示例代码)
目录 Python numpy.finfo()函数 Python Numpy MaskedArray.masked_less()函数 Python Numpy MaskedArray.masked_less_equal()函数 Python Numpy MaskedArray.masked_not_equal()函数 Python Numpy MaskedArray masked_outside()函数 Python Numpy MaskedArray.masked_wh…...
【Android笔记89】Android之全局加载框Gloading的使用
这篇文章,主要介绍Android之全局加载框Gloading的使用。 目录 一、Gloading全局加载框 1.1、Gloading介绍 1.2、Gloading运行效果 1.3、Gloading的使用...

php微信小程序java+Vue高校课程课后辅导在线教育系统nodejs+python
目 录 1绪论 1 1.1项目研究的背景 1 1.2开发意义 1 1.3项目研究现状及内容 5 1.4论文结构 5 2开发技术介绍 7 2.1 B/S架构 7 2.2 MySQL 介绍 7 2.3 MySQL环境配置 7 2.5微信小程序技术 8 3系统分析 9 3.1可行性分析 9 3.1.1技术可行性 9 3.1.2经济可行性 9 3.1.3操作可行性 10 …...

公司刚来的00后真卷,上班还没2年,跳到我们公司起薪20k....
都说00后躺平了,但是有一说一,该卷的还是卷。 这不,前段时间我们公司来了个00后,工作都没两年,跳槽到我们公司起薪18K,都快接近我了。后来才知道人家是个卷王,从早干到晚就差搬张床到工位睡觉了…...

Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...
【AI学习】三、AI算法中的向量
在人工智能(AI)算法中,向量(Vector)是一种将现实世界中的数据(如图像、文本、音频等)转化为计算机可处理的数值型特征表示的工具。它是连接人类认知(如语义、视觉特征)与…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...
代理篇12|深入理解 Vite中的Proxy接口代理配置
在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...

LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...

脑机新手指南(七):OpenBCI_GUI:从环境搭建到数据可视化(上)
一、OpenBCI_GUI 项目概述 (一)项目背景与目标 OpenBCI 是一个开源的脑电信号采集硬件平台,其配套的 OpenBCI_GUI 则是专为该硬件设计的图形化界面工具。对于研究人员、开发者和学生而言,首次接触 OpenBCI 设备时,往…...

给网站添加live2d看板娘
给网站添加live2d看板娘 参考文献: stevenjoezhang/live2d-widget: 把萌萌哒的看板娘抱回家 (ノ≧∇≦)ノ | Live2D widget for web platformEikanya/Live2d-model: Live2d model collectionzenghongtu/live2d-model-assets 前言 网站环境如下,文章也主…...

数据分析六部曲?
引言 上一章我们说到了数据分析六部曲,何谓六部曲呢? 其实啊,数据分析没那么难,只要掌握了下面这六个步骤,也就是数据分析六部曲,就算你是个啥都不懂的小白,也能慢慢上手做数据分析啦。 第一…...

如何把工业通信协议转换成http websocket
1.现状 工业通信协议多数工作在边缘设备上,比如:PLC、IOT盒子等。上层业务系统需要根据不同的工业协议做对应开发,当设备上用的是modbus从站时,采集设备数据需要开发modbus主站;当设备上用的是西门子PN协议时…...