【Spring Security系列】如何用Spring Security集成手机验证码登录?五分钟搞定!

作者:后端小肥肠

🍇 我写过的文章中的相关代码放到了gitee,地址:xfc-fdw-cloud: 公共解决方案

🍊 有疑问可私信或评论区联系我。

🥑  创作不易未经允许严禁转载。

姊妹篇:

【Spring Security系列】基于Spring Security实现权限动态分配之菜单-角色分配及动态鉴权实践_spring secrity权限角色动态管理-CSDN博客

【Spring Security系列】基于Spring Security实现权限动态分配之用户-角色分配_spring security 角色-CSDN博客

【Spring Security系列】权限之旅:SpringSecurity小程序登录深度探索_spring security 微信小程序登录-CSDN博客

【Spring Security系列】Spring Security+JWT+Redis实现用户认证登录及登出_spring security jwt 退出登录-CSDN博客

目录

1. 前言

2. 注册

2.1. 手机验证码注册流程

2.2. 代码实现(仅核心)

3. 登录

3.1. 手机验证码登录流程

3.2. 涉及到的Spring Security组件

3.3. 代码实现(仅核心)

3.3.1.  编写SmsAuthenticationFilter

3.3.2.  编写SmsAuthenticationProvider

3.3.3.  编写SmsAuthenticationToken

3.3.4. 配置WebSecurityConfigurerAdapter

3.4. 效果测试

4. 结语


1. 前言

在当今的互联网应用中,手机验证码登录已经成为一种常见的用户身份验证方式。相比传统的用户名密码登录方式,手机验证码具有使用方便、安全性较高的特点。对于开发者来说,如何在现有的系统中快速集成这一功能,尤其是在Spring Security框架下,可能是一个具有挑战性的任务。这篇文章将详细介绍如何利用Spring Security来实现手机验证码的注册和登录功能,帮助你在短时间内搞定这一需求。

2. 注册

2.1. 手机验证码注册流程

以下是对流程图的具体分析:

  1. 前端请求和手机号码处理

    • 用户发起获取验证码的请求,后端接收手机号码,生成随机验证码并存储在Redis中,这部分流程是标准的短信验证流程。
    • 在存储到Redis时明确了验证码的有效时间(5分钟)。
  2. 验证码发送

    • 验证码通过调用短信服务发送,这里需要自行选择像阿里云、华为云等短信发送平台。
  3. 用户验证和注册提交

    • 用户收到验证码后,在前端输入验证码并提交注册请求。
    • 系统从Redis中获取验证码并与用户输入的验证码进行匹配。
    • 如果匹配成功,注册流程继续进行并完成注册。
    • 如果匹配失败,提示用户验证码错误。

2.2. 代码实现(仅核心)

1. 匹配短信消息发送相关参数(以华为云为例)

2. 编写短信发送工具类

