【SaToken】 简化开发的身份认证与权限管理框架

news/2025/1/25 2:43:02/文章来源:https://www.cnblogs.com/o-O-oO/p/18689160

之前进行鉴权、授权都要写一大堆代码。如果使用像Spring Security这样的框架,又要花好多时间学习,拿过来一用,好多配置项也不知道是干嘛用的,又不想了解。要是不用Spring Security,token的生成、校验、刷新,权限的验证分配,又全要自己写,想想都头大。

Spring Security太重而且配置繁琐。自己实现所有的点必须又要顾及到,更是麻烦。

最近看到一个权限认证框架,真是够简单高效。这里分享一个使用Sa-Token的gateway鉴权demo。

官网:https://sa-token.cc/

需求分析

认证

sa-token模块

我们首先编写sa-token模块进行token生成和权限分配。

在sa-token的session模式下生成token非常方便,只需要调用:

StpUtil.login(Object id); 

就可以为账号生成 Token凭证与 Session会话了。

🎈 1、配置信息

server:# 端口port: 8081spring:application:name: weishuang-accountdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/weishuang_account?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULLusername: rootpassword: root
# redis配置redis:# Redis数据库索引(默认为0)database: 0# Redis服务器地址host: 127.0.0.1# Redis服务器连接端口port: 6379# Redis服务器连接密码(默认为空)# password:# 连接超时时间timeout: 10slettuce:pool:# 连接池最大连接数max-active: 200# 连接池最大阻塞等待时间(使用负值表示没有限制)max-wait: -1ms# 连接池中的最大空闲连接max-idle: 10# 连接池中的最小空闲连接min-idle: 0############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token名称 (同时也是cookie名称)token-name: weishuang-token
# token有效期,单位s 默认30天, -1代表永不过期timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒activity-timeout: -1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)is-share: true
# token风格token-style: uuid
# 是否输出操作日志is-log: false
# token前缀token-prefix: Bearer

在sa-token的配置中,我使用了token-name来指定token的名称,如果不指定那么就是默认的satoken。 使用token-prefix来指定token的前缀,这样前端在header里传入token的时候就要加上Bearer了(注意有个空格),建议和前端商量一下需不需要这个前缀,如果不使用,直接传token就好了。 现在调用接口时传入的格式就是:

weishuang-token = Bearer token123456

sa-token的session模式需要redis来存储session,在微服务中,各个服务的session也需要redis来同步。 当然sa-token也支持jwt来生成无状态的token,这样就不需要在服务中引入redis了。本文使用session模式(jwt的刷新token等机制还要自己实现,session的刷新sa-token都帮我们做好了,使用默认的模式更加方便,而且功能更多)
🎈 2、登录接口

【User】

@Data
public class User {/*** id*/private String id;/*** 账号*/private String userName;/*** 密码*/private String password;}

==【UserController】=:

@RestController
@RequestMapping("/account/user/")
public class UserController {@Autowiredprivate UserManager userManager;@PostMapping("doLogin")public SaResult doLogin(@RequestBody AccountUserLoginDTO req) {userManager.login(req);return SaResult.ok("登录成功");}
}

【UserManager】

@Component
public class UserManagerImpl implements UserManager {@Autowiredprivate UserService userService;@Overridepublic void login(AccountUserLoginDTO req) {//生成密码String password = PasswordUtil.generatePassword(req.getPassword());//调用数据库校验是否存在用户User user = userService.getOne(req.getUserName(), password);if (user == null) {throw new RuntimeException("账号或密码错误");}//为账号生成Token凭证与Session会话StpUtil.login(user.getId());//为该用户的session存储更多信息//这里为了方便直接把user实体存进去了,也包括了密码,自己实现时不建议这样做。StpUtil.getSession().set("USER_DATA", user);}
}

【UserService】

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Autowiredprivate UserMapper userMapper;public User getOne(String username, String password){LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUserName,username).eq(User::getPassword,password);return userMapper.selectOne(queryWrapper);}
}

gateway模块

<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId><scope>provided</scope></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency><!-- 引入gateway网关 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- Sa-Token 权限认证(Reactor响应式集成), 在线文档:https://sa-token.cc --><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-reactor-spring-boot-starter</artifactId><version>1.34.0</version></dependency><!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) --><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-dao-redis-jackson</artifactId><version>1.34.0</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency></dependencies>

【配置】

