【SpringCloud微服务项目实战-mall4cloud项目(3)】——mall4cloud-auth

news2024/11/17 23:36:33

mall4cloud-auth-认证与授权

  • pom依赖
  • nacos配置
  • 令牌认证
    • 介绍
    • 项目代码
    • 过滤器校验
  • 总结

目前项目登录使用的认证授权方式较为简单,认证通过token令牌方式,授权通过用户名密码方式,并且结合了captcha验证码登录。下面的介绍中会增加OAuth2的授权方式。

pom依赖

先看一下auth模块的相关依赖
在这里插入图片描述
①:common下的database模块,主要是关于分页工具、mybatis配置、分布式id等一些数据库相关内容
②:cache:主要是操作redis相关内容,像key、crud工具、redis分布式锁等
③:封装了授权过滤一些配置和过滤器实现
④:验证码相关
⑤:fegin调用的内部api接口

nacos配置

nacos配置如下内容,用于token生成。
在这里插入图片描述

令牌认证

介绍

令牌认证主要用于验证用户的身份。
通常,用户提供用户名和密码进行身份验证,服务器验证后颁发一个访问令牌(Token)给客户端。客户端可以在后续请求中使用这个令牌来证明其身份,而不需要再次提供用户名和密码。
令牌通常是一串字符,可以包含用户信息和权限信息。

项目代码

@PostMapping("/ua/login")
	@Operation(summary = "账号密码" , description = "通过账号登录,还要携带用户的类型,也就是用户所在的系统")
	public ServerResponseEntity<TokenInfoVO> login(
			@Valid @RequestBody AuthenticationDTO authenticationDTO) {

		// 这边获取了用户的用户信息,那么根据sessionid对应一个user的原则,我应该要把这个东西存起来,然后校验,那么存到哪里呢?
		// redis,redis有天然的自动过期的机制,有key value的形式
		ServerResponseEntity<UserInfoInTokenBO> userInfoInTokenResponse = authAccountService
				.getUserInfoInTokenByInputUserNameAndPassword(authenticationDTO.getPrincipal(),
						authenticationDTO.getCredentials(), authenticationDTO.getSysType());


		if (!userInfoInTokenResponse.isSuccess()) {
			return ServerResponseEntity.transform(userInfoInTokenResponse);
		}

		UserInfoInTokenBO data = userInfoInTokenResponse.getData();

		ClearUserPermissionsCacheDTO clearUserPermissionsCacheDTO = new ClearUserPermissionsCacheDTO();
		clearUserPermissionsCacheDTO.setSysType(data.getSysType());
		clearUserPermissionsCacheDTO.setUserId(data.getUserId());
		// 将以前的权限清理了,以免权限有缓存
		ServerResponseEntity<Void> clearResponseEntity = permissionFeignClient.clearUserPermissionsCache(clearUserPermissionsCacheDTO);

		if (!clearResponseEntity.isSuccess()) {
			return ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED);
		}

		// 保存token,返回token数据给前端,这里是最重要的
		return ServerResponseEntity.success(tokenStore.storeAndGetVo(data));
	}

完成登录后,获取一个token令牌返回到前端。再看一下用户名密码的校验

if (StrUtil.isBlank(inputUserName)) {
			return ServerResponseEntity.showFailMsg("用户名不能为空");
		}
		if (StrUtil.isBlank(password)) {
			return ServerResponseEntity.showFailMsg("密码不能为空");
		}

		InputUserNameEnum inputUserNameEnum = null;

		// 用户名
		if (PrincipalUtil.isUserName(inputUserName)) {
			inputUserNameEnum = InputUserNameEnum.USERNAME;
		}

		if (inputUserNameEnum == null) {
			return ServerResponseEntity.showFailMsg("请输入正确的用户名");
		}

		AuthAccountInVerifyBO authAccountInVerifyBO = authAccountMapper
				.getAuthAccountInVerifyByInputUserName(inputUserNameEnum.value(), inputUserName, sysType);

		if (authAccountInVerifyBO == null) {
			prepareTimingAttackProtection();
			// 再次进行运算,防止计时攻击
			// 计时攻击(Timing
			// attack),通过设备运算的用时来推断出所使用的运算操作,或者通过对比运算的时间推定数据位于哪个存储设备,或者利用通信的时间差进行数据窃取。
			mitigateAgainstTimingAttack(password);
			return ServerResponseEntity.showFailMsg("用户名或密码不正确");
		}

		if (Objects.equals(authAccountInVerifyBO.getStatus(), AuthAccountStatusEnum.DISABLE.value())) {
			return ServerResponseEntity.showFailMsg("用户已禁用,请联系客服");
		}

		if (!passwordEncoder.matches(password, authAccountInVerifyBO.getPassword())) {
			return ServerResponseEntity.showFailMsg("用户名或密码不正确");
		}
		return ServerResponseEntity.success(BeanUtil.map(authAccountInVerifyBO, UserInfoInTokenBO.class));