@Component
public class SendSmsUtil {@Value("${huawei.sms.url}")private String url;@Value("${huawei.sms.appKey}")private String appKey;@Value("${huawei.sms.appSecret}")private String appSecret;@Value("${huawei.sms.sender}")private String sender;@Value("${huawei.sms.signature}")private String signature;/*** 无需修改,用于格式化鉴权头域,给"X-WSSE"参数赋值*/private static final String WSSE_HEADER_FORMAT = "UsernameToken Username=\"%s\",PasswordDigest=\"%s\",Nonce=\"%s\",Created=\"%s\"";/*** 无需修改,用于格式化鉴权头域,给"Authorization"参数赋值*/private static final String AUTH_HEADER_VALUE = "WSSE realm=\"SDP\",profile=\"UsernameToken\",type=\"Appkey\"";public void sendSms(String templateId,String receiver, String templateParas) throws IOException {String body = buildRequestBody(sender, receiver, templateId, templateParas, "", signature);String wsseHeader = buildWsseHeader(appKey, appSecret);HttpsURLConnection connection = null;OutputStreamWriter out = null;BufferedReader in = null;StringBuilder result = new StringBuilder();try {URL realUrl = new URL(url);connection = (HttpsURLConnection) realUrl.openConnection();connection.setDoOutput(true);connection.setDoInput(true);connection.setRequestMethod("POST");connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");connection.setRequestProperty("Authorization", "WSSE realm=\"SDP\",profile=\"UsernameToken\",type=\"Appkey\"");connection.setRequestProperty("X-WSSE", wsseHeader);out = new OutputStreamWriter(connection.getOutputStream());out.write(body);out.flush();int status = connection.getResponseCode();InputStream is;if (status == 200) {is = connection.getInputStream();} else {is = connection.getErrorStream();}in = new BufferedReader(new InputStreamReader(is, "UTF-8"));String line;while ((line = in.readLine()) != null) {result.append(line);}System.out.println(result.toString());} catch (Exception e) {e.printStackTrace();} finally {if (out != null) {out.close();}if (in != null) {in.close();}if (connection != null) {connection.disconnect();}}}/*** 构造请求Body体* @param sender* @param receiver* @param templateId* @param templateParas* @param statusCallBack* @param signature | 签名名称,使用国内短信通用模板时填写* @return*/static String buildRequestBody(String sender, String receiver, String templateId, String templateParas,String statusCallBack, String signature) {if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()|| templateId.isEmpty()) {System.out.println("buildRequestBody(): sender, receiver or templateId is null.");return null;}Map<String, String> map = new HashMap<String, String>();map.put("from", sender);map.put("to", receiver);map.put("templateId", templateId);if (null != templateParas && !templateParas.isEmpty()) {map.put("templateParas", templateParas);}if (null != statusCallBack && !statusCallBack.isEmpty()) {map.put("statusCallback", statusCallBack);}if (null != signature && !signature.isEmpty()) {map.put("signature", signature);}StringBuilder sb = new StringBuilder();String temp = "";for (String s : map.keySet()) {try {temp = URLEncoder.encode(map.get(s), "UTF-8");} catch (UnsupportedEncodingException e) {e.printStackTrace();}sb.append(s).append("=").append(temp).append("&");}return sb.deleteCharAt(sb.length()-1).toString();}/*** 构造X-WSSE参数值* @param appKey* @param appSecret* @return*/static String buildWsseHeader(String appKey, String appSecret) {if (null == appKey || null == appSecret || appKey.isEmpty() || appSecret.isEmpty()) {System.out.println("buildWsseHeader(): appKey or appSecret is null.");return null;}SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");String time = sdf.format(new Date()); //CreatedString nonce = UUID.randomUUID().toString().replace("-", ""); //NonceMessageDigest md;byte[] passwordDigest = null;try {md = MessageDigest.getInstance("SHA-256");md.update((nonce + time + appSecret).getBytes());passwordDigest = md.digest();} catch (NoSuchAlgorithmException e) {e.printStackTrace();}//如果JDK版本是1.8,请加载原生Base64类,并使用如下代码String passwordDigestBase64Str = Base64.getEncoder().encodeToString(passwordDigest); //PasswordDigest//如果JDK版本低于1.8,请加载三方库提供Base64类,并使用如下代码//String passwordDigestBase64Str = Base64.encodeBase64String(passwordDigest); //PasswordDigest//若passwordDigestBase64Str中包含换行符,请执行如下代码进行修正//passwordDigestBase64Str = passwordDigestBase64Str.replaceAll("[\\s*\t\n\r]", "");return String.format(WSSE_HEADER_FORMAT, appKey, passwordDigestBase64Str, nonce, time);}/*** @throws Exception*/static void trustAllHttpsCertificates() throws Exception {TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() {public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {return;}public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {return;}public X509Certificate[] getAcceptedIssuers() {return null;}}};SSLContext sc = SSLContext.getInstance("SSL");sc.init(null, trustAllCerts, null);HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());}
}

