【Spring】图解SpringSecurity的RememberMe流程

news2024/11/24 0:24:09

  5a2585dded9b416fb4ea58637b42ed39.png

  Yan-英杰的主页

悟已往之不谏 知来者之可追  

C++程序员,2024届电子信息研究生


目录

前言

        原理

基础版

        搭建

初始化sql

依赖引入

配置类

验证

源码分析

进阶版

        集成

        源码分析

疑问1

疑问2

        鉴权

升级版

        集成

初始化sql

配置类

验证

        源码分析

        鉴权

流程

扩展版


前言

        如图就是博客园登陆时的“记住我”选项,在实际开发登陆接口以前,我一直认为这个“记住我”就是把我的用户名和密码保存到浏览器的 cookie 中,当下次登陆时浏览器会自动显示我的用户名和密码,就不用我再次输入了。

        直到我看了 Spring SecurityRemember Me 相关的源码,我才意识到之前的理解全错了,它的作用其实是让用户在关闭浏览器之后再次访问时不需要重新登陆。

原理

        如果用户勾选了 “记住我” 选项,Spring Security 将在用户登录时创建一个持久的安全令牌,并将令牌存储在 cookie 中或者数据库中。当用户关闭浏览器并再次打开时,Spring Security 可以根据该令牌自动验证用户身份。

        先来张图感受下,然后跟着阿Q从简单的Spring Security 登陆样例开始慢慢搭建吧!

基础版

        搭建

                初始化sql