通过用户名获取用户信息,并通过passwordEncoder.matches()校验了密码,密码如果成功返回成功状态,userInfoInTokenResponse.isSuccess()。向下进行,调用permissionFeignClient清理权限。最后获取token代码如下:

public TokenInfoVO storeAndGetVo(UserInfoInTokenBO userInfoInToken) {
		TokenInfoBO tokenInfoBO = storeAccessToken(userInfoInToken);

		TokenInfoVO tokenInfoVO = new TokenInfoVO();
		tokenInfoVO.setAccessToken(tokenInfoBO.getAccessToken());
		tokenInfoVO.setRefreshToken(tokenInfoBO.getRefreshToken());
		tokenInfoVO.setExpiresIn(tokenInfoBO.getExpiresIn());
		return tokenInfoVO;
	}

/**
	 * 将用户的部分信息存储在token中,并返回token信息
	 * @param userInfoInToken 用户在token中的信息
	 * @return token信息
	 */
	public TokenInfoBO storeAccessToken(UserInfoInTokenBO userInfoInToken) {
		TokenInfoBO tokenInfoBO = new TokenInfoBO();
		String accessToken = IdUtil.simpleUUID();
		String refreshToken = IdUtil.simpleUUID();

		tokenInfoBO.setUserInfoInToken(userInfoInToken);
		tokenInfoBO.setExpiresIn(getExpiresIn(userInfoInToken.getSysType()));

		String uidToAccessKeyStr = getUidToAccessKey(getApprovalKey(userInfoInToken));
		String accessKeyStr = getAccessKey(accessToken);
		String refreshToAccessKeyStr = getRefreshToAccessKey(refreshToken);

		// 一个用户会登陆很多次,每次登陆的token都会存在 uid_to_access里面
		// 但是每次保存都会更新这个key的时间,而key里面的token有可能会过期,过期就要移除掉
		List<String> existsAccessTokens = new ArrayList<>();
		// 新的token数据
		existsAccessTokens.add(accessToken + StrUtil.COLON + refreshToken);

		Long size = redisTemplate.opsForSet().size(uidToAccessKeyStr);
		if (size != null && size != 0) {
			List<String> tokenInfoBoList = stringRedisTemplate.opsForSet().pop(uidToAccessKeyStr, size);
			if (tokenInfoBoList != null) {
				for (String accessTokenWithRefreshToken : tokenInfoBoList) {
					String[] accessTokenWithRefreshTokenArr = accessTokenWithRefreshToken.split(StrUtil.COLON);
					String accessTokenData = accessTokenWithRefreshTokenArr[0];
					if (BooleanUtil.isTrue(stringRedisTemplate.hasKey(getAccessKey(accessTokenData)))) {
						existsAccessTokens.add(accessTokenWithRefreshToken);
					}
				}
			}
		}

		redisTemplate.executePipelined((RedisCallback<Object>) connection -> {

			long expiresIn = tokenInfoBO.getExpiresIn();

			byte[] uidKey = uidToAccessKeyStr.getBytes(StandardCharsets.UTF_8);
			byte[] refreshKey = refreshToAccessKeyStr.getBytes(StandardCharsets.UTF_8);
			byte[] accessKey = accessKeyStr.getBytes(StandardCharsets.UTF_8);

			for (String existsAccessToken : existsAccessTokens) {
				connection.sAdd(uidKey, existsAccessToken.getBytes(StandardCharsets.UTF_8));
			}

			// 通过uid + sysType 保存access_token,当需要禁用用户的时候,可以根据uid + sysType 禁用用户
			connection.expire(uidKey, expiresIn);

			// 通过refresh_token获取用户的access_token从而刷新token
			connection.setEx(refreshKey, expiresIn, accessToken.getBytes(StandardCharsets.UTF_8));

			// 通过access_token保存用户的租户id,用户id,uid
			connection.setEx(accessKey, expiresIn, Objects.requireNonNull(redisSerializer.serialize(userInfoInToken)));

			return null;
		});

		// 返回给前端是加密的token
		tokenInfoBO.setAccessToken(encryptToken(accessToken,userInfoInToken.getSysType()));
		tokenInfoBO.setRefreshToken(encryptToken(refreshToken,userInfoInToken.getSysType()));

		return tokenInfoBO;
	}