上述工具类 SendSmsUtil 是一个用于通过华为云短信服务发送短信验证码的工具类。它通过构建请求体和鉴权头信息,将短信发送请求发送到华为短信服务接口。该类包含了短信发送的核心逻辑,包括生成X-WSSE头用于请求认证、构造请求体以及处理HTTPS连接的相关逻辑。同时,工具类还包含了信任所有HTTPS证书的设置,以确保与华为云服务器的安全连接。 

3. 发送验证码函数方法

    public String sendSMS(SendSMSDTO sendSMSDTO) throws IOException {String phone = sendSMSDTO.getPhone();String captcha = generateCaptcha();String redisKey = sendSMSDTO.getCaptchaType().equals(0)? REDIS_REGISTER_CAPTCHA_KEY + phone: REDIS_LOGIN_CAPTCHA_KEY + phone;String message = sendSMSDTO.getCaptchaType().equals(0)? "发送注册短信验证码:{}": "发送登录短信验证码:{}";sendSmsUtil.sendSms(templateId, phone, "[\"" + captcha + "\"]");log.info(message, captcha);redisUtils.set(redisKey, captcha, 300);return "发送短信成功";}

上述代码实现了一个短信验证码发送流程。首先,通过 generateCaptcha() 方法生成一个验证码,并调用 sendSmsUtil.sendSms() 将验证码发送到用户的手机号码。短信发送后,利用日志记录了发送的验证码。接着,验证码被存储在 Redis 中,键为手机号加上特定前缀,且设置了300秒的有效期。最后,返回一个短信发送成功的消息。

之后还有提交注册时的验证,这个较为简单,不做讲解,本来发送验证码函数我都不想写的╮(╯▽╰)╭。 

3. 登录

3.1. 手机验证码登录流程

以下是对流程图的具体分析:

  1. 验证码发送流程

    • 流程依然从用户请求验证码开始,后端接收手机号并生成验证码,通过短信服务平台(如阿里云、华为云)发送验证码。
  2. 验证码验证及登录提交

    • 用户收到验证码后输入并提交登录请求,系统从Redis中获取存储的验证码,与用户输入的验证码进行匹配。
    • 如果验证码匹配失败,系统会提示用户验证码错误。
  3. 用户信息查询及Token生成

    • 当验证码匹配成功后,系统会进一步查询用户信息,检查是否存在有效的用户账号。
    • 如果用户信息存在,系统生成Token完成登录,确保用户的身份验证。

3.2. 涉及到的Spring Security组件

要实现手机验证码登录,我们需要灵活使用Spring Security的认证流程,并在其中引入自定义的验证码验证逻辑。以下是关键的Spring Security组件及其在实现手机验证码登录时的作用:

1. AuthenticationManager

AuthenticationManager 是Spring Security认证的核心组件,负责处理不同的认证请求。我们可以自定义一个 AuthenticationProvider 来处理手机验证码的认证逻辑,并将其注入到 AuthenticationManager 中。这样当用户提交验证码登录请求时, AuthenticationManager 会调用我们的自定义认证提供者进行验证。

2. AuthenticationProvider

AuthenticationProvider 是处理认证逻辑的核心接口。为了支持手机验证码登录,我们需要实现一个自定义的 AuthenticationProvider,其中包含以下逻辑:

  • 接收包含手机号和验证码的登录请求。
  • 验证Redis中存储的验证码是否与用户输入的验证码匹配。
  • 验证成功后,创建并返回 Authentication 对象,表示用户已通过认证。

3. UserDetailsService

UserDetailsService 是Spring Security中用于加载用户信息的接口。我们可以通过实现 UserDetailsService 来查询和加载用户信息,比如通过手机号查询用户的详细信息(包括权限、角色等)。如果用户信息存在且验证码验证通过,系统将生成相应的 UserDetails 对象,并将其与Spring Security的认证上下文进行关联。