server:port: 9000
spring:application:name: weishuang-gatewaycloud:loadbalancer:ribbon:enabled: falsenacos:discovery:username: nacospassword: nacosserver-addr: localhost:8848gateway:routes:- id: accounturi: lb://weishuang-accountorder: 1predicates:- Path=/account/**
# redis配置redis:# Redis数据库索引(默认为0)database: 0# Redis服务器地址host: 127.0.0.1# Redis服务器连接端口port: 6379# Redis服务器连接密码(默认为空)# password:# 连接超时时间timeout: 10slettuce:pool:# 连接池最大连接数max-active: 200# 连接池最大阻塞等待时间(使用负值表示没有限制)max-wait: -1ms# 连接池中的最大空闲连接max-idle: 10# 连接池中的最小空闲连接min-idle: 0
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token名称 (同时也是cookie名称)token-name: weishuang-token
# token有效期,单位s 默认30天, -1代表永不过期timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒activity-timeout: -1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)is-share: true
# token风格token-style: uuid
# 是否输出操作日志is-log: false
# token前缀token-prefix: Bearer

同样的,在gateway中也需要配置sa-token和redis,注意和在account服务中配置的要一致,否则在redis中获取信息的时候找不到。

gateway我们也注册到nacos中。
【拦截认证】

package com.weishuang.gateway.gateway.config;import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;@Configuration
public class SaTokenConfigure {// 注册 Sa-Token全局过滤器@Beanpublic SaReactorFilter getSaReactorFilter() {return new SaReactorFilter()// 拦截地址.addInclude("/**")    /* 拦截全部path */// 开放地址.addExclude("/favicon.ico")// 鉴权方法:每次访问进入.setAuth(obj -> {// 登录校验 -- 拦截所有路由,并排除/account/user/doLogin用于开放登录SaRouter.match("/**", "/account/user/doLogin", r -> StpUtil.checkLogin());//                    // 权限认证 -- 不同模块, 校验不同权限
//                    SaRouter.match("/account/**", r -> StpUtil.checkRole("user"));
//                    SaRouter.match("/admin/**", r -> StpUtil.checkRole("admin"));
//                    SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
//                    SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));// 更多匹配 ...  */})// 异常处理方法:每次setAuth函数出现异常时进入.setError(e -> {return SaResult.error(e.getMessage());});}
}

只需要在gateway中添加一个全局过滤器进行鉴权操作就可以实现认证/鉴权操作了。

这里我们对**全部路径进行拦截,但不要忘记把我们的登录接口释放出来,允许访问。

到这里简单的认证操作就实现了。我们仅仅使用了sa-token的一个StpUtil.login(Object id)方法,其他事情sa-token都帮我们完成了,更无需复杂的配置和多到爆炸的Bean。
【鉴权】

有时候一个token认证并不能让我们区分用户能不能访问这个资源,使用那个菜单,我们需要更细粒度的鉴权。

在经典的RBAC模型里,用户会拥有多个角色,不同的角色又会有不同的权限。

这里我们使用五个表来表示用户、角色、权限之间的关系。

很显然,我们想判断用户有没有权限访问一个path,需要判断用户是否还有该权限。

在sa-token中想要实现这个功能,只需要实现StpInterface接口即可。

/*** 自定义权限验证接口扩展 */
@Component   
public class StpInterfaceImpl implements StpInterface {@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {// 返回此 loginId 拥有的权限列表 return ...;}@Overridepublic List<String> getRoleList(Object loginId, String loginType) {// 返回此 loginId 拥有的角色列表return ...;}}

我们在gateway实现这个接口,为用户赋予权限,再进行权限校验,就可以精确到path了。

我们使用先从Redis中获取缓存数据,获取不到时走RPC调用account服务获取。

为了更方便的使用gateway调用account服务,我们使用nacos进行服务发现,用feign调用。

在account和gateway服务中配置nacos

配置nacos

spring:cloud:nacos:discovery:username: nacospassword: nacosserver-addr: localhost:8848datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/weishuang_account?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULLusername: rootpassword: root

配置gateway

需要注意的是,gateway是基于WebFlux的一个响应式组件,HttpMessageConverters不会像Spring Mvc一样自动注入,需要我们手动配置。

package com.weishuang.gateway.gateway.config;import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;import java.util.stream.Collectors;@Configuration
public class HttpMessageConvertersConfigure {@Bean@ConditionalOnMissingBeanpublic HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));}
}