对上面的token存储代码解释:

1、创建TokenInfoBO对象:首先,方法创建了一个TokenInfoBO对象,这个对象用于存储令牌相关的信息。
2、生成Access Token和Refresh Token:使用IdUtil.simpleUUID()生成了一个随机的Access Token和Refresh Token。
3、设置Token信息:将userInfoInToken对象设置到tokenInfoBO中,并设置了令牌的过期时间(expiresIn),该过期时间是根据userInfoInToken的sysType来确定的。
4、获取相关Key:获取了与令牌相关的一些键值(Key),如uidToAccessKeyStr,accessKeyStr,和refreshToAccessKeyStr。
处理已存在的令牌:通过查询Redis中的数据,检查是否已经存在相同用户的令牌。如果存在,将新生成的Access Token和Refresh Token添加到已存在令牌的列表中。
5、使用Redis Pipelining保存数据:使用Redis的Pipelining机制来一次性执行多个Redis命令,将令牌和相关信息存储到Redis中。这些命令包括将Access Token和Refresh Token与用户关联,设置它们的过期时间,并存储用户的信息。
6、加密令牌:使用encryptToken方法对Access Token和Refresh Token进行加密,然后将加密后的令牌设置到tokenInfoBO中。
返回Token信息:最后,返回包含Access Token和Refresh Token信息的tokenInfoBO对象,供前端使用。

后续的token解密防止攻击、token刷新代码通过代码注释可以看到相关逻辑,就不做一 一解释。

过滤器校验

前端获取到token后,会在访问接口中携带这个信息,之后后端服务通过过滤器来校验,看访问的接口是否能通过认证。主要代码在这个包下。
在这里插入图片描述

@Component
public class AuthFilter implements Filter {

	private static Logger logger = LoggerFactory.getLogger(AuthFilter.class);

	@Autowired
	private AuthConfigAdapter authConfigAdapter;

	@Autowired
	private HttpHandler httpHandler;

	@Autowired
	private TokenFeignClient tokenFeignClient;

	@Autowired
	private PermissionFeignClient permissionFeignClient;

	@Autowired
	private FeignInsideAuthConfig feignInsideAuthConfig;

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest req = (HttpServletRequest) request;
		HttpServletResponse resp = (HttpServletResponse) response;

		if (!feignRequestCheck(req)) {
			httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
			return;
		}

		if (Auth.CHECK_TOKEN_URI.equals(req.getRequestURI())) {
			chain.doFilter(req, resp);
			return;
		}


		List<String> excludePathPatterns = authConfigAdapter.excludePathPatterns();

		// 如果匹配不需要授权的路径,就不需要校验是否需要授权
		if (CollectionUtil.isNotEmpty(excludePathPatterns)) {
			for (String excludePathPattern : excludePathPatterns) {
				AntPathMatcher pathMatcher = new AntPathMatcher();
				if (pathMatcher.match(excludePathPattern, req.getRequestURI())) {
					chain.doFilter(req, resp);
					return;
				}
			}
		}

		String accessToken = req.getHeader("Authorization");

		if (StrUtil.isBlank(accessToken)) {
			httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
			return;
		}

		// 校验token,并返回用户信息
		ServerResponseEntity<UserInfoInTokenBO> userInfoInTokenVoServerResponseEntity = tokenFeignClient
				.checkToken(accessToken);
		if (!userInfoInTokenVoServerResponseEntity.isSuccess()) {
			httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
			return;
		}

		UserInfoInTokenBO userInfoInToken = userInfoInTokenVoServerResponseEntity.getData();

		// 需要用户角色权限,就去根据用户角色权限判断是否
		if (!checkRbac(userInfoInToken,req.getRequestURI(), req.getMethod())) {
			httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
			return;
		}

