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

文章目录

    • 前言
      • 原理
    • 基础版
      • 搭建
        • 初始化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,我们下期再见!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MIT 6.S081 Lab Four

MIT 6.S081 Lab Four 引言trapsRISC-V assembly (easy)代码解析 Backtrace(moderate)代码解析 Alarm(Hard)test0: invoke handler(调用处理程序)test1/test2(): resume interrupted code(恢复被中断的代码)代码解析issue解答 可选的挑战练习 引言 本文为 MIT 6.S081 2020 操作…

JDBC BasicDAO详解(通俗易懂)

目录 一、前言 二、BasicDAO的引入 1.为什么需要BasicDAO&#xff1f; 2.BasicDAO示意图 : 三、BasicDAO的分析 1.基本说明 : 2.简单设计 : 四、BasicDAO的实现 0.准备工作 : 1.工具类 : 2.JavaBean类 : 3.BasicDAO类 / StusDAO类 : 4.测试类 : 一、前言 第七节内容…

Jenkins+Docker 实现一键自动化部署项目!步骤齐全,少走坑路

大家好&#xff0c;我是互联网架构师&#xff01; 本文章实现最简单全面的Jenkinsdockerspringboot 一键自动部署项目&#xff0c;步骤齐全&#xff0c;少走坑路。 环境&#xff1a;centos7git(gitee) 简述实现步骤&#xff1a;在docker安装jenkins&#xff0c;配置jenkins基…

数据结构:二叉树经典例题(单选题)-->你真的掌握二叉树了吗?(第一弹)

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下有关二叉树的经典例题&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通 数…

Sentinel的限流和Gateway的限流差别?

Sentinel的限流与Gateway的限流有什么差别&#xff1f; 问题说明&#xff1a;考察对限流算法的掌握情况 限流算法常见的有三种实现&#xff1a;滑动时间窗口&#xff0c;令牌桶算法&#xff0c;漏桶算法。gateway则采用基于Redis实现的令牌桶算法。但是我们不会去用&#xff…

ubuntu常用命令

设置root密码 安装好ubuntu后谁也不知道root密码是多少&#xff0c;可以借助于passwd命令来设置root密码。 sudo passwd root 同理修改其他用户只需替换上方用户名即可 换源 备份原始文件 sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak 修改源文件 sudo vim /et…

数据链路层(MAC)、网络层(IP)、传输层(TCP/UDP)抓包分析

目录 OSI七层模型数据包逐层封装头部抓包分析数据包概况数据链路层抓包网络层抓包&#xff08;IP协议抓包&#xff09;UDP抓包数据负载抓包 Linux cooked-mode capture OSI七层模型 OSI模型&#xff08;OSI model&#xff09;&#xff0c;开放式系统互联通信参考模型&#xff…

台电x80HD 安装linux系统,可调电压电源供电,外网访问、3D打印klipper固件

一、系统安装 参照https://blog.csdn.net/gangtieren/article/details/102975027安装 安装过程遇到的问题&#xff1a; 1、试了 linux mint 21 、ubuntu20.04 、ubuntu22.04 都没有直接安装成功&#xff0c;u盘选择安装进入系统后一直黑屏&#xff0c;只有ubuntu18.04 选择后稍…

ChatGPT读PDF、生成思维导图的几种方案

大家好&#xff0c;我是可夫小子&#xff0c;《小白玩转ChatGPT》专栏作者&#xff0c;关注AIGC、读书和自媒体。 日常办公&#xff0c;我们离不开pdf文档读取&#xff0c;思维导图制作&#xff0c;那么ChatGPT能够给我们什么帮助呢&#xff1f; 通常的方法是&#xff1a;我们…

【大数据】大数据相关概念

文章目录 大数据&#xff1a;一种规模大到在获取、存储、管理、分析方面大大超出了传统数据库软件工具能力范围的数据集合&#xff0c;具有海量的数据规模、快速的数据流转、多样的数据类型以及价值密度四大特征。Hadoop&#xff1a;是一个能够对大量数据进行分布式处理的软件框…

