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

news2024/9/21 23:05:54

作者:后端小肥肠

🍇 我写过的文章中的相关代码放到了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()); //Created
        String nonce = UUID.randomUUID().toString().replace("-", ""); //Nonce

        MessageDigest 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"); // 拦截短信验证码登录请求
    }

    @Override
    public 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();

        // 创建验证请求的 Token
        SmsAuthenticationToken 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;
    }

    @Override
    public 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("未找到对应的用户,请先注册");
        }

        // 创建已认证的Token
        return new SmsAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }

    @Override
    public 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);
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

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

3.3.4. 配置WebSecurityConfigurerAdapter

新增验证码过滤

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

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

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

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

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

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

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

3.4. 效果测试

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

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

4. 结语

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

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

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

相关文章

拖拽排序的实现示例demo

拖拽排序的实现示例demo 文章说明核心代码示例效果展示 文章说明 文章主要为了学习拖拽排序的实现思路&#xff0c;并且采用此示例效果来进一步理解Flip动画的使用 参考渡一前端袁老师的讲解视频 核心代码 页面源码&#xff0c;拖拽排序的实现代码并不复杂&#xff0c;但是可以…

我的标志:奇特的头像

<!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>与妖为邻</title><style>figure.log…

C++11(4)

万众瞩目的C11特辑来了&#xff0c;本章将继续讲解C11更新的内容&#xff0c;不过C11的内容也快接近尾声了。 目录 10。lambda表达式 11。lambda捕捉列表[] 捕捉列表说明 lambda捕捉列表实际应用 10。lambda表达式 #include<iostream> using namespace std; #inclu…

手把手教你:在微信小程序中加载map并实现拖拽添加标记定位

本文将为大家详细介绍如何在微信小程序中加载map组件&#xff0c;并实现拖拽标记定位功能。 实现步骤 1、首先&#xff0c;我们需要在项目的app.json文件中添加map组件的相关配置。如下所示&#xff1a; {"pages": ["pages/index/index"],"permiss…

robomimic基础教程(三)——自带算法

robomimic自带几个高质量的离线学习算法的实现&#xff0c;包括模仿学习和强化学习&#xff0c;并提供相关工具来辅助你轻松构建自己的学习算法。 一、模仿学习&#xff08;Imitation Learning&#xff09; 1. BC (Behavioral Cloning) Vanilla Behavioral Cloning, 旨在通过…

使用knn算法对iris数据集进行分类

程序功能 使用 scikit-learn 库中的鸢尾花数据集&#xff08;Iris dataset&#xff09;&#xff0c;并基于 KNN&#xff08;K-Nearest Neighbors&#xff0c;K近邻&#xff09;算法进行分类&#xff0c;最后评估模型的准确率。 代码 from sklearn import datasets# 加载鸢尾…

链表在开空间时候出现的问题

题目&#xff1a; 第一种写法完整答案&#xff1a; 第二种写法完整答案&#xff1a;

【机器学习】--- 自监督学习

1. 引言 机器学习近年来的发展迅猛&#xff0c;许多领域都在不断产生新的突破。在监督学习和无监督学习之外&#xff0c;自监督学习&#xff08;Self-Supervised Learning, SSL&#xff09;作为一种新兴的学习范式&#xff0c;逐渐成为机器学习研究的热门话题之一。自监督学习…

【C++题解】1996. 每个小组的最大年龄

欢迎关注本专栏《C从零基础到信奥赛入门级&#xff08;CSP-J&#xff09;》 问题&#xff1a;1996. 每个小组的最大年龄 类型&#xff1a;二维数组 题目描述&#xff1a; 同学们在操场上排成了一个 n 行 m 列的队形&#xff0c;每行的同学属于一个小组&#xff0c;请问每个小…

PCIe进阶之TL:Completion Rules TLP Prefix Rules