		try {
			// 保存上下文
			AuthUserContext.set(userInfoInToken);

			chain.doFilter(req, resp);
		}
		finally {
			AuthUserContext.clean();
		}

	}

	private boolean feignRequestCheck(HttpServletRequest req) {
		// 不是feign请求,不用校验
		if (!req.getRequestURI().startsWith(FeignInsideAuthConfig.FEIGN_INSIDE_URL_PREFIX)) {
			return true;
		}
		String feignInsideSecret = req.getHeader(feignInsideAuthConfig.getKey());

		// 校验feign 请求携带的key 和 value是否正确
		if (StrUtil.isBlank(feignInsideSecret) || !Objects.equals(feignInsideSecret,feignInsideAuthConfig.getSecret())) {
			return false;
		}
		// ip白名单
		List<String> ips = feignInsideAuthConfig.getIps();
		// 移除无用的空ip
		ips.removeIf(StrUtil::isBlank);
		// 有ip白名单,且ip不在白名单内,校验失败
		if (CollectionUtil.isNotEmpty(ips)
				&& !ips.contains(IpHelper.getIpAddr())) {
			logger.error("ip not in ip White list: {}, ip, {}", ips, IpHelper.getIpAddr());
			return false;
		}
		return true;
	}

	/**
	 * 用户角色权限校验
	 * @param uri uri
	 * @return 是否校验成功
	 */
	public boolean checkRbac(UserInfoInTokenBO userInfoInToken, String uri, String method) {

		if (!Objects.equals(SysTypeEnum.PLATFORM.value(), userInfoInToken.getSysType()) && !Objects.equals(SysTypeEnum.MULTISHOP.value(), userInfoInToken.getSysType())) {
			return true;
		}

		ServerResponseEntity<Boolean> booleanServerResponseEntity = permissionFeignClient
				.checkPermission(userInfoInToken.getUserId(), userInfoInToken.getSysType(),uri,userInfoInToken.getIsAdmin(),HttpMethodEnum.valueOf(method.toUpperCase()).value() );

		if (!booleanServerResponseEntity.isSuccess()) {
			return false;
		}

		return booleanServerResponseEntity.getData();
	}

}

下面详细说明过滤逻辑
在这里插入图片描述
①:

这段代码中的 doFilter 方法是实现了 Filter 接口的一个方法,用于处理 HTTP 请求的过滤逻辑。它是一个回调方法,当有请求到达时,容器会调用这个方法来执行一些预处理和后处理的操作。
方法的参数包括:
request: 表示 HTTP 请求对象,通常是 ServletRequest 类型,可以用于获取请求的信息和数据。
response: 表示 HTTP 响应对象,通常是 ServletResponse 类型,用于生成和发送响应数据。
chain: 表示过滤器链(FilterChain),可以用于继续处理请求或将请求传递给下一个过滤器。

③:内部请求校验

private boolean feignRequestCheck(HttpServletRequest req) {
		// 不是feign请求,返回true
		if (!req.getRequestURI().startsWith(FeignInsideAuthConfig.FEIGN_INSIDE_URL_PREFIX)) {
			return true;
		}
		//获取fegin的value密钥,这个在nacos中已配置
		String feignInsideSecret = req.getHeader(feignInsideAuthConfig.getKey());

		// 校验feign 请求携带的key 和 value是否正确,不正确返回false
		if (StrUtil.isBlank(feignInsideSecret) || !Objects.equals(feignInsideSecret,feignInsideAuthConfig.getSecret())) {
			return false;
		}
		// ip白名单
		List<String> ips = feignInsideAuthConfig.getIps();
		// 移除无用的空ip
		ips.removeIf(StrUtil::isBlank);
		// 有ip白名单,且ip不在白名单内,校验失败。不为空或者获取当前用户真实ip不在白名单中,返回false
		if (CollectionUtil.isNotEmpty(ips)
				&& !ips.contains(IpHelper.getIpAddr())) {
			logger.error("ip not in ip White list: {}, ip, {}", ips, IpHelper.getIpAddr());
			return false;
		}
		return true;
	}

上面如果返回了false,是fegin请求但是不通过就走判断中的代码,意思是发送的相应为“未授权”。fegin请求校验不通过是认为失败的

if (!feignRequestCheck(req)) {
			httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
			return;
		}

④:如果为token校验请求,这个过滤器就不管了,发到下一个过滤器中,这也是chain.doFilter(request, response)的作用。但是可以看到,目前系统过滤器链上只有一个过滤器。在AuthConfig中。
在这里插入图片描述