4. AuthenticationToken

在Spring Security中,AuthenticationToken 是认证过程中传递用户凭据的对象。我们需要自定义一个 SmsAuthenticationToken,用于封装手机号和验证码,并传递给 AuthenticationProvider 进行处理。这个Token类需要继承自 AbstractAuthenticationToken,并包含手机号和验证码信息。

5. SecurityConfigurerAdapter

SecurityConfigurerAdapter 是Spring Security配置的核心类,用于配置Spring Security的各种安全策略。为了集成手机验证码登录,我们需要扩展 SecurityConfigurerAdapter 并在其中配置我们的 AuthenticationProvider 和自定义的登录过滤器。

6. 自定义过滤器

为了支持手机验证码登录,我们可以自定义一个类似的过滤器 SmsAuthenticationFilter,在其中获取用户的手机号和验证码,然后交给 AuthenticationManager 进行处理。这个过滤器将拦截验证码登录请求,并调用 AuthenticationProvider 进行验证。

7. SecurityContextHolder

SecurityContextHolder 是Spring Security中用于存储当前认证信息的类。在用户成功通过验证码登录认证后,系统会将 Authentication 对象存储到 SecurityContextHolder 中,表明当前用户已经成功登录。

3.3. 代码实现(仅核心)

3.3.1.  编写SmsAuthenticationFilter
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {public static final String PHONE_KEY = "phone";  // 手机号字段public static final String CAPTCHA_KEY = "captcha";  // 验证码字段private boolean postOnly = true;private final ObjectMapper objectMapper = new ObjectMapper();public SmsAuthenticationFilter() {super("/sms/login"); // 拦截短信验证码登录请求}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {if (postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}String phone;String captcha;try {// 读取请求体中的 JSON 数据并解析Map<String, String> requestBody = objectMapper.readValue(request.getInputStream(), Map.class);phone = requestBody.get(PHONE_KEY);  // 获取手机号captcha = requestBody.get(CAPTCHA_KEY);  // 获取验证码} catch (IOException e) {throw new AuthenticationServiceException("Failed to parse authentication request body", e);}if (phone == null) {phone = "";}if (captcha == null) {captcha = "";}phone = phone.trim();// 创建验证请求的 TokenSmsAuthenticationToken authRequest = new SmsAuthenticationToken(phone, captcha);return this.getAuthenticationManager().authenticate(authRequest);}public void setPostOnly(boolean postOnly) {this.postOnly = postOnly;}
}

上述代码实现了一个 SmsAuthenticationFilter,用于处理短信验证码登录请求。它继承了 AbstractAuthenticationProcessingFilter,并在接收到 POST 请求时从请求体中解析手机号和验证码的 JSON 数据,创建一个 SmsAuthenticationToken,然后通过 Spring Security 的认证管理器进行身份验证。如果请求不是 POST 方法或解析 JSON 失败,会抛出相应的异常。 