1 Completion Rules & TLP Prefix Rules 1.1 Completion Rules 所有的 Read、Non-Posted Write 和 AtomicOp Request 都需要返回一个 Completion。Completion 有两种类型:一种带数据负载的,一种不带数据负载的。以下各节定义了 Completion header 中每个字段的规则。 C…

【磨皮美白】基于Matlab的人像磨皮美白处理算法,Matlab处理

博主简介&#xff1a;matlab图像代码项目合作&#xff08;扣扣&#xff1a;3249726188&#xff09; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 本次案例是基于Matlab的图像磨皮美白处理&#xff0c;用matlab实现。 一、案例背景和算法介绍 …

【图像匹配】基于SURF算法的图像匹配,matlab实现

博主简介&#xff1a;matlab图像代码项目合作&#xff08;扣扣&#xff1a;3249726188&#xff09; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 本次案例是基于基于SURF算法的图像匹配&#xff0c;用matlab实现。 一、案例背景和算法介绍 前…

7天速成前端 ------学习日志 (继苍穹外卖之后)

前端速成计划总结&#xff1a; 全26h课程&#xff0c;包含html&#xff0c;css&#xff0c;js&#xff0c;vue3&#xff0c;预计7天内学完。 起始日期&#xff1a;9.16 预计截止&#xff1a;9.22 每日更新&#xff0c;学完为止。 学前计划 课…

文字loading加载

效果 1. 导入库 import sys from PyQt5.QtCore import QTimer, Qt, QThread, pyqtSignal from PyQt5.QtGui import QPainter, QFont, QColor, QBrush from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QProgressBar, QLabel 代码首先导入了P…

编辑器拓展(入门与实践)

学习目标:入门编辑器并实现几个简单的工具 菜单编辑器 MenuItem [MenuItem("编辑器拓展/MenuItem")]static void MenuItem(){Debug.Log("这是编辑器拓展");} } 案例 1&#xff1a;在场景中的 GameObject 设置 1. 设置面板2. 直接创建 GameObject 结构…

2-96 基于matlab的SMOTE数据扩充算法

基于matlab的SMOTE数据扩充算法&#xff0c;主动设置数据扩充百分比&#xff0c;并考虑最近邻居数进行扩充&#xff0c;计算样本到他所在类样本集中所有样本距离&#xff0c;从样本的K近邻中随机选择若干样本添加到扩充样本集。程序已调通&#xff0c;可直接运行。 下载源程序…

c++中引用是通过指针的方式实现

其实在汇编层面上&#xff0c;引用的代码和指针的代码是一致的。 先看指针情况下的代码分析&#xff0c;如下所示&#xff1a; #include <iostream>using namespace std;void fuzhi(int *x)//引用传参 {*x 10; }int main(int argc, char** argv) {int a 0;int b;a …

LeetCode[简单] 283.移动零

给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 请注意 &#xff0c;必须在不复制数组的情况下原地对数组进行操作。 思路&#xff1a;利用快慢指针&#xff0c;快指针遍历数组&#xff0c;慢指针是非零元素索…

【D3.js in Action 3 精译_023】3.3 使用 D3 将数据绑定到 DOM 元素

当前内容所在位置&#xff1a; 第一部分 D3.js 基础知识 第一章 D3.js 简介&#xff08;已完结&#xff09; 1.1 何为 D3.js&#xff1f;1.2 D3 生态系统——入门须知1.3 数据可视化最佳实践&#xff08;上&#xff09;1.3 数据可视化最佳实践&#xff08;下&#xff09;1.4 本…

销管系统 —— P14 菜单项悬停高亮显示遇到的问题

悬停在子菜单背景颜色并没有显示&#xff0c;为什么&#xff1a; 什么是后代选择器 —— 选中父元素 后代中 满足条件的元素&#xff1b;这个子菜单menu—item它既满足上面的也满足下面的&#xff0c;按这个顺序的话&#xff0c;下面的就被覆盖了&#xff08;CSS优先级规则&…