@ConditionalOnMissingBean 是一个常用于 Spring 框架应用中,特别是在 Spring Boot 中的注解,用于根据应用上下文中是否已存在相同类型的其他 Bean,有条件地配置一个 Bean。这个注解是 Spring 的基于注解的配置的一部分,用于控制 Bean 的实例化

⑤:目前这个list有下面的url需排除,然后这个是通过bean的注入实现的
还是这个config代码,第一个bean,下面的截图说明了哪些需要排除,

@Configuration
public class AuthConfig {

	@Bean
	@ConditionalOnMissingBean
	public AuthConfigAdapter authConfigAdapter() {
		return new DefaultAuthConfigAdapter();
	}

	@Bean
	@Lazy
	public FilterRegistrationBean<AuthFilter> filterRegistration(AuthConfigAdapter authConfigAdapter, AuthFilter authFilter) {
		FilterRegistrationBean<AuthFilter> registration = new FilterRegistrationBean<>();
		// 添加过滤器
		registration.setFilter(authFilter);
		// 设置过滤路径,/*所有路径
		registration.addUrlPatterns(ArrayUtil.toArray(authConfigAdapter.pathPatterns(), String.class));
		registration.setName("authFilter");
		// 设置优先级
		registration.setOrder(0);
		registration.setDispatcherTypes(DispatcherType.REQUEST);
		return registration;
	}

}

在这里插入图片描述
在这里插入图片描述
⑥之后循环这个list,做了一个正则的匹配。来排除不需要经过这个auth过滤器的url。
⑦⑧:这里终于到了正式的请求了,看他是否携带了Authorization这个内容,为空则直接返回未授权。
接下来的代码如下
在这里插入图片描述
①检查接口代码如下,是调用了OpenFegin接口,这里顺便看一下项目对fegin的实践方式。
首先是在mall4cloud-api包下建立了某个模块要提供的api接口,在fegin包下,选择其中一个接口查看

可以看到通过@FeignClient(value = “mall4cloud-auth”,contextId =“token”)注解修饰:value指定了服务的名称,contextId指定了这个接口的唯一标识。@GetMapping(value = Auth.CHECK_TOKEN_URI)指定了访问的uri
在这里插入图片描述

之后是对接口的实现

实现都是在各个模块的fegin包下,且实现是通过@RestController注解实现。这样的作法类似于@DubboService注解修饰api实现类。
在这里插入图片描述
‘’‘’‘’‘
在这里插入图片描述
而通常的feign接口是直接通过http请求调用的,当调用 checkToken() 方法时,实际上调用的是 Feign 生成的代理对象的方法。
代理对象会根据方法的定义和注解(例如 @GetMapping(value = Auth.CHECK_TOKEN_URI))生成一个 HTTP 请求,并将请求发送到远程服务的相应路径。
远程服务响应后,代理对象会将响应解析为ServerResponseEntity< UserInfoInTokenBO > 类型,并将其返回。
但是mall4cloud-auth这个模块代码中没有“/feign/token/checkToken”这个请求uri和对应的controller
同时,“/feign/token/checkToken”这个请求也是被拦截器放行的,这个在上面已说明。

②③权限校验
这里分为了商家端、平台端、用户端,之后检查是否有某个uri的权限,主要代码如下,这个权限校验属于rbac模块,下一章节再详细介绍
在这里插入图片描述
如果校验失败,就返回未授权状态码
在这里插入图片描述
④:使用ThreadLocal保存用户信息。可以做到线程间隔离作用,以及在整个线程中传递上下文信息。

线程安全的数据隔离:ThreadLocal 可以用于在多线程环境中隔离数据,确保每个线程都有自己独立的数据副本,从而避免多个线程之间的数据共享和竞态条件。这对于一些上下文相关的数据非常有用,例如用户登录信息、会话信息等。
传递上下文信息:有些情况下,您可能需要在整个线程上下文中传递某些信息,而不必在每个方法调用中显式传递这些信息作为参数。ThreadLocal 可以用于存储和访问这些上下文信息。

之后进入下一个过滤器(可以扩展),用户信息用完以后释放即可。

总结

auth模块的代码逻辑主要流程是登录接口/ua/login->tokenStore下的storeAccessToken()方法,令牌存储完成后,就是过滤器AuthFilter类的实现。其中的doFilter()方法进行了访问路径(请求)授权,用户角色授权(这里主要调用了rbac模块的服务)。