//用户表
CREATE TABLE `sys_user_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
//插入用户数据
INSERT INTO sys_user_info
(id, username, password)
VALUES(1, 'cheetah', '$2a$10$N.zJIQtKLyFe62/.wL17Oue4YFXUYmbWICsMiB7c0Q.sF/yMn5i3q');
​
//产品表
CREATE TABLE `product_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `price` decimal(10,4) DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  `update_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
//插入产品数据
INSERT INTO product_info
(id, name, price, create_date, update_date)
VALUES(1, '从你的全世界路过', 32.0000, '2020-11-21 21:26:12', '2021-03-27 22:17:39');
INSERT INTO product_info
(id, name, price, create_date, update_date)
VALUES(2, '乔布斯传', 25.0000, '2020-11-21 21:26:42', '2021-03-27 22:17:42');
INSERT INTO product_info
(id, name, price, create_date, update_date)
VALUES(3, 'java开发', 87.0000, '2021-03-27 22:43:31', '2021-03-27 22:43:34');

依赖引入

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
</dependency>

配置类

自定义 SecurityConfig 类继承 WebSecurityConfigurerAdapter 类,并实现里边的 configure(HttpSecurity httpSecurity)方法。

/**
  * 安全认证及授权规则配置
  **/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
 httpSecurity
   .authorizeRequests()
   .anyRequest()
   //除上面外的所有请求全部需要鉴权认证
   .authenticated()
   .and()
   //登陆成功之后的跳转页面
   .formLogin().defaultSuccessUrl("/productInfo/index").permitAll()
   .and()
   //CSRF禁用
   .csrf().disable();
}

另外还需要指定认证对象的来源和密码加密方式

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
 auth.userDetailsService(userInfoService).passwordEncoder(passwordEncoder());
}
​
@Bean
public BCryptPasswordEncoder passwordEncoder(){
 return new BCryptPasswordEncoder();
}

        验证

        启动程序,浏览器打开http://127.0.0.1:8080/login

        输入用户名密码登陆成功

        我们就可以拿着 JSESSIONID 去请求需要登陆的资源了。

源码分析

方框中的是类和方法名,方框外是类中的方法具体执行到的代码。

        首先会按照图中箭头的方向来执行,最终会执行到我们自定义的实现了 UserDetailsService 接口的 UserInfoServiceImpl 类中的查询用户的方法 loadUserByUsername()

        当认证通过之后会在SecurityContext中设置Authentication对象

org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication`中的方法`SecurityContextHolder.getContext().setAuthentication(authResult);

最后调用onAuthenticationSuccess方法跳转链接。

进阶版

集成

接下来我们就要开始进入正题了,快速接入“记住我”功能。

在配置类 SecurityConfig 的 configure() 方法中加入两行代码,如下所示

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
 httpSecurity
   .authorizeRequests()
   .anyRequest()
   //除上面外的所有请求全部需要鉴权认证
   .authenticated()
   .and()
   //开启 rememberMe 功能
   .rememberMe()
   .and()
   //登陆成功之后的跳转页面
   .formLogin().defaultSuccessUrl("/productInfo/index").permitAll()
   .and()
   //CSRF禁用
   .csrf().disable();
}

重启应用页面上会出现单选框“Remember me on this computer”

可以查看下页面的属性,该单选框的名字为“remember-me”

点击登陆,在 cookie 中会出现一个属性为 remember-me 的值,在以后的每次发送请求都会携带这个值到后台

然后我们直接输入http://127.0.0.1:8080/productInfo/getProductList获取产品信息

当我们把 cookie 中的 JSESSIONID 删除之后重新获取产品信息,发现会生成一个新的 JSESSIONID。

源码分析

认证通过的流程和基础版本一致,我们着重来分析身份认证通过之后,跳转链接之前的逻辑。

疑问1

图中1处为啥是 AbstractRememberMeServices 类呢?

我们发现在项目启动时,在类 AbstractAuthenticationFilterConfigurer 的 configure() 方法中有如下代码

RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class);
if (rememberMeServices != null) {
 this.authFilter.setRememberMeServices(rememberMeServices);
}

AbstractRememberMeServices 类型就是在此处设置完成的,是不是一目了然了?

疑问2

当代码执行到图中2和3处时

@Override
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
                               Authentication successfulAuthentication) {
    if (!rememberMeRequested(request, this.parameter)) {
        this.logger.debug("Remember-me login not requested.");
        return;
    }
    onLoginSuccess(request, response, successfulAuthentication);
}

因为我们勾选了“记住我”,所以此时的值为“on”,即rememberMeRequested(request, this.parameter)返回 true,然后加非返回 false,最后一步就是设置 cookie 的值。

鉴权

此处的讲解一定要对照着代码来看,要不然很容易错位,没有类标记的方法都属于RememberMeAuthenticationFilter#doFilter

当直接调用http://127.0.0.1:8080/productInfo/index接口时,会走RememberMeAuthenticationFilter#doFilter的代码

//此处存放的是登陆的用户信息,可以理解为对应的cookie中的 JSESSIONID 
if (SecurityContextHolder.getContext().getAuthentication() != null) {
 this.logger.debug(LogMessage
   .of(() -> "SecurityContextHolder not populated with remember-me token, as it already contained: '"
     + SecurityContextHolder.getContext().getAuthentication() + "'"));
 chain.doFilter(request, response);
 return;
}

因为SecurityContextHolder.getContext().getAuthentication()中有用户信息,所以直接返回商品信息。

当删掉 JSESSIONID 后重新发起请求,发现SecurityContextHolder.getContext().getAuthentication()为 null ,即用户未登录,会往下走Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);代码,即自动登陆的逻辑

@Override
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
    //该方法的this.cookieName 的值为"remember-me",所以该处返回的是 cookie中remember-me的值
 String rememberMeCookie = extractRememberMeCookie(request);
 if (rememberMeCookie == null) {
  return null;
 }
 this.logger.debug("Remember-me cookie detected");
 if (rememberMeCookie.length() == 0) {
  this.logger.debug("Cookie was empty");
  cancelCookie(request, response);
  return null;
 }
 try {
        //对rememberMeCookie进行解码:
  String[] cookieTokens = decodeCookie(rememberMeCookie);
        //重点:执行TokenBasedRememberMeServices#processAutoLoginCookie下的 UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);
        //就又回到我们自定义的 UserInfoServiceImpl 类中执行代码,返回user
  UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
  this.userDetailsChecker.check(user);
  this.logger.debug("Remember-me cookie accepted");
  return createSuccessfulAuthentication(request, user);
 }
 catch (CookieTheftException ex) {
  cancelCookie(request, response);
  throw ex;
 }
 catch (UsernameNotFoundException ex) {
  this.logger.debug("Remember-me login was valid but corresponding user not found.", ex);
 }
 catch (InvalidCookieException ex) {
  this.logger.debug("Invalid remember-me cookie: " + ex.getMessage());
 }
 catch (AccountStatusException ex) {
  this.logger.debug("Invalid UserDetails: " + ex.getMessage());
 }
 catch (RememberMeAuthenticationException ex) {
  this.logger.debug(ex.getMessage());
 }
 cancelCookie(request, response);
 return null;
}

        执行完之后接着执行RememberMeAuthenticationFilter#doFilter中的rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);

        当执行到ProviderManager#authenticate中的result = provider.authenticate(authentication);时,会走RememberMeAuthenticationProvider 中的方法返回 Authentication 对象。

        SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);将登录成功信息保存到 SecurityContextHolder 对象中,然后返回商品信息。

升级版

        如果记录在服务器 session 中的 token 因为服务重启而失效,就会导致前端用户明明勾选了“记住我”的功能,但是仍然提示需要登陆。

        这就需要我们对 session 中的 token 做持久化处理,接下来我们就对他进行升级。

集成

        初始化sql

CREATE TABLE `persistent_logins` (
  `username` varchar(64) NOT NULL COMMENT '用户名',
  `series` varchar(64) NOT NULL COMMENT '主键',
  `token` varchar(64) NOT NULL COMMENT 'token',
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后一次使用的时间',
  PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

配置类

//在SecurityConfig的configure方法中增加一行
.rememberMe().tokenRepository(persistentTokenRepository());
    
//引入依赖,注入bean
@Autowired
private DataSource dataSource;
​
@Bean
public PersistentTokenRepository persistentTokenRepository(){
 JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
 tokenRepository.setDataSource(dataSource);
 return tokenRepository;
}

验证

重启项目,访问http://127.0.0.1:8080/login之后返回数据,查看表中数据,完美。

源码分析

前边的流程和升级版是相同的,区别就在于创建 token 之后是保存到 session 中还是持久化到数据库中,接下来我们从源码分析一波。

定位到AbstractRememberMeServices#loginSuccess中的 onLoginSuccess()方法,实际执行的是PersistentTokenBasedRememberMeServices#onLoginSuccess方法。

/**
 * 使用新的序列号创建新的永久登录令牌,并将数据存储在
 * 持久令牌存储库,并将相应的 cookie 添加到响应中。
 *
 */
@Override
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
  Authentication successfulAuthentication) {
 ......
 try {
        //重点代码创建token并保存到数据库中
  this.tokenRepository.createNewToken(persistentToken);
  addCookie(persistentToken, request, response);
 }
 ......
}

因为我们在配置类中定义的是JdbcTokenRepositoryImpl,所以进入改类的createNewToken方法。

@Override
public void createNewToken(PersistentRememberMeToken token) {
 getJdbcTemplate().update(this.insertTokenSql, token.getUsername(), token.getSeries(), token.getTokenValue(),
   token.getDate());
}

此时我们发现他就是做了插入数据库的操作,并且this.insertTokenSql

insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)

同时我们看到了熟悉的建表语句

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
   + "token varchar(64) not null, last_used timestamp not null)

这样是不是就决解了上边的疑惑了呢。

执行完PersistentTokenBasedRememberMeServices#onLoginSuccess方法之后又进入到RememberMeAuthenticationFilter#doFilter()方法中结束。

有了持久化之后就不用担心服务重启了,接着我们重启服务,继续访问获取商品接口,成功返回商品信息。

鉴权

鉴权的逻辑也是和进阶版相似的,区别在于删除浏览器的 JSESSIONID 之后的逻辑。

定位到AbstractRememberMeServices#autoLogin中的UserDetails user = processAutoLoginCookie(cookieTokens, request, response);执行的是PersistentTokenBasedRememberMeServices#processAutoLoginCookie

//删减版代码
@Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
  HttpServletResponse response) {
 ......
 String presentedSeries = cookieTokens[0];
 String presentedToken = cookieTokens[1];
 PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
 if (token == null) {
  throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
 }
 if (!presentedToken.equals(token.getTokenValue())) {
  this.tokenRepository.removeUserTokens(token.getUsername());
  throw new CookieTheftException(this.messages.getMessage(
    "PersistentTokenBasedRememberMeServices.cookieStolen",
    "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
 }
 if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
  throw new RememberMeAuthenticationException("Remember-me login has expired");
 }
 PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(),
   generateTokenData(), new Date());
 try {
  this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
  addCookie(newToken, request, response);
 }
 ......
 return getUserDetailsService().loadUserByUsername(token.getUsername());
}

流程

  • 通过数据库中的 series 字段找到对应的记录;

  • 记录是否为空判断以及记录中的 token 是否和传入的相同;

  • 记录中的 last_used 加上默认的两周后是否大于当前时间,即是否 token 失效;

  • 更新该记录并将新生成的 token 放到 cookie 中;

后续的逻辑和进阶版一致。

扩展版

看到这有的小伙伴肯定会问了,如果我不用默认的登录页面,想用自己的登录页需要注意些什么呢?

首先要注意的就是“记住我”勾选框参数名必须为“remember-me”。如果你想自定义的话也是可以的,需要将自定义的名字例如:remember-me-new 配置到配置类中。

.rememberMe().rememberMeParameter("remember-me-new")

token 的有效期也是可以自定义的,例如设置有效期为2天

.rememberMe().tokenValiditySeconds(2*24*60*60)

我们还可以自定义保存在浏览器中的 cookie 的名称

.rememberMe().rememberMeCookieName("remember-me-cookie")

文末送书

        

内容简介

        

        本书是一本全面介绍在PyTorch环境下学习机器学习和深度学习的综合指南,可以作为初学者的入门教程,也可以作为读者开发机器学习项目时的参考书。

        本书讲解清晰、示例生动,深入介绍了机器学习方法的基础知识,不仅提供了构建机器学习模型的说明,而且提供了构建机器学习模型和解决实际问题的基本准则。本书添加了基于PyTorch的深度学习内容,介绍了新版Scikit-Learn。本书涵盖了多种用于文本和图像分类的机器学习与深度学习方法,介绍了用于生成新数据的生成对抗网络(GAN)和用于训练智能体的强化学习。最后,本书还介绍了深度学习的新动态,包括图神经网络和用于自然语言处理(NLP)的大型transformer。无论是机器学习入门新手,还是计划跟踪机器学习进展的研发人员,都可以将本书作为使用Python进行机器学习的不二之选。

作者简介

        

        塞巴斯蒂安·拉施卡(Sebastian Raschka)
        获密歇根州立大学博士学位,现在是威斯康星-麦迪逊大学统计学助理教授,从事机器学习和深度学习研究。他的研究方向是数据受限的小样本学习和构建预测有序目标值的深度神经网络。他还是一位开源贡献者,担任Grid.ai的首席AI教育家,热衷于传播机器学习和AI领域知识。
        刘玉溪(海登)[ Yuxi (Hayden) Liu ]
        在谷歌公司担任机器学习软件工程师,曾担任机器学习科学家。他是一系列机器学习书籍的作者。他的第一本书Python Machine Learning By Example在2017年和2018年亚马逊同类产品中排名第一,已被翻译成多种语言。
        瓦希德·米尔贾利利(Vahid Mirjalili)
        获密歇根州立大学机械工程和计算机科学双博士学位,是一名专注于计算机视觉和深度学习的科研工作者。

        作者Sebastian Raschka很擅长用易于理解的方式解释复杂的方法和概念。随着深度学习革命深入到各个领域,Sebastian Raschka和他的团队不断升级、完善书的内容,陆续出版了第2版和第3版。本书在前3个版本的基础上新增了某些章节,包含了PyTorch相关的内容,覆盖了Transformer和图神经网络。这些是目前深度学习领域的前沿方法,在过去两年中席卷了文本理解和分子结构等领域。

        作者拥有专业知识和解决实际问题的经验,因此出色地平衡了书中的理论知识和动手实践内容。Sebastian Raschka和Vahid Mirjalili在计算机视觉和计算生物学领域拥有丰富的科研经验。Yuxi Liu擅长解决机器学习领域的实际问题,例如将机器学习方法用于事件预测、推荐系统等。本书的作者都对教育有着满腔热忱,他们用浅显易懂的语言编写了本书以满足读者的需求。

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

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

相关文章

spring cloud 之 sentinel

sentinel概述 随着微服务的流行&#xff0c;服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点&#xff0c;从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性 sentinel的两个部分 核心库&#xff08;Java 客户端&#xff09;&#xff1a; 不依赖任…

社保卡读卡器用的NCN8025接触式芯片供电问题

社保卡读卡器接触式卡座里面用了NCN8025芯片&#xff0c;NCN8025芯片的电压输入端&#xff0c;选择5V输出的时候&#xff0c;最小应该是4.75V&#xff0c;对应到读卡器USB这边那最好是不低于4.8V 如果电压过低&#xff0c;低于4.8就会出现可能无法读取社保卡情况&#xff0c;即…

Cesium 加载发光材质的行政区边界

在 cesium 中可以为 entities 设置材质,比如一条发光的线可以像如下这样设置: viewer.entities.add({name: "",polyline: {positions: Cesium.Cartesian3.fromDegreesArrayHeights([104.57311103029339, 30.71454550939871, 451.0205115032204,104.57369073762032,…

Appium+python自动化(三)- SDK Manager(超详解)

简介 本来一开始打算用真机做的&#xff0c;所以在前边搭建环境时候就没有下载SDK&#xff0c;但是由于许多小伙伴留言说是没有真机&#xff0c;所以顺应民意整理一下模拟器。SDK顾名思义&#xff0c;Android SDK Manager就是一个Android软件开发工具包管理器&#xff0c;就像一…

部署langchain+chatglm

先参考&#xff1a;window零基础部署langchain-ChatGLM_飞奔的屎壳郎的博客-CSDN博客 安装一部分&#xff0c; 1.GCC安装 gcc64位下载 一定要装64位的gcc&#xff0c;因为我的电脑是w10 64位的&#xff0c;装32位运行langchain报错并配置环境变量 可直接用压缩包中的文件&am…

改进 Elastic Stack 中的信息检索:混合检索 - hybrid retrieval

作者&#xff1a;Quentin Herreros, Thomas Veasey 在上一篇博文中&#xff0c;我们介绍了 Elastic Learned Sparse Encoder&#xff0c;这是一种经过训练可有效进行零样本文本检索的模型。 Elasticsearch 还具有强大的词汇检索功能和丰富的工具来组合不同查询的结果。 在本博客…

测试开发必备技能:JMeter 二次开发函数

一、简介 1、概述 Apache JMeter 是 Apache 基于 Java 开发的开源压力测试工具 最初被设计用于 Web 应用测试&#xff0c;但后来扩展到了其他测试领域&#xff0c;像接口测试 但&#xff0c;随着 IT 行业的快速发展&#xff0c;不同企业或组织需求更加丰富&#xff0c;JMeter…

C# Linq 详解四

目录 概述 二十、SelectMany 二十一、Aggregate 二十二、DistinctBy 二十三、Reverse 二十四、SequenceEqual 二十五、Zip 二十六、SkipWhile 二十七、TakeWhile C# Linq 详解一 1.Where 2.Select 3.GroupBy 4.First / FirstOrDefault 5.Last / LastOrDefault C# Li…

PMP,一场不可能fail的考试

2018年我参加了PMP考试&#xff0c;结果毫无悬念地轻松通过了。和我一起参加考试的七位伙伴也都取得了不错的成绩。 我是在去年7月份报名的&#xff0c;准备参加9月初的PMP考试。我记得我花费了3800元的培训费和3300元的考试费。由于PMP国内考试的报名条件之一是要求参加过由国…

C++ 指向vector[0]的指针

结论&#xff1a;指针p指向vector的第0个元素 p &v[0] 可以使用p[i]继续访问vector的其他元素&#xff0c;见test3 #include <vector> #include <iostream>/* 测试目的&#xff0c;指针p0指向vector的第0个元素 p &v[0] 可以使用p[i]继续访问vector的…

骨传导耳机好用吗?盘点五款好用的骨传导耳机推荐

在骨传导耳机还没有火之前&#xff0c;相信很多朋友都是使用入耳式和头戴式耳机比较多一点&#xff0c;但是慢慢的会发现&#xff0c;这两种耳机都存在很大的问题&#xff0c;比如说入耳式耳机&#xff0c;长时间佩戴会造成耳朵痛等问题&#xff0c;而头戴式耳机因为隔音效果好…

Spark编程-键值对RDD(K,V)创建及常用操作

简述 SparkRDD中可以包含任何类型的对象&#xff0c;在实际应用中&#xff0c;“键值对”是一种比较常见的RDD元素类型&#xff0c;分组和聚合操作中经常会用到&#xff0c;尤其是groupByKey和reduceByKey。 Spark操作中经常会用到“键值对RDD”&#xff08;Pair RDD&a…

swiper滚动块宽度踩坑记录

背景&#xff1a;需要给swiper增加图片懒加载优化性能&#xff0c;这里使用的是swiper自带的 lazy api。但是加了懒加载后发现滚动块的宽度变长了&#xff0c;这里的原因是我只给滚动条设置了宽度的样式但是没有给滚动块设置宽度的样式。于是我按照官方文档的做法给滚动块设置宽…

STM32单片机语音识别台灯控制系统人检测亮度调节

实践制作DIY- GC00156-语音识别台灯控制系统 一、功能说明&#xff1a; 基于STM32单片机设计-语音识别台灯控制系统 二、功能说明&#xff1a; 电路&#xff1a;STM32F103C系列最小系统串口语音识别模块LED灯板1个红外传感器 1.任何时候没有人则关闭灯。有人可以自动打开灯。…

LIS检验信息系统

LIS检验信息系统是以病人为中心、以业务处理为基础、以提高检验科室管理水平和工作效率为目标&#xff0c;将医学检验、科室管理和财务统计等工作进行整合&#xff0c;全面改善检验科室的工作现状。 LIS把检验、检疫、放免、细菌微生物及科研使用的各类分析仪器&#xff0c;通…

pandas的DataFrame转存MATLAB的mat格式

有的时候需要把 pandas 处理好的 DataFrame 进一步交给MATLAB来处理。当然可以保存成 excel 文件&#xff0c;不过当数据量比较大的时候&#xff0c;读取比较慢&#xff0c;这个时候转存成 MATLAB 可读的 mat 文件更合适&#xff08;MATLAB 能快速读取&#xff09;。 标准的操…

接口自动化测试的最佳工程实践 (ApiTestEngine)

目录 前言&#xff1a; 背景 核心特性 特性拆解介绍 写在后面 前言&#xff1a; 接口自动化测试是现代软件开发中不可或缺的一环。一个良好的测试框架和最佳工程实践可以提高测试效率和质量。 背景 当前市面上存在的接口测试工具已经非常多&#xff0c;常见的如Postman…

建筑结构健康监测常见问题及解决措施

建筑结构健康监测(SHM)是指利用无损传感技术&#xff0c;通过对结构特性进行分析&#xff0c;达到检测结构损伤或退化的目的&#xff0c;是当前守护建筑安全的一种新型技术手段&#xff0c;通过建筑结构健康监测管理者可以直观的了解到建筑物的健康状态&#xff0c;为建筑维护和…

【力扣算法13】之 12. 整数转罗马数字 python

文章目录 问题描述示例1示例2 示例 3:示例 4:示例 5:提示 思路分析代码分析完整代码详细分析运行效果截图调用示例运行结果 完结 问题描述 罗马数字包含以下七种字符&#xff1a; I&#xff0c; V&#xff0c; X&#xff0c; L&#xff0c;C&#xff0c;D 和 M。 字符数值I1V5X…

AP5193 DC-DC宽电压LED降压恒流驱动器 LED电源驱动IC

产品 AP5193是一款PWM工作模式、外围简单、内置功率MOS管&#xff0c;适用于4.5-100V输入的高精度降压LED恒流驱动芯片。电流2.5A。AP5193可实现线性调光和PWM调光&#xff0c;线性调光脚有效电压范围0.55-2.6V.AP5193 工作频率可以通过RT 外部电阻编程来设定&#xff0c;同时…