华硕天选2ubuntu18.04升级内核后黑屏

https://piaoyun.cc/post/26957.html 1.开机&#xff0c;进入grub画面 2.按’‘‘e’’’ 进入编辑开机指令的模式,同样找到’‘‘quite splash’’,并在后面加上对应的字。 1.Intel 82852/82855 或8系列显示晶片&#xff1a;i915.modeset1或i915.modeset0 2.Nvidia&#xff…

Pytest教程__钩子方法setup、teardown、setup_class、teardown_class(8)

pytest跳过用例执行的用法与unittest跳过用例大致相同。 pytest跳过用例的方法如下&#xff1a; pytest.mark.skip(reason)&#xff1a;无条件用例。reason是跳过原因&#xff0c;下同。pytest.mark.skipIf(condition, reason)&#xff1a;condition为True时跳过用例。 pyte…

设计模式之单例模式

一.单例模式 1.1 定义 我们来解释一下什么是单例模式.在软件系统中有很多对象,他们在同一时刻只能被一个用户或者多个线程访问,如果被共享的word文档在同一时间内,只能由一个用户对其进行写操作. 换一种说法就是 在单例模式中&#xff0c;类自身负责创建自己的唯一实例&#…

从零开始 Spring Boot 47:缓存

从零开始 Spring Boot 47&#xff1a;缓存 图源&#xff1a;简书 (jianshu.com) Spring 提供一个简单但使用的缓存&#xff08;Cache&#xff09;机制&#xff0c;我们可以利用它来优化代码执行效率。 简单示例 老规矩&#xff0c;我们从一个简单示例开始&#xff1a; Serv…

夜不收见证:夫妻从内江到成都,从真诚到真相

他们从四川内江的一条小巷&#xff0c;走进了成都的大街小巷。那里的房屋挨挨挤挤&#xff0c;像是在讲述他们曾经的梦想和勇气。他们是那些在内江的土地上种下了友情种子的少年&#xff0c;他们在成都的大地上&#xff0c;硕果累累。 他们从初中的课桌前走到了成人的世界里&am…

特征选择:过滤法,嵌入法,包装法

特征选择时首先要去除冗余特征。 它是由其他其他的特征中推演出来的。比如&#xff0c;一个球的体积&#xff0c;那么半径这个特征就是冗余的&#xff0c;因为我们可以由球的体积推算半径。冗余特征在很多时候都是不起作用的 过滤法 过滤方法通常用作预处理步骤&#xff0c;特…

6、DuiLib控件消息响应处理

文章目录 1、DuiLib控件消息响应处理2、基本的消息响应处理 Notify3、仿 MFC 形式消息响应 DUI_DECLARE_MESSAGE_MAP4、事件委托 MakeDelegate5、消息捕获&#xff08;拦截&#xff09;原生消息 HandleMessage 1、DuiLib控件消息响应处理 <?xml version"1.0" en…

PromptBench:大型语言模型的对抗性基准测试

PromptBench是微软研究人员设计的一个用于测量大型语言模型(llm)对对抗性提示鲁棒性的基准测试。这个的工具是理解LLM的重要一步&#xff0c;随着这些模型在各种应用中越来越普遍&#xff0c;这个主题也变得越来越重要。 研究及其方法论 PromptBench采用多种对抗性文本攻击&am…

前端后端交互-ElementUI(日期选择器)

日期选择器 页面效果 页面效果 组件源码 <!-- daterange: 范围选择类型format: 绑定后表单中显示的格式value-format: 传递时显示的格式--> <template><el-date-picker v-model"rangeTime" type"daterange" range-separator"至" …

吴恩达471机器学习入门课程3第1周——K-means

K-means 聚类 1 - 实现 K-means1.1 找到最近的质心练习11.2 计算质心均值练习2 2 - K-means在样本数据集上的应用3 - 随机初始化4 - K-means图像压缩4.1 数据集可视化处理数据 4.2图像像素上的 K-mean4.3 压缩图片 实现 K-means 算法&#xff0c;并将其用于图像压缩。 您将从一…