3.3.2.  编写SmsAuthenticationProvider
public class SmsAuthenticationProvider implements AuthenticationProvider {private final UserDetailsService userDetailsService;private final RedisUtils redisUtils;public SmsAuthenticationProvider(UserDetailsService userDetailsService, RedisUtils redisUtils) {this.userDetailsService = userDetailsService;this.redisUtils = redisUtils;}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {String phone = (String) authentication.getPrincipal();  // 获取手机号String captcha = (String) authentication.getCredentials();  // 获取验证码if(!redisUtils.hasKey(REDIS_LOGIN_CAPTCHA_KEY + phone)){throw new BadCredentialsException("验证码已过期");}// 验证码是否正确String redisCaptcha = redisUtils.get(REDIS_LOGIN_CAPTCHA_KEY + phone).toString();if (redisCaptcha == null || !redisCaptcha.equals(captcha)) {throw new BadCredentialsException("验证码错误");}// 验证用户信息UserDetails userDetails = userDetailsService.loadUserByUsername(phone);if (userDetails == null) {throw new BadCredentialsException("未找到对应的用户,请先注册");}// 创建已认证的Tokenreturn new SmsAuthenticationToken(userDetails, null, userDetails.getAuthorities());}@Overridepublic boolean supports(Class<?> authentication) {return SmsAuthenticationToken.class.isAssignableFrom(authentication);}
}

上述代码实现了一个 SmsAuthenticationProvider,用于处理短信验证码登录的身份验证逻辑。它通过 UserDetailsService 加载用户信息,并使用 RedisUtils 从 Redis 中获取验证码进行比对。如果验证码不存在或不匹配,会抛出 BadCredentialsException 异常。如果验证码正确且用户存在,则生成已认证的 SmsAuthenticationToken 并返回,完成用户身份验证。该类还定义了它支持的身份验证类型为 SmsAuthenticationToken。 

3.3.3.  编写SmsAuthenticationToken
public class SmsAuthenticationToken extends AbstractAuthenticationToken {private final Object principal;private Object credentials;public SmsAuthenticationToken(Object principal, Object credentials) {super(null);this.principal = principal; // 用户的手机号this.credentials = credentials; // 验证码setAuthenticated(false);}public SmsAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;this.credentials = credentials;setAuthenticated(true);}@Overridepublic Object getCredentials() {return this.credentials;}@Overridepublic Object getPrincipal() {return this.principal;}@Overridepublic void eraseCredentials() {super.eraseCredentials();this.credentials = null;}
}

上述代码实现了一个自定义的 SmsAuthenticationToken,继承自 AbstractAuthenticationToken,用于表示短信验证码登录的认证信息。它包含用户的手机号 (principal) 和验证码 (credentials) 两个字段,并提供两种构造方法:一种用于未认证的登录请求,另一种用于已认证的用户信息。通过 getPrincipal() 获取手机号,getCredentials() 获取验证码,并且在调用 eraseCredentials() 时清除验证码以增强安全性。 

3.3.4. 配置WebSecurityConfigurerAdapter

新增验证码过滤

  // 添加短信验证码过滤器http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

定义短信验证码认证过滤器,设置认证管理器及认证成功和失败的处理器。