实现获取角色、权限接口

【RoleController、PermissionController】

@RestController
@RequestMapping("/account/role/")
public class RoleController {@Autowiredprivate RoleManager roleManager;@PostMapping("/getRoles")public List<RoleDTO> getRoles(@RequestParam String userId) {return roleManager.getRoles(userId);}
}@RestController
@RequestMapping("/account/permission/")
public class PermissionController {@Autowiredprivate PermissionManager permissionManager;@PostMapping("/getPermissions")public List<PermissionDTO> getPermissions(@RequestParam String userId) {return permissionManager.getPermissions(userId);}}

【RoleManager】

@Component
public class RoleManagerImpl implements RoleManager {@Autowiredprivate RoleService roleService;@Autowiredprivate UserRoleService userRoleService;@Autowiredprivate Role2RoleDTOCovert role2RoleDTOCovert;@Overridepublic List<RoleDTO> getRoles(String userId) {List<UserRole> userRoles = userRoleService.getByUserId(userId);Set<String> roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toSet());List<RoleDTO> roleDTOS = role2RoleDTOCovert.covertTargetList2SourceList(roleService.getByIds(roleIds));//服务不对外暴露,网关不传token到子服务,这里通过userId获取session,并设置角色。String tokenValue = StpUtil.getTokenValueByLoginId(userId);//为这个token在redis中设置角色,使网关获取更方便if(StringUtils.isNotEmpty(tokenValue)){if(CollectionUtils.isEmpty(roleDTOS)){StpUtil.getTokenSessionByToken(tokenValue).set("ROLES", "");}else{List<String> roleNames = roleDTOS.stream().map(RoleDTO::getRoleName).collect(Collectors.toList());StpUtil.getTokenSessionByToken(tokenValue).set("ROLES", ListUtil.list2String(roleNames));}}return roleDTOS;}
}

【PermissionManager】

@Component
public class PermissionManagerImpl implements PermissionManager {@Autowiredprivate PermissionService permissionService;@Autowiredprivate RolePermService rolePermService;@Autowiredprivate UserRoleService userRoleService;@Autowiredprivate Permission2PermissionDTOCovert permissionDTOCovert;@Overridepublic List<PermissionDTO> getPermissions(String userId) {//获取用户的角色List<UserRole> roles = userRoleService.getByUserId(userId);if (CollectionUtils.isEmpty(roles)) {handleUserPermSession(userId, null);}Set<String> roleIds = roles.stream().map(UserRole::getRoleId).collect(Collectors.toSet());List<RolePerm> rolePerms = rolePermService.getByRoleIds(roleIds);if (CollectionUtils.isEmpty(rolePerms)) {handleUserPermSession(userId, null);}Set<String> permIds = rolePerms.stream().map(RolePerm::getPermId).collect(Collectors.toSet());List<PermissionDTO> perms = permissionDTOCovert.covertTargetList2SourceList(permissionService.getByIds(permIds));handleUserPermSession(userId, perms);return perms;}private void handleUserPermSession(String userId, List<PermissionDTO> perms) {//通过userId获取session,并设置权限String tokenValue = StpUtil.getTokenValueByLoginId(userId);if (StringUtils.isNotEmpty(tokenValue)) {//为了防止没有权限的用户多次进入到该接口,没权限的用户在redis中存入空字符串if (CollectionUtils.isEmpty(perms)) {StpUtil.getTokenSessionByToken(tokenValue).set("PERMS", "");} else {List<String> paths = perms.stream().map(PermissionDTO::getPath).collect(Collectors.toList());StpUtil.getTokenSessionByToken(tokenValue).set("PERMS", ListUtil.list2String(paths));}}}
}

gateway获取角色、权限

方式一: 官方写的实现StpInterfaceImpl中的方法

作为一个异步组件,gateway中不允许使用引起阻塞的同步调用,若使用feign进行调用就会发生错误,我们使用CompletableFuture来将同步调用转换成异步操作,但使用CompletableFuture我们需要指定线程池,否则将会使用默认的ForkJoinPool

这里我们创建一个线程池,用于权限获取使用

package com.weishuang.gateway.gateway.config;import org.springframework.context.annotation.Configuration;import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;@Configuration
public class ThreadPollConfig {private final BlockingQueue<Runnable> asyncSenderThreadPoolQueue = new LinkedBlockingQueue<Runnable>(50000);public final ExecutorService USER_ROLE_PERM_THREAD_POOL = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),Runtime.getRuntime().availableProcessors(),1000 * 60,TimeUnit.MILLISECONDS,this.asyncSenderThreadPoolQueue,new ThreadFactory() {private final AtomicInteger threadIndex = new AtomicInteger(0);@Overridepublic Thread newThread(Runnable r) {return new Thread(r, "RolePermExecutor_" + this.threadIndex.incrementAndGet());}});
}

