【Spring Security】的RememberMe功能流程与源码详解

news2024/7/6 19:59:51

文章目录

    • 前言
      • 原理
    • 基础版
      • 搭建
        • 初始化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();
}

【阿Q说代码】后台回复“reme”获取项目源码。

验证

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

在这里插入图片描述

输入用户名密码登陆成功

在这里插入图片描述

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

源码分析

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

在这里插入图片描述

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

该流程如果不清楚的话记得复习《实战篇:Security+JWT组合拳 | 附源码》

当认证通过之后会在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")

后台回复“reme”获取源码吧!

本文到这里就结束了,看看我为了写这篇文章又日渐稀少的头发,我忍不住哭出声来。可能只有给我点赞,才能平复我的心情吧。

好看的皮囊千篇一律,有趣的灵魂万里挑一,让我们在冷漠的城市里相互温暖,我是阿Q,我们下期再见!

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

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

相关文章

Ada Tutorial(3)SPARK2——Post condition + Loop Invariant 后置条件 + 循环不变量

文章目录 divmod循环不变量 v.s. 后置条件扩展思考 divmod -- divmod.adb package body DivMod with SPARK_Mode isprocedure DivMod(X : Positive; N : Positive; K : out Natural; Remainder : out Natural)isY : Natural : X;beginK : 0;while Y > N loopY : Y - N;K : …

MySQL 日期与时间函数

一、获取日期、时间 函数用法CURDATE()&#xff0c;CURRENT_DATE()返回当前日期&#xff0c;只包含年、月、日CURTIME() &#xff0c; CURRENT_TIME()返回当前时间&#xff0c;只包含时、分、秒NOW() &#xff0c; SYSDATE()&#xff0c;CURRENT_TIMESTAMP() &#xff0c; LOC…

为什么企业和品牌一定要创建百度百科词条呢?

在企业和品牌宣传推广方面&#xff0c;百度百科作为一个权威的知识平台&#xff0c;早已成为了宣传阶段非常重要的一环。本文伯乐网络传媒将从以下几个方面探讨为什么企业和品牌一定要创建百度百科词条。 一、提升企业和品牌知名度 在信息时代&#xff0c;信息的获取渠道变得更…

PySpark简单使用(零)

介绍 Spark是Apache基金会旗下的顶级开源项目&#xff0c;用于对海量数据进行大规模分布式计算。PySpark是Spark的Python实现&#xff0c;是Spark为Python开发者提供的编程入口&#xff0c;用于以Python代码完成Spark任务的开发PySpark不仅可以作为Python第三方库使用&#xf…

【深入浅出 Spring Security(九)】解决跨域问题和 Axios 所需配置

跨域 一、SpringMVC 跨域的解决方案CrossOrigin&#xff08;注解的方式解决&#xff09;addCorsMappings&#xff08;实现WebMvcConfigurer接口&#xff0c;重写方法&#xff09; 二、Spring Security 跨域的解决方案前后端跨域测试&#xff08;前端相关配置&#xff09; 啥是跨…

计算机提示“找不到vcruntime140.dll,无法继续执行代码可”以这样子修复

首先&#xff0c;对于那些不熟悉的人来说&#xff0c;vcruntime140.dll是一个关键文件&#xff0c;用于在Windows操作系统上运行使用C语言编写的大型应用程序。如果你正在运行或安装这样的应用程序&#xff0c;但找不到vcruntime140.dll文件&#xff0c;那么你的应用程序可能无…

word插入图片为何无法显示?

word是我们常用的软件&#xff0c;在使用过程中&#xff0c;我们难免会遇到这样或那样的问题&#xff0c;解决问题的思路我认为首先要找出原因&#xff0c;然后再想办法解决才是最好的方案。 如果在 Word 文档中插入的图片无法显示&#xff0c;可能是以下原因之一&#xff1a;…

安装ORB-SLAM2纯记录(caffe)

ubuntu20.04 显卡配置3050TI 显卡驱动&#xff1a; NVIDIA-SMI 525.116.04 Driver Version: 525.116.04 cuda:Cuda compilation tools, release 11.0, V11.0.221 eigen:3.4.0 opencv 3.4.9 opencv下载链接 pangolin :高翔SLAM十四讲中的 没有安装最新版本 安装pangolin时候出…

有趣的图(二)(56)

小朋友们好&#xff0c;大朋友们好&#xff01; 我是猫妹&#xff0c;一名爱上Python编程的小学生。 和猫妹学Python&#xff0c;一起趣味学编程。 今日主题 咱们书接上回&#xff0c;上次学了图的基本概念&#xff0c;你都学会了吗&#xff1f; 咱们今天要学习内容如下&a…