    @Beanpublic SmsAuthenticationFilter smsAuthenticationFilter() throws Exception {SmsAuthenticationFilter filter = new SmsAuthenticationFilter();filter.setAuthenticationManager(authenticationManagerBean());  // 设置认证管理器filter.setAuthenticationSuccessHandler(securAuthenticationSuccessHandler);  // 设置成功处理器filter.setAuthenticationFailureHandler(securAuthenticationFailureHandler);  // 设置失败处理器return filter;}

 定义短信验证码认证提供者,注入用户详情服务和 Redis 工具类,用于处理短信验证码的认证逻辑。

    @Beanpublic SmsAuthenticationProvider smsAuthenticationProvider() {return new SmsAuthenticationProvider(smeUserDetailsService,redisUtils);}

配置认证管理器,添加短信验证码、微信登录以及用户名密码的认证提供者。

    @Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// 添加短信验证码认证提供者auth.authenticationProvider(smsAuthenticationProvider());// 添加微信登录认证提供者auth.authenticationProvider(weChatAuthenticationProvider());// 添加用户名密码登录认证提供者auth.authenticationProvider(daoAuthenticationProvider());}

3.4. 效果测试

基于上述的手机验证码登录代码,我们来测试一下接口成果:

 到此圆满完结✿✿ヽ(°▽°)ノ✿

4. 结语

通过以上步骤,我们成功实现了基于Spring Security的手机验证码登录功能。无论是注册流程中的验证码发送与验证,还是登录时的身份认证,Spring Security提供了足够的灵活性,让我们能够快速集成这项功能。在实际应用中,开发者可以根据自身需求进一步优化和扩展,比如增加更复杂的验证逻辑或增强安全性。希望本教程能帮助你轻松解决验证码登录的问题,让开发过程更加顺畅高效。

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

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

相关文章

今天中秋,中秋快乐,分析一个中秋月饼的项目

特色功能 使用obj模型&#xff0c;搭配tga文件&#xff0c;附加上颜色 normalMap 是让字和线条看起来更清楚和真实 高光贴图 凹凸贴图 ...... 源码 https://github.com/Lonely1201/lonely1201.github.io/tree/main/Juejin/mooncake 在线预览 https://lonely1201.githu…

TensorRT-LLM——优化大型语言模型推理以实现最大性能的综合指南

引言 随着对大型语言模型 (LLM) 的需求不断增长&#xff0c;确保快速、高效和可扩展的推理变得比以往任何时候都更加重要。NVIDIA 的 TensorRT-LLM 通过提供一套专为 LLM 推理设计的强大工具和优化&#xff0c;TensorRT-LLM 可以应对这一挑战。TensorRT-LLM 提供了一系列令人印…

828华为云征文 | 华为云X实例的镜像管理详解

前言 随着云计算的不断普及&#xff0c;云服务器成为企业和开发者日常工作中的重要工具。为了提升工作效率和降低运维成本&#xff0c;云服务器镜像的管理尤为重要。镜像作为服务器或磁盘的模板&#xff0c;预装了操作系统、软件及配置&#xff0c;是快速部署和迁移业务的重要…

【Linux】进程序言

这里是阿川的博客&#xff0c;祝您变得更强 ✨ 个人主页&#xff1a;在线OJ的阿川 &#x1f496;文章专栏&#xff1a;Linux入门到进阶 &#x1f30f;代码仓库&#xff1a; 写在开头 现在您看到的是我的结论或想法&#xff0c;但在这背后凝结了大量的思考、经验和讨论 目录 1.…

【例题】lanqiao4397 图书排序

在希尔排序的基础上&#xff0c;对数组(w0,id0)进行排序&#xff0c;先排权重w&#xff0c;再排id. nint(input()) w[] for _ in range(n):id0,w0map(int,input().split())w.append((w0,id0)) def shell_sort(a):gapn//2while gap>0:for i in range(gap,n):tmpa[i]jiwhile …

水下目标检测数据集 urpc2021

项目背景&#xff1a; 水下目标检测在海洋科学研究、水下考古、海洋资源勘探等多个领域具有重要的应用价值。由于水下环境的复杂性和多变性&#xff0c;传统的人工检测方法存在诸多限制&#xff0c;自动化检测技术的需求日益增加。URPC2021数据集旨在为水下目标检测提供高质量…

【C++】STL数据结构最全函数详解2-向量vector

关于STL&#xff0c;我们之前浅浅提过&#xff1a;这里 另外对于栈&#xff0c;这里有更加详尽的介绍&#xff1a;CSTL常用数据结构1详解---栈&#xff08;stack&#xff09;-CSDN博客 这个系列将会更加深入地从函数原型开始用详细的例子解释用法 首先这一篇介绍的是一个非常…

macOS Sequoia发布:Apple又给我们带来了什么惊喜?

今天是个激动人心的日子,尤其是对于Mac用户来说,因为Apple正式发布了全新的操作系统——macOS Sequoia。作为一款专为Mac设备设计的操作系统,Sequoia不仅仅是简单的升级,它承载了Apple在系统体验上的巨大飞跃。听到这个消息,你可能会好奇,Apple这次又会带来什么样的创新?…

ABC371E I Hate Sigma Problems 题解

ABC371E I Hate Sigma Problems 题解 题目描述问题陈述限制因素 样例1解析题解(1) 暴力枚举做法代码运行结果 (2) 暴力优化做法代码运行结果 正解代码运行结果 结语 题目描述 问题陈述 给你一个长度为 N N N 的整数序列 A ( A 1 , A 2 , … , A N ) A (A_1, A_2, \ldots,…

PyCharm 安装教程

传送门 PyCharm 是一款由 JetBrains 开发的强大的 Python 集成开发环境&#xff08;IDE&#xff09;。它支持多种功能&#xff0c;包括调试、代码补全、智能代码分析、版本控制集成等&#xff0c;特别适合开发 Python 项目。接下来&#xff0c;我们将详细介绍如何在不同操作系…

每日一个数据结构-跳表

文章目录 什么是跳表&#xff1f;示意图跳表的基本原理跳表的操作跳表与其他数据结构的比较 跳表构造过程 什么是跳表&#xff1f; 跳表&#xff08;Skip List&#xff09;是一种随机化的数据结构&#xff0c;它通过在有序链表上增加多级索引来实现快速查找、插入和删除操作。…

<<编码>> 第 12 章 二进制加法器--16位加法器 示例电路

16 位加法器内部结构 info::操作说明 鼠标单击逻辑输入切换 0|1 状态 primary::在线交互操作链接 https://cc.xiaogd.net/?startCircuitLinkhttps://book.xiaogd.net/code-hlchs-examples/assets/circuit/code-hlchs-ch12-10-16-bit-adder-internal.txt 16 位加法器示例 info:…

详解JUC

Java并发工具包&#xff08;Java Util Concurrent&#xff0c; 简称JUC&#xff09;是Java提供的一组用于简化多线程编程的类和接口&#xff0c;它包含了用于线程同步、并发数据结构、线程池、锁、原子操作以及其他并发实用工具的丰富集合。 1. 线程池 线程池是 Java 并发编程…

SOMEIP_ETS_112: SD_Empty_Option

测试目的&#xff1a; 验证DUT能够拒绝长度为0的IPv4选项的SubscribeEventgroup消息&#xff0c;并以SubscribeEventgroupNAck作为响应。 描述 本测试用例旨在确保DUT遵循SOME/IP协议&#xff0c;当接收到一个IPv4选项长度为0的SubscribeEventgroup消息时&#xff0c;能够正…

电子竞技信息交流平台|基于java的电子竞技信息交流平台系统小程序(源码+数据库+文档)

电子竞技信息交流平台系统小程序 目录 基于java的电子竞技信息交流平台系统小程序 一、前言 二、系统设计 三、系统功能设计 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂码农|毕设…

如何设置 Django 错误邮件通知 ?

Django 是一个强大的 web 框架&#xff0c;非常适合那些想要完美快速完成任务的人。它有许多内置的工具和特性&#xff0c;一个有用的特性是 Django 可以在出现错误时发送电子邮件提醒。这对开发人员和管理员非常有用&#xff0c;因为如果出现问题&#xff0c;他们会立即得到通…

基于Springboot的物流管理系统设计与实现(附源代码)

物流管理|物流管理系统|基于Springboot的物流管理系统设计与实现 物流管理系统源码&#xff1a;物流管理系统主要是借助计算机&#xff0c;通过对物流管理系统所需的信息管理&#xff0c;物流管理系统的目的 2.2物流管理系统作用 2.3物流管理系统的发展趋势 3物流管理系统分析…

课堂助手|微信课堂助手系统小程序(源码+数据库+文档)

课堂助手|课堂助手系统小程序 目录 微信课堂助手系统小程序 一、前言 二、系统设计 三、系统功能设计 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂码农|毕设布道师&#xff0c;阿…

react 组件通讯

组件通讯 组件是独立且封闭的单元&#xff0c;默认情况下&#xff0c;只能使用组件自己的数据。在组件化过程中&#xff0c;我们将一个完整的功能拆分成多个组件&#xff0c;以更好的完成整个应用的功能。而在这个过程中&#xff0c;多个组件之间不可避免的要共享某些数据。为…