【Spring Security系列】权限之旅:SpringSecurity小程序登录深度探索
作者:后端小肥肠
创作不易,未经允许严禁转载。
姊妹篇:
【Spring Security系列】Spring Security+JWT+Redis实现用户认证登录及登出_spring security jwt 退出登录-CSDN博客
1. 前言
欢迎来到【Spring Security系列】!在当今数字化时代,安全是任何应用程序都必须优先考虑的核心问题之一。而在众多安全框架中,Spring Security 作为一个功能强大且广泛应用的安全框架,为 Java 应用程序提供了全面的身份验证、授权、攻击防护等功能。而随着移动应用的普及,小程序作为一种轻量级、跨平台的应用形式,其安全性也成为了开发者们关注的焦点。本文将带领您深入探索如何使用 Spring Security 来保护小程序的登录认证,旨在为您提供全方位的学习体验和实践指导。
2. 小程序登录涉及SpringSecurity核心组件介绍
如果要在SpringSecurity默认的用户名密码模式登录模式上扩展小程序登录,涉及到的核心组件如下:
-
AuthenticationProvider:
创建自定义的AuthenticationProvider,负责处理从微信开放平台获取的用户信息,并进行身份验证。 -
UserDetailsService:
调整UserDetailsService来获取并管理基于微信OpenID的用户信息。 -
AuthenticationManager:
确保您的自定义AuthenticationProvider被正确注册到AuthenticationManager中,以便处理小程序登录请求。 -
SecurityConfigurer:
创建一个SecurityConfigurer来配置Spring Security以支持小程序登录,并将其添加到Spring Security的配置类中。 -
Filter:
创建一个自定义的过滤器来拦截和处理小程序登录请求,提取微信登录凭证,并将其传递给您的自定义AuthenticationProvider进行处理。
要扩展Spring Security以支持小程序登录,您需要创建自定义的AuthenticationProvider并调整UserDetailsService以处理微信OpenID的用户信息。
3. SpringSecurity集成小程序登录原理
3.1. 小程序登录流程
以下是微信官方文档中小程序登录的流程:
由上图可看出,小程序登录使用微信提供的登录凭证 code,通过微信开放平台的接口获取用户的唯一标识 OpenID 和会话密钥 SessionKey。在集成小程序登录时,我们需要将这些凭证传递给后端服务器,由后端服务器进行校验和处理,最终完成用户的登录认证。
3.2. SpringSecurity集成小程序登录流程梳理
结合SpringSecurity原理,在SpringSecurity中集成小程序登录的流程如下:
- 小程序端:通过微信登录接口获取登录凭证 code,这里取名为loginCode。
- 小程序端,通过手机号快速验证组件获取phoneCode。
- 小程序端,获取用户昵称(nickName)和用户头像(imageUrl)地址。
- 小程序端:将登录凭证 loginCode、phoneCode、nickName、imageUrl发送给后端服务器。
- 后端服务器:接收到登录凭证 loginCode后,调用微信开放平台的接口,换取用户的唯一标识 OpenID 和会话密钥 SessionKey。
- 后端服务器:根据 OpenID 查询用户信息,如果用户不存在,则创建新用户;如果用户已存在,则返回用户信息。
- 后端服务器:生成用户的身份认证信息JWT Token,返回给小程序端。
- 小程序端:存储用户的身份认证信息,后续请求携带该信息进行访问控制。
大体流程只是在3.1小程序登录流程上做了细化,图我就不画了(因为懒)。
3.3. 小程序登录接口设计
小程序登录接口如下图所示:
由上图所示,我们需要传入loginCode,phoneCode(获取手机号),nickName(昵称用于登录后展示),imageUrl(头像用于登录后展示)这几个必传参数。
4. 核心代码讲解
4.1. 小程序端获取必要参数
1. 小程序端调用微信登录接口,获取用户登录凭证 loginCode。
wx.login({success (res) {if (res.code) {//发起网络请求wx.request({url: 'https://example.com/onLogin',data: {code: res.code}})} else {console.log('登录失败!' + res.errMsg)}}
})
2. 获取PhoneCode
3. 获取头像昵称
4.2. 编写WeChatAuthenticationFilter
public class WeChatAuthenticationFilter extends AbstractAuthenticationProcessingFilter {private final String loginCode = "loginCode";private final String phoneCode="phoneCode";private final String nickName="nickName";private final String imageUrl="imageUrl";public WeChatAuthenticationFilter(String appId, String secret) {super(new AntPathRequestMatcher("/wx/login", "POST"));}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {String loginCode = obtainLoginCode(request)==null?"":obtainLoginCode(request).trim();String phoneCode=obtainPhoneCode(request)==null?"":obtainPhoneCode(request).trim();String nickName=obtainNickName(request)==null?"":obtainNickName(request).trim();String imageUrl=obtainImageUrl(request)==null?"":obtainImageUrl(request).trim();WechatAuthenticationToken authRequest = new WechatAuthenticationToken(loginCode,phoneCode,nickName,imageUrl);return this.getAuthenticationManager().authenticate(authRequest);}protected String obtainLoginCode(HttpServletRequest request) {return request.getParameter(loginCode);}protected String obtainPhoneCode(HttpServletRequest request){return request.getParameter(phoneCode);}protected String obtainNickName(HttpServletRequest request){return request.getParameter(nickName);}protected String obtainImageUrl(HttpServletRequest request){return request.getParameter(imageUrl);}
}
以上代码定义了一个名为WeChatAuthenticationFilter的类,它继承自AbstractAuthenticationProcessingFilter类,用于处理微信登录认证。在构造函数中,指定了请求匹配路径为"/wx/login",请求方法为POST。类中定义了四个常量:loginCode、phoneCode、nickName和imageUrl,分别表示登录码、手机号码、昵称和头像URL。
在attemptAuthentication方法中,首先通过obtainLoginCode、obtainPhoneCode、obtainNickName和obtainImageUrl方法获取请求中的登录码、手机号码、昵称和头像URL,并进行了空值处理。然后将这些信息封装到WechatAuthenticationToken对象中,并通过getAuthenticationManager().authenticate方法进行认证。
4.3. 编写WeChatAuthenticationProvider
@Slf4j
public class WeChatAuthenticationProvider implements AuthenticationProvider {private final WechatConfig wechatConfig;private RestTemplate restTemplate;private final WeChatService weChatService;private final ISysUserAuthService sysUserAuthService;public WeChatAuthenticationProvider(WechatConfig wechatConfig, WeChatService weChatService,ISysUserAuthService sysUserAuthService) {this.wechatConfig = wechatConfig;this.weChatService = weChatService;this.sysUserAuthService=sysUserAuthService;}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {WechatAuthenticationToken wechatAuthenticationToken = (WechatAuthenticationToken) authentication;String loginCode = wechatAuthenticationToken.getPrincipal().toString();log.info("loginCode is {}",loginCode);String phoneCode=wechatAuthenticationToken.getPhoneCode().toString();log.info("phoneCode is {}",phoneCode);String nickName=wechatAuthenticationToken.getNickName().toString();log.info("nickName is {}",nickName);String imageUrl=wechatAuthenticationToken.getImageUrl().toString();log.info("imageUrl is {}",imageUrl);restTemplate=new RestTemplate();//获取openIdJwtUser jwtUser=null;String url = "https://api.weixin.qq.com/sns/jscode2session?appid={appid}&secret={secret}&js_code={code}&grant_type=authorization_code";Map<String, String> requestMap = new HashMap<>();requestMap.put("appid", wechatConfig.getAppid());requestMap.put("secret", wechatConfig.getSecret());requestMap.put("code", loginCode);ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class,requestMap);JSONObject jsonObject= JSONObject.parseObject(responseEntity.getBody());log.info(JSONObject.toJSONString(jsonObject));String openId=jsonObject.getString("openid");if(StringUtils.isBlank(openId)) {throw new BadCredentialsException("weChat get openId error");}if(sysUserAuthService.getUserAuthCountByIdentifier(openId)>0){jwtUser = (JwtUser) weChatService.getUserByOpenId(openId);if(!jwtUser.isEnabled()){throw new BadCredentialsException("用户已失效");}return getauthenticationToken(jwtUser,jwtUser.getAuthorities());}//获取手机号第一步,获取accessTokenString accessTokenUrl="https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}";Map<String, String> accessTokenRequestMap = new HashMap<>();accessTokenRequestMap.put("appid", wechatConfig.getAppid());accessTokenRequestMap.put("secret", wechatConfig.getSecret());ResponseEntity<String> accessTokenResponseEntity = restTemplate.getForEntity(accessTokenUrl, String.class,accessTokenRequestMap);JSONObject accessTokenJsonObject= JSONObject.parseObject(accessTokenResponseEntity.getBody());log.info(JSONObject.toJSONString(accessTokenJsonObject));String accessToken=accessTokenJsonObject.getString("access_token");if(StringUtils.isBlank(accessToken)) {throw new BadCredentialsException("weChat get accessToken error");}//获取手机号第二部,远程请求获取手机号String pohoneUrl="https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token="+accessToken+"";JSONObject phoneJson=new JSONObject();phoneJson.put("code",phoneCode);String resPhoneStr= RestTemplateUtil.postForJson(pohoneUrl,phoneJson,restTemplate);log.info(resPhoneStr);JSONObject resPhonJson= JSON.parseObject(resPhoneStr);JSONObject phoneInfo=resPhonJson.getJSONObject("phone_info");String mobile=phoneInfo.getString("phoneNumber");if(StringUtils.isBlank(mobile)){throw new BadCredentialsException("Wechat get mobile error");}jwtUser= (JwtUser) weChatService.getUserByMobile(mobile,nickName,imageUrl);sysUserAuthService.saveUserAuth(new AddUserAuthReq(jwtUser.getUid(),"wechat",openId));return getauthenticationToken(jwtUser,jwtUser.getAuthorities());}@Overridepublic boolean supports(Class<?> authentication) {return WechatAuthenticationToken.class.isAssignableFrom(authentication);}public WechatAuthenticationToken getauthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities){WechatAuthenticationToken authenticationToken=new WechatAuthenticationToken(principal,authorities);LinkedHashMap<Object, Object> linkedHashMap = new LinkedHashMap<>();linkedHashMap.put("principal", authenticationToken.getPrincipal());authenticationToken.setDetails(linkedHashMap);return authenticationToken;}
}
上述代码是一个自定义的认证提供者类,名为WeChatAuthenticationProvider。其主要功能是处理微信登录认证。在authenticate方法中,首先从传入的Authentication对象中提取出微信登录所需的参数,包括登录码、手机号码、昵称和头像URL。然后通过RestTemplate发送HTTP请求到微信API获取用户的openId,以验证用户身份。若成功获取openId,则检查系统中是否存在该用户的认证信息,若存在则直接返回认证token;若不存在,则继续获取用户的手机号,并根据手机号获取用户信息,并保存用户认证信息。最后,返回经过认证的token。
4.4. 编写WechatAuthenticationToken
public class WechatAuthenticationToken extends AbstractAuthenticationToken {private final Object principal;private Object phoneCode;private Object nickName;private Object imageUrl;public WechatAuthenticationToken(String loginCode,String phoneCode,String nickName,String imageUrl) {super(null);this.principal = loginCode;this.phoneCode=phoneCode;this.nickName=nickName;this.imageUrl=imageUrl;setAuthenticated(false);}public WechatAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;super.setAuthenticated(true);}@Overridepublic Object getCredentials() {return null;}@Overridepublic Object getPrincipal() {return this.principal;}public Object getPhoneCode() {return phoneCode;}public Object getNickName() {return nickName;}public Object getImageUrl() {return imageUrl;}
}
4.5. WechatConfig
@Data
@Component
@ConfigurationProperties(prefix="wechat")
public class WechatConfig {private String appid;private String secret;
}
4.6. 更新WebSecurityConfigurer
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Autowired@Qualifier("authUserDetailsServiceImpl")private UserDetailsService userDetailsService;@Autowiredprivate SecurOncePerRequestFilter securOncePerRequestFilter;@Autowiredprivate SecurAuthenticationEntryPoint securAuthenticationEntryPoint;@Autowiredprivate SecurAccessDeniedHandler securAccessDeniedHandler;//登录成功处理器@Autowiredprivate SecurAuthenticationSuccessHandler securAuthenticationSuccessHandler;@Autowiredprivate SecurAuthenticationFailureHandler securAuthenticationFailureHandler;//退出处理器@Autowiredprivate SecurLogoutHandler securLogoutHandler;@Autowiredprivate SecurLogoutSuccessHandler securLogoutSuccessHandler;@AutowiredBCryptPasswordEncoderUtil bCryptPasswordEncoderUtil;@Value("${wechat.appid}")private String appId;@Value("${wechat.secret}")private String secret;@AutowiredWechatConfig wechatConfig;@Autowiredprivate WeChatService weChatService;@Autowiredprivate ISysUserAuthService sysUserAuthService;// @Autowired
// DynamicPermission dynamicPermission;/*** 从容器中取出 AuthenticationManagerBuilder,执行方法里面的逻辑之后,放回容器** @param authenticationManagerBuilder* @throws Exception*/@Autowiredpublic void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoderUtil);}@Overrideprotected void configure(HttpSecurity http) throws Exception {//第1步:解决跨域问题。cors 预检请求放行,让Spring security 放行所有preflight request(cors 预检请求)http.authorizeRequests().requestMatchers(CorsUtils::isPreFlightRequest).permitAll();//第2步:让Security永远不会创建HttpSession,它不会使用HttpSession来获取SecurityContexthttp.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().headers().cacheControl();//第3步:请求权限配置//放行注册API请求,其它任何请求都必须经过身份验证.http.authorizeRequests()
// .antMatchers("/**").permitAll().antMatchers(HttpMethod.POST,"/sys-user/register").permitAll().antMatchers(HttpMethod.GET,"/temp/create","/department/enable-department","/instance/**","/file/download/**").permitAll().antMatchers("/css/**", "/js/**", "/images/**", "/fonts/**","/editor-app/**","/model/**","/editor/**").permitAll().antMatchers("/modeler.html/**").permitAll().antMatchers("/feign/**").permitAll()//ROLE_ADMIN可以操作任何事情.antMatchers("/v2/api-docs", "/v2/feign-docs","/swagger-resources/configuration/ui","/swagger-resources","/swagger-resources/configuration/security","/swagger-ui.html", "/webjars/**").permitAll().antMatchers(HttpMethod.POST, "/user/wx/login").permitAll().anyRequest().authenticated();// .antMatchers("/**").hasAnyAuthority("USER","SUPER_ADMIN","ADMIN");/*由于使用动态资源配置,以上代码在数据库中配置如下:在sys_backend_api_table中添加一条记录backend_api_id=1,backend_api_name = 所有API,backend_api_url=/**,backend_api_method=GET,POST,PUT,DELETE*///动态加载资源
// .anyRequest().access("@dynamicPermission.checkPermisstion(request,authentication)");//第4步:拦截账号、密码。覆盖 UsernamePasswordAuthenticationFilter过滤器http.addFilterAt(securUsernamePasswordAuthenticationFilter() , UsernamePasswordAuthenticationFilter.class);//第5步:拦截token,并检测。在 UsernamePasswordAuthenticationFilter 之前添加 JwtAuthenticationTokenFilterhttp.addFilterBefore(securOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);http.addFilterBefore(weChatAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);//第6步:处理异常情况:认证失败和权限不足http.exceptionHandling().authenticationEntryPoint(securAuthenticationEntryPoint).accessDeniedHandler(securAccessDeniedHandler);//第7步:登录,因为使用前端发送JSON方式进行登录,所以登录模式不设置也是可以的。http.formLogin();//第8步:退出http.logout().addLogoutHandler(securLogoutHandler).logoutSuccessHandler(securLogoutSuccessHandler);}@Beanpublic WeChatAuthenticationFilter weChatAuthenticationFilter() throws Exception {WeChatAuthenticationFilter filter = new WeChatAuthenticationFilter(appId, secret);filter.setAuthenticationManager(authenticationManagerBean());filter.setAuthenticationSuccessHandler(securAuthenticationSuccessHandler);filter.setAuthenticationFailureHandler(securAuthenticationFailureHandler);return filter;}/*** 手动注册账号、密码拦截器* @return* @throws Exception*/@BeanSecurUsernamePasswordAuthenticationFilter securUsernamePasswordAuthenticationFilter() throws Exception {SecurUsernamePasswordAuthenticationFilter filter = new SecurUsernamePasswordAuthenticationFilter();//成功后处理filter.setAuthenticationSuccessHandler(securAuthenticationSuccessHandler);//失败后处理filter.setAuthenticationFailureHandler(securAuthenticationFailureHandler);filter.setAuthenticationManager(authenticationManagerBean());return filter;}@Beanpublic WeChatAuthenticationProvider weChatAuthenticationProvider() {return new WeChatAuthenticationProvider(wechatConfig,weChatService,sysUserAuthService);}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// 添加微信登录认证提供者auth.authenticationProvider(weChatAuthenticationProvider());// 添加用户名密码登录认证提供者auth.authenticationProvider(daoAuthenticationProvider());}@Beanpublic DaoAuthenticationProvider daoAuthenticationProvider() {DaoAuthenticationProvider provider = new DaoAuthenticationProvider();provider.setUserDetailsService(userDetailsService);provider.setPasswordEncoder(new BCryptPasswordEncoder());return provider;}
}
说一个容易踩坑的地方:在Spring Security中,当你配置了自定义的认证提供者(如weChatAuthenticationProvider()
)来处理特定类型的认证(如微信登录),如果没有同时配置默认的认证提供者(如daoAuthenticationProvider()
),则原有的基于用户名和密码的认证机制不会自动生效。这是因为Spring Security的认证机制是基于一个可配置的AuthenticationManager
,它管理一个AuthenticationProvider
列表,这些提供者会依次尝试认证用户提交的Authentication
请求。
5. 结语
在本文中以流程讲解和代码实操讲解了如何在已有用户名和密码登录的基础上,实现微信小程序登录集成。下期将介绍基于OAuth2框架如何实现小程序登录,感兴趣的同学动动你们发财的小手点点关注吧~
6. 参考链接
开放能力 / 用户信息 / 手机号快速验证组件 (qq.com)
相关文章:

【Spring Security系列】权限之旅:SpringSecurity小程序登录深度探索
作者:后端小肥肠 创作不易,未经允许严禁转载。 姊妹篇: 【Spring Security系列】Spring SecurityJWTRedis实现用户认证登录及登出_spring security jwt 退出登录-CSDN博客 1. 前言 欢迎来到【Spring Security系列】!在当今数字化…...

【收录 Hello 算法】第 10 章 搜索
目录 第 10 章 搜索 本章内容 第 10 章 搜索 搜索是一场未知的冒险,我们或许需要走遍神秘空间的每个角落,又或许可以快速锁定目标。 在这场寻觅之旅中,每一次探索都可能得到一个未曾料想的答案。 本章内容 10.1 二分查找10.2 二…...
【MySQL精通之路】SQL优化(1)-查询优化(11)-多范围查询优化
主博客: 【MySQL精通之路】SQL优化(1)-查询优化-CSDN博客 上一篇: 【MySQL精通之路】SQL优化(1)-查询优化(10)-外部联接简化-CSDN博客 下一篇: 当基表很大且未存储在存储引擎的缓存中时,使用辅助索引上的范围扫描读取行可能会…...

Mysql之基本架构
1.Mysql简介 mysql是一种关系型数据库,由表结构来存储数据与数据之间的关系,同时为sql(结构化查询语句)来进行数据操作。 sql语句进行操作又分为几个重要的操作类型 DQL: Data Query Language 数据查询语句 DML: Data Manipulation Language 添加、删…...
Python学习---基于正则表达式的简单爬取电影下载信息案例
一、定义函数获取列表页的内容页地址 get_movie_links() 1、定义列表的地址 2、打开url地址,获取数据 3、解码获取到的数据 4、使用正则得到所有的影片内容也地址 4.1 遍历,取出内容页地址 4.2 拼接内容页地址 4.3 打开内容页地址 4.4 获…...

.DS_store文件
感觉mac里的这个.DS_store文件烦人,老是莫名其妙的出现,然后造成困扰 处理方式如下: import os pic_list os.listdir("./mask_pic/") print(len(pic_list)) # 从文件夹中删掉 if(".DS_Store" in pic_list):print(&quo…...

【webrtc】内置opus解码器的移植
m98 ,不知道是什么版本的opus,之前的交叉编译构建: 【mia】ffmpeg + opus 交叉编译 【mia】ubuntu22.04 : mingw:编译ffmpeg支持opus编解码 看起来是opus是1.3.1 只需要移植libopus和opus的webrtc解码部分即可。 linux构建的windows可运行的opus库 G:\NDDEV\aliply-0.4\C…...
Java注解:讲解Java注解(Annotations)的概念,使用,并展示如何自定义注解,甚至框架级别的使用说明
1. 注解的概念 1.1 介绍Annotation的基础概念 Java注解(Annotation)是Java 5.0及更高版本中引入的一种元数据(meta-data),即数据的数据。它以一种形式附着在代码中,但是对代码的运行不产生直接效果。注解可以用于创建文档、追踪代码依赖性、甚至执行编译期版错误检查等…...
二维矩阵乘法案例
二维矩阵相乘计算原理:第一个矩阵的每一行分别与第二个矩阵的每一列做向量点乘,将所得结果填入新矩阵相应的位置。 例如,给定矩阵 A [ [1, 2 ], [3, 4] ]和 B [ [5, 6 ], [7, 8] ],它们的乘积AB分别为: AB[ 0 ] [ 0…...

selenium安装出错
selenium安装步骤(法1): 安装失败法1 第一次实验,失败 又试了一次,失败 安装法2-失败: ERROR: Could not install packages due to an EnvironmentError: [WinError 5] 拒绝访问。: c:\\programdata\\a…...

前端中 dayjs 时间的插件使用(在vue 项目中)
Day.js中文网 这是dayjs的中文文档 里面包括了使用方法 下面我来详细介绍一下这个插件的使用 Day.js 可以运行在浏览器和 Node.js 中。 一般咱直接是 npm 安装 npm install dayjs 目前应该使用的是Es6 的语法 import dayjs from dayjs 当前时间 直接调用 dayjs() 将返回…...
tp5问题集记录 一
tp5问题集记录 一 前言车祸现场 前言 在写tp5接口的时候,发现model里面的参数查询出来之后,怎么改都不生效,也是自己不熟悉钻牛角尖了。 车祸现场 例如下面的代码使用model处理预处理 // SPUpublic function getSpuAttr($value, $data){$…...

AGI技术与原理浅析:曙光还是迷失?
前言:回顾以往博客文章,最近一次更新在2020-07,内容以机器学习、深度学习、CV、Slam为主,顺带夹杂个人感悟。笔者并非算法科班出身,本科学制药、研究生学金融,最原始的算法积累都来源于网络,当时…...

探秘机器学习经典:K-近邻算法(KNN)全解析
在浩瀚的机器学习宇宙中,K-近邻算法(K-Nearest Neighbors,简称KNN)如同一颗璀璨的明星,以其简洁直观的原理和广泛的应用范围,赢得了众多数据科学家的喜爱。今天,让我们一起揭开KNN的神秘面纱,深入探讨它的运作机制、优缺点、应用场景,以及如何在实际项目中灵活运用。 …...

数据可视化每周挑战——全国星巴克门店数据可视化
这是我国星巴克门店的位置,营业时间等数据。 1.导入需要用的库,同时设置绘图时用到的字体,同时防止绘图时负号无法正常显示的情况。 import pandas as pd from pyecharts.charts import Bar,Map,Line,Pie,Geo from pyecharts import option…...
【前端】js通过元素属性获取元素
【前端】js通过元素属性获取元素 <div for"hc_opportunity_config">aaaaa</div>//通过属性获取元素document.querySelector([for"hc_opportunity_config"]) document.querySelector([属性"属性值"])...
申请轻纺行业工程设计资乙级对企业有什么要求
注册资金:企业的注册资金应至少达到三百万,这是衡量企业经济实力和承担风险能力的重要指标。独立法人资格:企业应具备独立的法人资格,能够独立承担民事责任,并具备相应的经营自主权。专业技术人员配备:企业…...
基于单片机电梯控制系统设计与实现
摘 要: 介绍了电梯控制系统架构 , 指出了该系统的硬件设计和控制系统的软件设计以及系统调试 , 使系统可根据按键 要求完成载客任务,为电梯控制系统的优化提供了参考 。 关键词 : 电梯控制 ; 单片机 ; 系统设计 0 引言 在高层建筑中发挥…...

嵌入式单片机笔试题
DC-DC 和 LDO两者有何区别? DC-DC转换器(直流-直流转换器)和LDO(低压差线性稳压器)都是用于电源管理的设备,但它们在原理和特性上有一些显著的区别: 原理: DC-DC转换器通过改变输…...

生活小区火灾预警新篇章:泵吸式可燃气体报警器的检定与运用
在现代化的生活小区中,燃气设备广泛应用于居民的日常生活之中,但同时也带来了潜在的火灾风险。 可燃气体报警器作为一种安全监测设备,能够及时检测到燃气泄漏等安全隐患,并在达到预设的阈值时发出警报,提醒居民采取相…...
求解插值多项式及其余项表达式
例 求满足 P ( x j ) f ( x j ) P(x_j) f(x_j) P(xj)f(xj) ( j 0 , 1 , 2 j0,1,2 j0,1,2) 及 P ′ ( x 1 ) f ′ ( x 1 ) P(x_1) f(x_1) P′(x1)f′(x1) 的插值多项式及其余项表达式。 解: 由给定条件,可确定次数不超过3的插值多项式。…...
【C++快读快写】
算法竞赛中用于解决卡常问题 int rd(){int k 0;char c getchar();while(!isdigit(c)){c getchar();}while(isdigit(c)){k (k << 1) (k << 3) (c^0), c getchar();}return k; }void wr(int x) {if (x > 9)wr(x / 10);putchar((x % 10) ^ 0); }用法&#x…...

《Progressive Transformers for End-to-End Sign Language Production》复现报告
摘要 本文复现了《Progressive Transformers for End-to-End Sign Language Production》一文中的核心模型结构。该论文提出了一种端到端的手语生成方法,能够将自然语言文本映射为连续的 3D 骨架序列,并引入 Counter Decoding 实现动态序列长度控制。我…...

家政小程序开发——AI+IoT技术融合,打造“智慧家政”新物种
基于用户历史订单(如“每周一次保洁”)、设备状态(如智能门锁记录的清洁频率),自动生成服务计划。 结合天气数据(如“雨天推荐玻璃清洁”),动态推送服务套餐。 IoT设备联动&#x…...

STM32使用土壤湿度传感器
1.1 介绍: 土壤湿度传感器是一种传感装置,主要用于检测土壤湿度的大小,并广泛应用于汽车自动刮水系统、智能灯光系统和智能天窗系统等。传感器采用优质FR-04双料,大面积5.0 * 4.0厘米,镀镍处理面。 它具有抗氧化&…...
CppCon 2015 学习:Intro to the C++ Object Model
这段代码展示了使用 make 工具来编译 C 程序的简单过程。 代码和步骤解析: C 代码(intro.cpp):#include <iostream> int main() { std::cout<<"hello world\n"; } 这是一个简单的 C 程序,它包…...

html 滚动条滚动过快会留下边框线
滚动条滚动过快时,会留下边框线 但其实大部分时候是这样的,没有多出边框线的 滚动条滚动过快时留下边框线的问题通常与滚动条样式和滚动行为有关。这种问题可能出现在使用了自定义滚动条样式的情况下。 注意:使用方法 6 好使,其它…...
【CSS-4】掌握CSS文字样式:从基础到高级技巧
文字是网页内容的核心载体,良好的文字样式设计不仅能提升可读性,还能增强网站的整体视觉效果。本文将全面介绍CSS中控制文字样式的各种属性和技巧,帮助您打造专业级的网页排版。 1. 基础文字属性 1.1 字体设置 (font-family) body {font-f…...

作为过来人,浅谈一下高考、考研、读博
写在前面 由于本人正在读博,标题中的三个阶段都经历过或正在经历,本意是闲聊,也算是给将要经历的读者们做个参考、排雷。本文写于2022年,时效性略有落后,不过逻辑上还是值得大家参考,若所述存在偏颇&#…...
# STM32F103 SD卡读写程序
下面是一个基于STM32F103系列微控制器的SD卡读写完整程序,使用标准外设库(StdPeriph)和FatFs文件系统。 硬件准备 STM32F103C8T6开发板(或其他F103系列)SD卡模块(SPI接口)连接线缆 硬件连接 SD卡模块 STM32F103 CS -> PA4 (SPI1_NSS) SCK -> PA5 (SPI…...