springboot整合Sa-Token实现登录认证和权限校验(万字长文)

目前在国内的后端开发中,常用的安全框架有spring security、shiro。现在,介绍一款由国人开发的安全框架Sa-Token。这个框架完全由国人开发,所提供的Api文档和一些设置都是比较符合国人的开发习惯的,本次就来介绍一下如何在spring boot框架中整合Sa-Token框架,以实现我们最常使用的登录认证和权限校验;

Sa-Token的官网地址如下:
Sa-Token

Sa-Token 是由国人开发的 一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0分布式Session会话微服务网关鉴权 等一系列权限相关问题。

版本:spring boot3.15、Sa-Token1.37.0

1、新建一个spring boot项目,并导入一些起始的依赖:
 

<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.32</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.18</version></dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId><version>4.3.0</version></dependency><!-- Sa-Token 权限认证,在线文档:https://sa-token.cc --><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot3-starter</artifactId><version>1.37.0</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>

2、新建五张数据表,来展示登录认证和权限校验:


1、用户表

CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY,username VARCHAR(50) NOT NULL UNIQUE,password VARCHAR(100) NOT NULL,email VARCHAR(100) NOT NULL,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

2、角色表

CREATE TABLE roles (id INT AUTO_INCREMENT PRIMARY KEY,name VARCHAR(50) NOT NULL UNIQUE,description VARCHAR(255),created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

3、权限表

CREATE TABLE permissions (id INT AUTO_INCREMENT PRIMARY KEY,name VARCHAR(50) NOT NULL UNIQUE,description VARCHAR(255),created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

4、用户角色表

CREATE TABLE user_roles (id INT AUTO_INCREMENT PRIMARY KEY,user_id INT NOT NULL,role_id INT NOT NULL,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (user_id) REFERENCES users(id),FOREIGN KEY (role_id) REFERENCES roles(id)
);

5、角色权限表

CREATE TABLE role_permissions (id INT AUTO_INCREMENT PRIMARY KEY,role_id INT NOT NULL,permission_id INT NOT NULL,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (role_id) REFERENCES roles(id),FOREIGN KEY (permission_id) REFERENCES permissions(id)
);

3、现在我们开始进行基于Sa-Token进行的登录校验和权限认证:
 

与spring security不同的是,引入了Sa-Token的依赖之后。并不会自动生效,需要我们自己编写相应的逻辑代码;

写一个TestController,用来进行测试;

@RestController
@RequestMapping("/test")
public class TestController {@GetMapping("/hello")public String hello() {System.out.println("说hello啊");return "test接口的hello";}}

我们现在启动项目,并访问/test/hello接口,发现能够正常访问。这一点与spring security是不同的,spring security默认会拦截所有请求;

Sa-Token已经为我们编写了很多我们经常使用的功能,我们只需要在yml配置文件中进行配置即可。以下是一些常用的配置:
 

############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token: # token 名称(同时也是 cookie 名称)token-name: satoken# token 有效期(单位:秒) 默认30天,-1 代表永久有效timeout: 2592000# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结active-timeout: -1# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)is-concurrent: true# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)is-share: true# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)token-style: uuid# 是否输出操作日志 is-log: true

3.1、登录认证:

使用Sa-Token进行登录认证是非常简单的,只需要一句代码就可以搞定:

// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
StpUtil.login(Object id);     

只此一句代码,便可以使会话登录成功,实际上,Sa-Token 在背后做了大量的工作,包括但不限于:

  1. 检查此账号是否之前已有登录;
  2. 为账号生成 Token 凭证与 Session 会话;
  3. 记录 Token 活跃时间;
  4. 通知全局侦听器,xx 账号登录成功;
  5. 将 Token 注入到请求上下文;
  6. 等等其它工作……

我们暂时不需要完整了解整个登录过程,只需要记住关键一点:Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端

一个登录接口:

@AutowiredIUsersService usersService;@PostMapping("/login")
public SaResult login(String username, String password){
//根据用户名从数据库中查询Users users = usersService.getOne(new LambdaQueryWrapper<Users>().eq(Users::getUsername, username));if(users == null){return SaResult.error("用户名不存在");}if(!users.getPassword().equals(password)){return SaResult.error("密码错误");}
//根据用户id登录StpUtil.login(users.getId());return SaResult.ok("登录成功");}

运行检验:

发现确实能够登录成功。

此处仅仅做了会话登录,但并没有主动向前端返回 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 已经过期 …… 等等

还有一些登录过程中常用的方法:

// 获取当前会话账号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 值
StpUtil.getTokenValue();// 获取当前`StpLogic`的 token 名称
StpUtil.getTokenName();// 获取指定 token 对应的账号id,如果未登录,则返回 null
StpUtil.getLoginIdByToken(String tokenValue);// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
StpUtil.getTokenTimeout();// 获取当前会话的 token 信息参数
StpUtil.getTokenInfo();
3.1.1前后端分离模式下的登录(无cookie模式)

无 Cookie 模式:特指不支持 Cookie 功能的终端,通俗来讲就是我们常说的 —— 前后端分离模式

常规 Web 端鉴权方法,一般由 Cookie模式 完成,而 Cookie 有两个特性:

  1. 可由后端控制写入。
  2. 每次请求自动提交。

这就使得我们在前端代码中,无需任何特殊操作,就能完成鉴权的全部流程(因为整个流程都是后端控制完成的)

而在app、小程序等前后端分离场景中,一般是没有 Cookie 这一功能的,此时大多数人都会一脸懵逼,咋进行鉴权啊?

见招拆招,其实答案很简单:

  • 不能后端控制写入了,就前端自己写入。(后端将登录成功之后生成的Token 传递到前端
  • 每次请求不能自动提交了,那就手动提交。(前端将登陆成功后获取的Token封装到请求头中每次请求时都携带后端将其读取出来,并判断用户信息

修改我们之前写的登录接口,返回token给前端;

 @PostMapping("/login")
public SaResult login(String username, String password){
//根据用户名从数据库中查询Users users = usersService.getOne(new LambdaQueryWrapper<Users>().eq(Users::getUsername, username));if(users == null){return SaResult.error("用户名不存在");}if(!users.getPassword().equals(password)){return SaResult.error("密码错误");}
//根据用户id登录,第1步,先登录上StpUtil.login(users.getId());// 第2步,获取 Token  相关参数 SaTokenInfo tokenInfo = StpUtil.getTokenInfo();// 第3步,返回给前端 return SaResult.data(tokenInfo);}

在进行一次登录:

可以看到登录成功之后,返回了很多相关的数据,而我们的token也在其中,那么前端就可以将token取出来放到一个状态管理器(pinia中),并且在每次请求时都将token放在请求头中。

如果你对 Cookie 非常了解,那你就会明白,所谓 Cookie ,本质上就是一个特殊的header参数而已, 而既然它只是一个 header 参数,我们就能手动模拟实现它,从而完成鉴权操作。

3.1.2路由拦截鉴权:

假设我们有如下需求:

项目中所有接口均需要登录认证,只有 “登录接口” 本身对外开放

我们怎么实现呢?给每个接口加上鉴权注解?手写全局拦截器?似乎都不是非常方便。

在这个需求中我们真正需要的是一种基于路由拦截的鉴权模式,那么在Sa-Token怎么实现路由拦截鉴权呢?

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {// 注册拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())).addPathPatterns("/**").excludePathPatterns("/users/login");}}

以上代码,我们注册了一个基于 StpUtil.checkLogin() 的登录校验拦截器,并且排除了/user/login接口用来开放登录(除了/user/login以外的所有接口都需要登录才能访问)这就与我们spring security有了一点点相似了,因为spring security默认的就是所有的接口都被拦截到。

我们再启动项目,并访问我们最开始写的/test/hello接口;

可以看到这个接口被拦截了。那么我们带上之前登录成功,生成的token之后再去访问呢?

可以看到我们成功访问了这个接口;

new SaInterceptor(handle -> StpUtil.checkLogin()) 是最简单的写法,代表只进行登录校验功能。实际上Sa-Token还可以实现一些更详细的校验规则,在Sa-Token的官网上可以看到。

3.2权限认证:

我们已经了解了常用的登录认证方法,接下来,我们来看一下权限认证;

1、获取当前用户的权限码集合:

因为每个项目的需求不同,其权限设计也千变万化,因此 [ 获取当前账号权限码集合 ] 这一操作不可能内置到框架中, 所以 Sa-Token 将此操作以接口的方式暴露给你,以方便你根据自己的业务逻辑进行重写。

你需要做的就是新建一个类,实现 StpInterface接口

/*** 自定义权限加载接口实现类*/
@Component    // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {/*** 返回一个账号所拥有的权限码集合 *///    角色权限表@AutowiredIRolePermissionsService rolePermissionsService;
//    用户角色表@AutowiredIUserRolesService userRolesService;
//权限表@AutowiredIPermissionsService permissionsService;//    角色表@AutowiredIRolesService rolesService;@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {
//        根据用户id从用户角色表中获取角色idList<UserRoles> roleIds = userRolesService.list(new LambdaQueryWrapper<UserRoles>().eq(UserRoles::getUserId,Integer.parseInt(loginId.toString())));List<Integer> rolesList = roleIds.stream().map(UserRoles::getRoleId).toList();
if (!(rolesList.size() >0)){
//    没有任何权限return null;
}List<String> list = new ArrayList<>();rolesList.forEach(roleId ->{// 根据角色id从角色权限表中获取权限id   List<RolePermissions> rolePermissions = rolePermissionsService.list(new LambdaQueryWrapper<RolePermissions>().eq(RolePermissions::getRoleId, roleId));// 根据权限id从权限表中获取权限名称rolePermissions.forEach(permissionsId->{Permissions permissions = permissionsService.getById(permissionsId.getPermissionId());list.add(permissions.getName());});});// 返回用户所有权限,return list;}/*** 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)*/@Overridepublic List<String> getRoleList(Object loginId, String loginType) {// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色//        根据用户id从用户角色表中获取角色idList<UserRoles> roleIds = userRolesService.list(new LambdaQueryWrapper<UserRoles>().eq(UserRoles::getUserId,Integer.parseInt(loginId.toString())));List<String> list = new ArrayList<>();if (!(roleIds.size() >0)){
//            用户没有分配角色return null;}roleIds.forEach(roleId ->{Roles byId = rolesService.getById(roleId.getRoleId());list.add(byId.getName());});
//        返回用户所有角色标识集合return list;}}

我所实现的是标准的RBAC(基于用户、角色、权限的访问控制模型)。所以,在得到用户id的情况下、先根据用户角色表查出角色id、在根据角色权限表查询权限id,在根据权限表查出具体权限名称。

上面使用了Mybatis-plus的条件构造器和stream流的形式进行查询。

参数解释:

  • loginId:账号id,即你在调用 StpUtil.login(id) 时写入的标识值。
  • loginType:账号体系标识,此处可以暂时忽略,在 [ 多账户认证 ] 章节下会对这个概念做详细的解释。

有同学会产生疑问:我实现了此接口,但是程序启动时好像并没有执行,是不是我写错了? 答:不执行是正常现象,程序启动时不会执行这个接口的方法,在每次调用鉴权代码时,才会执行到此。

权限校验:
然后就可以用以下 api 来鉴权了:

// 获取:当前账号所拥有的权限集合
StpUtil.getPermissionList();// 判断:当前账号是否含有指定权限, 返回 true 或 false
StpUtil.hasPermission("user.add");        // 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException 
StpUtil.checkPermission("user.add");        // 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get");        // 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");    

扩展:NotPermissionException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常

角色校验:
在 Sa-Token 中,角色和权限可以分开独立验证

// 获取:当前账号所拥有的角色集合
StpUtil.getRoleList();// 判断:当前账号是否拥有指定角色, 返回 true 或 false
StpUtil.hasRole("super-admin");        // 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
StpUtil.checkRole("super-admin");        // 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
StpUtil.checkRoleAnd("super-admin", "shop-admin");        // 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] 
StpUtil.checkRoleOr("super-admin", "shop-admin");        

扩展:NotRoleException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常

权限通配符:
Sa-Token允许你根据通配符指定泛权限,例如当一个账号拥有art.*的权限时,art.addart.deleteart.update都将匹配通过

// 当拥有 art.* 权限时
StpUtil.hasPermission("art.add");        // true
StpUtil.hasPermission("art.update");     // true
StpUtil.hasPermission("goods.add");      // false// 当拥有 *.delete 权限时
StpUtil.hasPermission("art.delete");      // true
StpUtil.hasPermission("user.delete");     // true
StpUtil.hasPermission("user.update");     // false// 当拥有 *.js 权限时
StpUtil.hasPermission("index.js");        // true
StpUtil.hasPermission("index.css");       // false
StpUtil.hasPermission("index.html");      // false

上帝权限:当一个账号拥有 "*" 权限时,他可以验证通过任何权限码 (角色认证同理)

在我们的项目的TestController中进行测试:
 

  @GetMapping("/hello")public String hello() {System.out.println("验证权限,是否具有‘所有权限’===============>"+StpUtil.hasPermission("所有权限"));System.out.println("验证角色,是否具有‘管理员’角色===============>"+StpUtil.hasRole("管理员"));  System.out.println("说hello啊");return "test接口的hello";}

我们测试的最终结果:
 

可以看到确实输出了两个true。并且,只有在每次调用鉴权代码时,才会执行到我们自定义的权限方法中。

总结:

我本来是想写一篇介绍spring boot项目中整合Sa-Token来实现最常用的登录校验和权限认证的,但是写着写着就变成官网的复制机了。我在本篇文章中大量复制了官网上的内容,原本只是想复制一些官方介绍就行了。但是这也从侧面说明了Sa-Token官网制作的确实是比较好的,基本上不需要额外的学习,只要你有做过登录和权限方面的项目经验,再看一遍官网的介绍就能直接上手了。

我之前写过一个B2C模式的购物商台,分为用户端和管理端。管理端的登录和权限校验是用spring security写的。现在又了解了Sa-Token之后,真的还是认为Sa-Token的使用是比较简单的。(也可能是我的项目还是比较简单的,并没有多少权限要实现)并且这也是我们国人写的开源框架,在一些方法的封装、Api的设计上还是能直接理解的。在这里也算是为Sa-Token这个框架打个广告吧,希望大家还是能够多多支持国产,国内的开源之路也是充满了坎坷与艰辛。

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

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

相关文章

Multisim14.0仿真(四十五)AC220V转DC12V稳压电源设计

一、仿真原理图&#xff1a; 二、运行效果&#xff1a;

【Vue】组件间通信的7种方法(全)

目录 组件之前的通信方法 1. props/$emit 2.parent/children 3.ref 4.v-model 5.sync 6.attrs,attrs,attrs,listeners 7.provide/inject 7.eventBus 组件之前的通信方法 1. props/$emit 父传子 props 这个只能够接收父组件传来的数据 不能进行修改 可以静态传递 也可…

机器学习系列——(九)决策树

简介 决策树作为机器学习的一种经典算法&#xff0c;在数据挖掘、分类和回归等任务中广泛应用。本文将详细介绍机器学习中的决策树算法&#xff0c;包括其原理、构建过程和应用场景。 原理 决策树是一种基于树状结构的监督学习算法&#xff0c;它通过构建一棵树来对数据进行分…

【Rust】——基础Hello_world

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

DBeaver添加阿里maven镜像

1、点击数据库->驱动管理器 2、选择任意数据库&#xff0c;点击编辑按钮 3、点击下载/更新(D) 4、点击下载配置 5、点击添加 6、添加阿里云地址 http://maven.aliyun.com/nexus/content/groups/public/ 7、将阿里云地址移动到首位并点击"应用并关闭"

笔记---容斥原理

AcWing,890.能被整除的数 给定一个整数 n n n 和 m m m 个不同的质数 p 1 , p 2 , … , p m p_{1},p_{2},…,p_{m} p1​,p2​,…,pm​。 请你求出 1 ∼ n 1∼n 1∼n 中能被 p 1 , p 2 , … , p m p_{1},p_{2},…,p_{m} p1​,p2​,…,pm​ 中的至少一个数整除的整数有多少…

【项目日记(八)】第三层: 页缓存的具体实现(下)

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:项目日记-高并发内存池⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你做项目   &#x1f51d;&#x1f51d; 开发环境: Visual Studio 2022 项目日…

LangChain 81 LangGraph 从入门到精通三

LangChain系列文章 LangChain 60 深入理解LangChain 表达式语言23 multiple chains链透传参数 LangChain Expression Language (LCEL)LangChain 61 深入理解LangChain 表达式语言24 multiple chains链透传参数 LangChain Expression Language (LCEL)LangChain 62 深入理解Lang…

低版本MATLAB打开高版本Simulink文件的方法

打开simulink&#xff0c;依次点击“建模”、“环境”、“simulink预设项”&#xff0c;如图所示&#xff1a; 然后在弹出的窗口中&#xff0c;点击“模型文件”&#xff0c;并取消勾选“不要加载用更新版本的simulink创建的模型”&#xff0c;接着点击“应用”即可。如图所示&…

EAK厚膜功率电阻成功在eVTOL大量使用

eVTOL操作的特点是更高的放电曲线&#xff0c;特别是在起飞和着陆期间。 “传统上&#xff0c;电池要么被设计成提供大量能量&#xff0c;要么被设计成高功率&#xff0c;”Cuberg创始人兼首席执行官Richard Wang说。“对于eVTOL电池来说&#xff0c;在能量和功率之间保持良好…

情人节适合送哪些礼物?2024年情人节送礼指南大推荐!

情人节即将来临&#xff0c;这是一年一度表达爱意的时刻。在这个特殊的日子里&#xff0c;送上一份精心挑选的礼物&#xff0c;将会让爱意更加深刻。但是&#xff0c;肯定会有朋友会困惑于选择哪种礼物能够最好地表达您的心意。不用担心&#xff0c;今天小编就为大家精心准备了…

JDK17中的密封类sealed和permits使用指南:什么是Java中的sealed和permits?

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

FPGA项目(16)——基于FPGA的音乐演奏电路

1.设计要求 能在实验箱上&#xff0c;循环播放一段音乐。&#xff08;需要源码的直接看最后一节&#xff09; 2.设计原理 组成乐曲的每个音符的发音频率值及其持续的时间是乐曲能连续演奏所需要的两个基本要素&#xff0c;问题是如何来获取这两个要素所对应的数值以及通过纯硬件…

界面控件DevExpress ASP.NET Spreadsheet组件 - 轻松集成电子表格功能!(一)

DevExpress ASP. NET Spreadsheet组件允许您轻松地将电子表格功能合并到任意ASP. NET应用程序&#xff0c;它可以加载、转换和保存工作簿到XLS-XLSx二进制文件格式&#xff0c;还可以导出和导入XLSX、CSV和TXT文件。 P.S&#xff1a;DevExpress ASP.NET Web Forms Controls拥有…

课时13:变量基础_变量场景

2.1.1 变量场景 学习目标 这一节&#xff0c; 我们从 数据存储、变量场景、小结 三个方面来学习。 数据存储 数据存储 所谓的数据存储&#xff0c;我们从三方面来理解这句话&#xff1a;1、数据保存到哪里 -- 各种媒介&#xff0c;CPU、内存、磁盘、磁带、网盘...2、数据保…

06:原生云K8S解密|K8S集群安装部署|K8S网络插件

原生云K8S解密&#xff5c;K8S集群安装部署&#xff5c;K8S网络插件 K8SK8S集群架构图解 K8S部署仓库初始化kube-master安装计算节点的安装token管理 配置flannel网络&#xff08;master主机操作&#xff09; K8S 有大量夸主机的容器需要管理&#xff0c;快速部署应用&#xff…

河西走廊潜在蒸散发时空格局变化与气象因素的关系_马亚丽_2023

河西走廊潜在蒸散发时空格局变化与气象因素的关系_马亚丽_2023 摘要关键词 1 数据与方法1.1 数据来源1.2 变化趋势分析1.3 定性分析方法1.3.1 主成分分析1.3.2 相关系数1.3.3 通径分析 1.4 定量分析方法1.4.1 敏感系数1.4.2 贡献率计算 2 结果与分析2.1 ET0多年变化特征2.1.1 E…

Python绘图工具seaborn,教会你如何绘制更加精美的图形(二)

文章目录 用分类数据绘图1 类别散点图2 类别内的数据分布2.1 绘制箱形图2.2 绘制提琴图 3 类别内的统计估计3.1 绘制条形图3.2 绘制点图 Hello&#xff0c;大家好&#xff0c;我是景天&#xff0c;今天我们探讨下seaborn根据分类数据类绘图的方法 用分类数据绘图 数据集中的数…

【前端模板】bootstrap5实现高端手表网页Chrono(适用电商,附带源码)

一、需求分析 高端手表网页是指专门销售高端手表的在线平台或网站。这些网页旨在向消费者展示和销售高级手表品牌的产品。以下是一些常见的功能&#xff1a; 产品展示&#xff1a;高端手表网页提供详细的产品页面&#xff0c;展示不同品牌和型号的高级手表。这些页面通常包括产…

Vim工具使用全攻略:从入门到精通

引言 在软件开发的世界里&#xff0c;Vim不仅仅是一个文本编辑器&#xff0c;它是一个让你的编程效率倍增的神器。然而&#xff0c;对于新手来说&#xff0c;Vim的学习曲线似乎有些陡峭。本文将手把手教你如何从Vim的新手逐渐变为高手&#xff0c;深入理解Vim的操作模式&#…