【StpInterfaceImpl】

@Component
public class StpInterfaceImpl implements StpInterface {@Autowiredprivate RoleFacade roleFacade;@Autowiredprivate PermissionFacade permissionFacade;@Autowiredprivate ThreadPollConfig threadPollConfig;@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {Object res = StpUtil.getTokenSession().get("PERMS");if (res == null) {CompletableFuture<List<String>> permFuture = CompletableFuture.supplyAsync(() -> {// 返回此 loginId 拥有的权限列表List<PermissionDTO> permissions = permissionFacade.getPermissions((String) loginId);return permissions.stream().map(PermissionDTO::getPath).collect(Collectors.toList());}, threadPollConfig.USER_ROLE_PERM_THREAD_POOL);try {return permFuture.get();} catch (InterruptedException | ExecutionException e) {throw new RuntimeException(e);}}String paths = (String) res;System.out.println(paths);return ListUtil.string2List(paths);}@Overridepublic List<String> getRoleList(Object loginId, String loginType) {Object res = StpUtil.getTokenSession().get("ROLES");if (res == null) {CompletableFuture<List<String>> roleFuture = CompletableFuture.supplyAsync(() -> {// 返回此 loginId 拥有的权限列表List<RoleDTO> roles = roleFacade.getRoles((String) loginId);return roles.stream().map(RoleDTO::getRoleName).collect(Collectors.toList());}, threadPollConfig.USER_ROLE_PERM_THREAD_POOL);try {return roleFuture.get();} catch (InterruptedException | ExecutionException e) {throw new RuntimeException(e);}}String roleNames = (String) res;System.out.println(roleNames);return ListUtil.string2List(roleNames);}}

gateway配置过滤器,实现鉴权

@Component
public class ForwardAuthFilter implements WebFilter {static Set<String> whitePaths = new HashSet<>();static {whitePaths.add("/account/user/doLogin");whitePaths.add("/account/user/logout");whitePaths.add("/account/user/register");}@Overridepublic Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {ServerHttpRequest serverHttpRequest = serverWebExchange.getRequest();String path = serverHttpRequest.getPath().toString();//需要校验权限if(!whitePaths.contains(path)){//判断用户是否有该权限if(!StpUtil.hasPermission(path)){throw new NotPermissionException(path);}}return webFilterChain.filter(serverWebExchange);}
}

方式二: 如果您觉得一定要使用响应式才行,那么无需实现StpInterfaceImpl

/*** 全局过滤器*/
@Component
public class ForwardAuthFilter implements WebFilter {@Autowiredprivate WebClient.Builder webClientBuilder;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {ServerHttpRequest serverHttpRequest = exchange.getRequest();String path = serverHttpRequest.getPath().toString();// /api开头的都要鉴权if (StringUtils.isNotEmpty(path) && path.startsWith("/api")) {Mono<List<String>> permissionList = getPermissionList();return permissionList.flatMap(list -> {if (!StpUtil.stpLogic.hasElement(list, path)) {return Mono.error(new NotPermissionException(path));}return chain.filter(exchange);});}return chain.filter(exchange);}@Bean@LoadBalancedpublic WebClient.Builder loadBalancedWebClientBuilder() {return WebClient.builder();}private Mono<List<String>> getPermissionList() {String userId = (String) StpUtil.getLoginId();Mono<List<PermissionDTO>> listMono = webClientBuilder.build().post().uri("http://weishuang-account/account/permission/getPermissions").contentType(MediaType.APPLICATION_FORM_URLENCODED).body(BodyInserters.fromFormData("userId", userId)).retrieve().bodyToFlux(PermissionDTO.class).collectList();return listMono.map(permissions -> permissions.stream().map(PermissionDTO::getPath).collect(Collectors.toList()));}}

修改sa-token的配置

@Configuration
public class SaTokenConfigure {// 注册 Sa-Token全局过滤器@Beanpublic SaReactorFilter getSaReactorFilter() {return new SaReactorFilter()// 拦截地址.addInclude("/**")    /* 拦截全部path */// 开放地址.addExclude("/favicon.ico")// 鉴权方法:每次访问进入.setAuth(obj -> {// 登录校验 -- 拦截所有路由,排除白名单SaRouter.match("/**").notMatch(new ArrayList<>(WhitePath.whitePaths)).check(r -> StpUtil.checkLogin());})// 异常处理方法:每次setAuth函数出现异常时进入.setError(e -> {return SaResult.error(e.getMessage());});}
}

白名单

public class WhitePath {static Set<String> whitePaths = new HashSet<>();static {whitePaths.add("/account/user/doLogin");whitePaths.add("/account/user/logout");whitePaths.add("/account/user/register");}
}

图片
Java内部类使用不当有可能导致内存泄露!
如何实现 MySQL 百万级数据量导出并避免 OOM
牢记这16个SpringBoot 扩展接口,写出更加漂亮的代码
MySQL binlog 的三个业务应用场景
实际开发中,你如何停止一个线程?
图片

微信扫一扫
关注该公众号

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

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

相关文章

【AI安全】货运AI安全防控:违规载人治理

# 智能运营部 # 货拉拉技术 AI识别技术 前言 安全无小事,安全关乎每个人的切身利益,任何一个小的疏忽都可能导致重大的事故。在货运行业,确保人员和货物的安全是企业必须履行的基本责任。近年来,随着需求和业务量的高速增长,货运安全面临的挑战愈发严峻。货箱载人、违禁品…

Jailer实现十几种数据库的高效管理

数据库:推荐一款非常实用的数据库定时备份工具,大神必备(附下载)! 一、介绍 对于程序员来说,数据库管理是一个非常重要的环节。为了提高开发效率和数据一致性,开发人员经常需要在生产数据库和开发/测试环境之间传输数据。 Jailer 是一款强大的工具,能够帮助开发人员实现…

大模型部署 工具 ollama | AIGC

Ollama是一个集成了多种大型语言模型的工具,它支持模型的部署、运行以及API的整合和调用。 Ollama为不同操作系统的用户提供了便捷的安装方式,并具备丰富的命令行界面(CLI)和API接口,使得用户可以轻松地管理和运行大型模型。一、安装 Ollama的安装方式因操作系统而异,以下…

深入探讨索引的创建与删除:提升数据库查询效率的关键技术

title: 深入探讨索引的创建与删除:提升数据库查询效率的关键技术 date: 2025/1/25 updated: 2025/1/25 author: cmdragon excerpt: 在数据量猛增的今天,如何有效地从庞大的数据库中快速获取所需信息,已成为数据库管理的一个重要课题。索引作为一种有效的数据结构,能够大幅…

【模拟电子技术】13-H参数等效模型

【模拟电子技术】13-H参数等效模型 我们已经知道在不同的静态工作点下,即使给三极管相同的的Ib,得到的Ic也会不同,引出H参数等效模型在第07节中有一张这样的图我们可以看出IB与UBE和UCE都有关,注意这里的真正的物理原因,UCE大到一定程度时候,从发射极到集电极的电子会饱和…

25.1.24小记

今天因为打美赛的原因,全天都比较忙,只能抽出晚上的一点点时间随便听一听课。(美赛第一天进度还算客观) 包(package) 当要用到类的时候,需要给出全名(即package.class) 举个例子 :其中包clock中包含Clock类,包display中包含display类 1.在package中的class开头必须写…

极空间避坑指南 - 苹果 Live Photo 备份后成静态图

背景 最近入手了一款新 NAS:极空间 Z40Pro,准备用作家里人的相册备份和影音中心。 折腾的过程以后有机会单独聊一聊,这篇文章主要是为了避免极空间用户踩坑。 问题说明 当使用极空间 NAS 备份 iPhone 的 Live Photo 数据之后,如果你有冷备份或者想换硬盘的情况下,贸然把备…

RocketMQ原理—3.源码设计简单分析下

大纲 1.Producer作为生产者是如何创建出来的 2.Producer启动时是如何准备好相关资源的 3.Producer是如何从拉取Topic元数据的 4.Producer是如何选择MessageQueue的 5.Producer与Broker是如何进行网络通信的 6.Broker收到一条消息后是如何存储的 7.Broker是如何实时更新索引文件…