这个auth模块的代码并不是主流的用户认证授权功能。更像一个单体架构的服务。下面介绍一下有疑问的地方。

  1. 首先是token的生成,这里直接使用uuid方式。这样做具有了可预测性以及只当作随机字符串不能存储任何信息。当然,这里使用了分布式redis缓存了用户信息,也是合理的。

上面的方式可以增加登录时获取随机码,然后再通过随机码加密的方式生成token
为了让token存储用户信息,或者增加用户的权限,可以使用jwt方式。这也是一种用于分布式

  1. 授权时使用的过滤器是servlet下的Filter,这个大多数的逻辑操作需要手动编写和指定,并没有使用一些流行的权限认证框架,比如SpringSecrity或者shiro等。这作为学习项目可以学习其中的授权认证逻辑,但是作为企业级项目,认证授权并不够精细化。但是总的逻辑一致。

比如过滤器中的一些放行接口,不是基于配置,而是在代码中写死的。或者对于接口的访问,如果是未授权,正常应该是跳转到指定登录页面提示,不是直接返回未授权。这些通过SpringSecrity框架可能做的更精细一些。

后续补充一个使用SpringSecrity或者shiro框架项目做用户认证授权的文章。

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

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

相关文章

git 取消待推送内容

选择最后一次提交的记录&#xff0c;右键->软合并

Web测试的基础流程(外加测试过程需要的注意5点)

前言 在Web工程过程中&#xff0c;基于Web系统的测试、确认和验收是一项重要而富有挑战性的工作。基于Web的系统测试与传统的软件测试不同&#xff0c;它不但需要检查和验证是否按照设计的要求运行&#xff0c;而且还要测试系统在不同用户的浏览器端的显示是否合适。 重要的是…

灯具从深圳寄国际物流到墨西哥

在国际贸易的日益频繁的今天&#xff0c;越来越多的企业开始将产品销往海外市场。然而&#xff0c;如何将这些产品安全、快速地送达目的地&#xff0c;成为了每个企业都需要面对的问题。对于灯具这种重量大、体积大的物品来说&#xff0c;如何选择合适的国际物流方式&#xff0…

如何在 Spring Boot 中使用 WebSocket

在Spring Boot中使用WebSocket构建实时应用 WebSocket是一种用于实现双向通信的网络协议&#xff0c;它非常适合构建实时应用程序&#xff0c;如在线聊天、实时通知和多人协作工具。Spring Boot提供了对WebSocket的支持&#xff0c;使得在应用程序中集成WebSocket变得非常容易…

KWin、libdrm、DRM从上到下全过程 —— drmModeAddFBxxx(2)

接前一篇文章&#xff1a;KWin、libdrm、DRM从上到下全过程 —— drmModeAddFBxxx&#xff08;1&#xff09; 上回书说到drmModeAddFB、drmModeAddFB2和drmModeAddFB2WithModifiers函数最终“三分归一统”&#xff0c;在内核层统一调用到drm_mode_addfb2函数。 这里我们先不急…

手撕各种排序

> 作者简介&#xff1a;დ旧言~&#xff0c;目前大一&#xff0c;现在学习Java&#xff0c;c&#xff0c;c&#xff0c;Python等 > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;掌握每种排序的方法&#xff0c;理解每种排序利弊…

视频剪辑利器,批量随机抽帧轻松保存,让精彩片段永不丢失!

您是否经常遇到这样的情况&#xff1a;在一段长视频中&#xff0c;有一些精彩的瞬间&#xff0c;但却不知道如何将其提取并保存下来&#xff1f;现在&#xff0c;我们为您推出了一款强大的视频剪辑利器&#xff0c;能够帮助您批量从视频中指定的区间内随机抽帧&#xff0c;并轻…

NPDP考什么?难度大不大?

之前给大家分享了NPDP考试时间以及报名条件&#xff0c;最近有宝子问我&#xff0c;这个考试难度咋样&#xff1f;都考察什么内容啊&#xff1f;今天给大家详细回答一下~ 1&#xff09;NPDP考试及报名时间 2023年下半年NPDP考试将于12月2日进行 预报名时间&#xff1a;10月8日…

LLM 时代,如何优雅地训练大模型?