通过Appium Desktop实现录制功能并导出自动化脚本

1、我们进入下面这个界面 图中红色标记1为 “top by coordinates” 按钮&#xff0c; 这是一种通过坐标定位元素的方式。 图中红色标记2为 “Start Recording” 按钮&#xff0c; 选中表示处理录制状态。 2、点击“top by coordinates” 按钮&#xff0c;再点击 “Start Recor…

vue3基础知识的简单应用

vue3基础知识的简单应用 vue3基础知识的简单应用运行结果 vue3基础知识的简单应用 父组件代码 <template><div class"home"><img alt"Vue logo" src"../assets/logo.png"><HelloWorld msg"Welcome to Your Vue.js A…

linux opencv4.2静态库编译及链接

1. 编译静态库 拉取opencv源码&#xff1a; https://github.com/opencv/opencv.git进入源码根目录&#xff0c;在platforms/linux目录下创建编译脚本build_linux.sh cd platforms/linux vi build_linux.sh脚本里配置cmake编译参数&#xff0c;这里编译静态库需要将 -DBUILD_S…

华为OD机试真题 JavaScript 实现【勾股数元组】【2022Q4 100分】,附详细解题思路

一、题目描述 如果三个正整数A、B、C ,ABC则为勾股数 如果ABC之间两两互质&#xff0c;即A与B&#xff0c;A与C&#xff0c;B与C均互质没有公约数&#xff0c; 则称 其为勾股数元组。 请求出给定n~m范围内所有的勾股数元组。 二、输入描述 起始范围 1 < n < 10000 n &…

[数据结构初阶]单链表

顾名思义&#xff0c;单链表就是一个相邻节点用一个单向指针串起来&#xff0c;形成一种链式结构&#xff0c;那怎么将这些节点连结起来方便管理呢&#xff1f; 目录 单链表定义 申请空间 创建节点 打印链表 尾插 尾删 头插 头删 查找 插入 删除 pos后删除 pos位置删除…

Python 接口测试之接口关键字封装

引言 我们使用RF做UI自动化测试的时候&#xff0c;使用的是关键字驱动。同样&#xff0c;Python做接口自动化测试的时候&#xff0c;也可以使用关键字驱动。但是这里并不是叫关键字驱动&#xff0c;而是叫数据驱动。而接口测试的关键字是什么呢&#xff1f; 我们数据驱动的载体…

知识图谱实战应用15-知识图谱在生物基因学上的应用,实现基因与疾病关联查询

大家好,我是微学AI,今天给大家介绍一下知识图谱实战应用15-知识图谱在生物基因学上的应用,实现基因与疾病关联查询。知识图谱在生物基因学中的应用非常广泛,可以帮助研究人员更好地理解和发现基因与疾病之间的关联关系,并推进相关领域的发展。 目录 引言知识图谱简介生物…

Linux之命令别名---alias

Linux之命令别名 定义 别名是命令的快捷方式。为那些需要经常执行&#xff0c;但需要很长时间输入的长命令创建快捷方式很有用 语法格式 alias 别名’原命令 -选项/参数’ 基本用法 查看设置的别名 [redhat8alocalhost ~]$ alias 删除别名 [redhat8alocalhost ~]$ unalia…

你见过的这里都有,开源Web性能测试工具集合

1、JMeter。这个也不用多介绍&#xff0c;基本属于做过Web性能测试都会接触的工具。Apache旗下的开源项目&#xff0c;属于一个Java桌面应用程序。优势是开源免费&#xff0c;扩展能力强。自身性能是硬伤。 2、Locust。Web性能测试的蝗药师&#xff0c;开源免费&#xff0c;并发…

C语言解决任意一个整数分解,逆序输出

C语言解决任意一个整数分解,逆序输出 输入&#xff1a;1234 输出&#xff1a;1 2 3 4 思路&#xff1a; 对于任意一个整数num, &#xff08;1&#xff09;num%10就可以得到其个位数的数字&#xff0c;例如&#xff1a;1234%104 &#xff08;2&#xff09;num/10就可以获取到这个…

【SQL】sqladvisor

文章目录 概述架构流程产品特点安装部署使用帮助输出命令行传参调用配置文件传参调用测试一:对小表进行测试测试二&#xff1a;对大表有索引测试测试三&#xff1a;对大表无索引进行测试测试四&#xff1a;多条SQL同时分析&#xff1a; 来源 概述 SQLAdvisor是由美团点评公司技…