原作者王嘉宁 基于https://wjn1996.blog.csdn.net/article/details/130764843 整理 大家好&#xff0c;ChatGPT于2022年12月初发布&#xff0c;震惊轰动了全世界&#xff0c;发布后的这段时间里&#xff0c;一系列国内外的大模型训练开源项目接踵而至&#xff0c;例如Alpaca、B…

英码边缘计算盒子IVP03X——32T超强算力,搭载BM1684X算能TPU处理器

产品8大优势&#xff1a; 高效节能&#xff1a;相较异构产品&#xff0c;IVP03X数据调配效率更高&#xff0c;资源利用率更高&#xff0c;平均功耗更低&#xff1b;升级换代&#xff1a;相较算能BM1684平台&#xff0c;IVP03X算力、编码&#xff0c;模型转换性能均翻倍提升&am…

DALL-E 3调参教程;百度新出的AI写小说神器;通义听悟看播客也太爽了;系列博文带你理解生成式AI | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f525; 2023年诺贝尔奖全部揭晓&#xff0c;一文看完6类奖项花落谁家 https://www.nobelprize.org/prizes 随着最后一项「经济学奖」的揭秘&a…

c++视觉检测-----Canny边缘算子

Canny边缘算子 cv::Canny()是OpenCV库中用于执行Canny边缘检测的函数。Canny边缘检测是一种广泛使用的图像处理技术&#xff0c;用于检测图像中的边缘。 以下是cv::Canny()函数的一般用法和参数&#xff1a; void cv::Canny(cv::InputArray image, // 输入图像&#x…

005 OA人事管理系统

人事管理系统 一、系统介绍 本系统为职工人事管理系统&#xff0c;系统分为七大模块&#xff1a;职工管理&#xff0c;部门管理&#xff0c;岗位管理&#xff0c;招聘管理&#xff0c;奖惩管理&#xff0c;薪资管理&#xff0c;培训管理 系统默认有两个个角色&#xff1a;管…

css实现一行N个元素动态布局(以4个为例)

昨日同事问了我一个前端问题&#xff0c;前端开发的尺寸都不按照UI图上面还原的吗&#xff1f; 我了解了其中原由&#xff0c;告知UI图并不会考虑到所有的场景&#xff0c;只能给个案例&#xff0c;画图是死的&#xff0c;代码写出来的得是活的。就像他遇到的案例&#xff0c;请…

【高级语言程序设计】python函数式编程(一)

基础知识 Python函数式编程的主要内容包括以下几个方面&#xff1a; (1)函数作为一等公民&#xff1a;在函数式编程中&#xff0c;函数被视为一等公民&#xff0c;可以像其他数据类型一样被传递、赋值以及作为返回值。 (2)不可变数据&#xff1a;函数式编程鼓励使用不可变数据…

函数栈帧的创建与销毁剖析

目录 一、前言 二、基础知识介绍 2.1 寄存器介绍 2.2、汇编指令介绍 三、函数栈帧的创建销毁过程 3.1 调用main函数的函数 3.2 main函数开辟栈帧 3.3 在main函数中创建变量 3.4 调用Add函数前的准备 3.5 为Add函数开辟栈帧 3.6 在Add函数中创建变量并运算 3.7 Add函…

给手机上液冷?谈谈华为Mate 60系列手机专属黑科技—— “微泵液冷”手机壳

最近&#xff0c;有一个手机配件吸引了我的注意——华为的微泵液冷壳。 简单来说&#xff0c;就是在手机壳里装了无线充电微泵&#xff0c;为手机实现外置水冷的能力。让手机壳在“外观装饰”和“防摔保护”的功能性上额外加了一个“降温提性能”的作用。 接下来&#xff0c;本…

ATFX汇市:9月非农再超预期,高利率并未导致美国宏观经济收缩

ATFX汇市&#xff1a;据美国劳工部数据&#xff0c;9月季调后非农就业人口33.6万人&#xff0c;远高于前值18.7万人&#xff0c;高于预期值17万人&#xff0c;创出今年一月份以来的新高。亮眼的就业数据意味着美国宏观经济仍处于急速扩张状态&#xff0c;高利率的破坏性远低于此…

【c#】adapter.fill(dt)报错specified cast is not valid

报错信息&#xff1a; 报错specified cast is not valid,指定转换类型无效。 原因 查出来的数据有小数&#xff0c;且小数位数较多&#xff0c;问题就出现在这里&#xff0c;ORacle可以查出精确度高的数据&#xff0c;但是C#没办法查出来&#xff0c;就导致了有数据类型转换&…