Spring Security 身份验证的基本类/架构

目录

1、SecurityContextHolder 核心类

2、SecurityContext 接口

3、Authentication 用户认证信息接口

4、GrantedAuthority 拥有权限接口

5、AuthenticationManager 身份认证管理器接口

6、ProviderManager 身份认证管理器的实现

7、AuthenticationProvider 特定类型的身份验证接口

8、AuthenticationEntryPoint 身份验证方案接口

9、AbstractAuthenticationProcessingFilter 用户凭据验证过滤器


        // Spring Security 基于 Servlet 的身份验证和身份验证的基本构件

  • SecurityContextHolder(类)——用来存储身份验证详细信息//验证流程核心类
  • SecurityContext(接口)——从SecurityContextHolder 中获取,包含当前认证用户的身份验证信息
  • Authentication(接口) ——该对象可以通过 AuthenticationManager 进行赋值,作为用户身份验证的凭据,然后通过 SecurityContext 获取 Authentication 的详细信息。
  • GrantedAuthority(接口)——用于对用户的授权
  • AuthenticationManager(接口)——用于定义 Spring Security 过滤器如何执行身份验证的API。
  • ProviderManager(类)——AuthenticationManager 最常见的实现。//常规实现
  • AuthenticationProvider(接口)——提供给ProviderManager,用于执行特定类型的身份验证//身份验证的具体细节由xxxProvider提供
  • AuthenticationEntryPoint(接口)——用于从客户端请求用户凭据(例如,重定向到登录页面,发送WWW-Authenticate响应等)
  • AbstractAuthenticationProcessingFilter(抽象类)——用于身份验证的基本过滤器。通过该类可以很好地了解身份验证流程及各个部分如何协同工作的过程。//所有身份验证的过滤器都会实现该类

1、SecurityContextHolder 核心类

        SecurityContextHolder 类是 Spring Security 身份验证模型的核心, 在该类中包含了存储用户认证信息的上下文 SecurityContext。

        下边是一个对象之间的包含关系图,它很重要,对于理解 Spring Security 中的对象关系非常有用:

        SecurityContextHolder 用来存储身份验证详细信息。Spring Security 并不关心SecurityContextHolder 是如何填充的。如果其中包含值,则使用该值作为当前通过认证的用户信息。//只要SecurityContextHolder有值就会被使用

        所以,指示用户已被认证的最简单方法就是直接设置 SecurityContextHolder:

SecurityContext context = SecurityContextHolder.createEmptyContext(); 

//1-创建Authentication -> Token接口
Authentication authentication = new TestingAuthenticationToken("username", "password", "ROLE_USER"); 
//2-设置用户信息
context.setAuthentication(authentication);

SecurityContextHolder.setContext(context); 

        使用 TestingAuthenticationToken,是因为它非常简单。也可以使用 UsernamePasswordAuthenticationToken(UserDetails、密码、权限)等。

         // 定义一个Authentication -> 放入SecurityContext -> 放入 SecurityContextHolder

        在 SecurityContextHolder 上设置完 SecurityContextSpring Security 会使用此信息进行授权。如果要获取已验证用户的有关信息,可以通过SecurityContextHolder获得:

//1-获取Security上下文
SecurityContext context = SecurityContextHolder.getContext();

//2-获取用户通过身份验证的对象
Authentication authentication = context.getAuthentication();

//-通过Authentication获取用户名称(密码方式)
String username = authentication.getName(); 
//-通过Authentication被认证用户的身份信息。
Object principal = authentication.getPrincipal(); 
//-通过Authentication用户权限信息
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

        默认情况下,SecurityContextHolder 使用 ThreadLocal 来存储 SecurityContext,这意味着 SecurityContext 对于同一线程中的方法总是可用的,并不需要把 SecurityContext 作为参数显式地传递给这些方法。//SecurityContext 属于线程私有,如果我们不想显示传参,也可以这样用

    //包含SecurityContext的成员变量
    private static SecurityContextHolderStrategy strategy; 

    //初始化方法
    private static void initialize() {
        //1-如果没有指定mode,使用MODE_THREADLOCAL
        if (!StringUtils.hasText(strategyName)) { 
            strategyName = "MODE_THREADLOCAL";
        }

        if (strategyName.equals("MODE_THREADLOCAL")) {
            //2-默认策略
            strategy = new ThreadLocalSecurityContextHolderStrategy(); 
        } else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
            strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
        } else if (strategyName.equals("MODE_GLOBAL")) {
            strategy = new GlobalSecurityContextHolderStrategy();
        } else {
            try {
                Class<?> clazz = Class.forName(strategyName);
                Constructor<?> customStrategy = clazz.getConstructor();
                strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
            } catch (Exception var2) {
                ReflectionUtils.handleReflectionException(var2);
            }
        }
        ++initializeCount;
    }

    // MODE_THREADLOCAL策略
    final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

        // 使用ThreadLocal保存SecurityContext
        private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal();

        ThreadLocalSecurityContextHolderStrategy() {
        }
        //省略...
    }

        因为使用了 ThreadLocal 存储值,如果处理完当前用户请求后需要清除线程中的ThreadLocal,避免内存泄漏。Spring SecurityFilterChainProxy会确保SecurityContext总是被清除//FilterChainProxy是一个过滤器链的总代理,在总体架构那篇文章中提到过

    //在FilterChainProxy中的doFilter方法中清除
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
        if (!clearContext) {
            this.doFilterInternal(request, response, chain);
        } else {
            try {
                request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
                this.doFilterInternal(request, response, chain);
            } catch (RequestRejectedException var9) {
                this.requestRejectedHandler.handle((HttpServletRequest)request, (HttpServletResponse)response, var9);
            } finally {
                //处理完一个用户请求时,SecurityContext在这里被清除
                SecurityContextHolder.clearContext();   
                request.removeAttribute(FILTER_APPLIED);
            }
        }
    }

2、SecurityContext 接口

        SecurityContext 从 SecurityContextHolder 中获取。SecurityContext 包含一个 Authentication 对象

// SecurityContext接口
public interface SecurityContext extends Serializable {

    //接口很简单,只包含对Authentication的get和set方法
    Authentication getAuthentication();

    void setAuthentication(Authentication var1);
}

        SecurityContext 的实现类 SecurityContextImpl 也很简单,也就是对 Authentication 对象 get 和 set 方法的实现,所以重点还是 Authentication 对象。

3、Authentication 用户认证信息接口

        在 Spring Security 中,Authentication 接口有两个主要用途:

  1. 作为 AuthenticationManager 对象 authenticate方法的入参,用于对用户进行身份验证。在此场景中,isAuthenticated() 返回 false。
  2. 获取已经过身份验证的用户信息,可以从 SecurityContext 中获取当前的用户信息。

        一旦请求通过了AuthenticationManager#authenticate,Authentication 通常会存储在线程本地的 SecurityContext 中,由 SecurityContextHolder 进行管理。//Authentication->SecurityContext

        注意,除非 Authentication 将 isAuthenticated 属性设置为 true,否则安全拦截器仍然会对其进行身份验证。

        Authentication 接口详情://每个方法的注释都值得好好去读

public interface Authentication extends Principal, Serializable {

    //1-权限:由AuthenticationManager设置,用于指示已授予主体的权限。认证后不能为空(即无权限)
    Collection<? extends GrantedAuthority> getAuthorities();

    //2-密码:证明委托人正确的凭据。通常是密码
    Object getCredentials();

    //3-用户详情:存储有关身份验证请求的其他详细信息。如IP地址,证书序列号等
    Object getDetails();

    //4-用户名:被认证主体的身份。通常是用户名,许多身份验证提供程序将创建UserDetails对象作为主体
    Object getPrincipal();

    //用于指示AbstractSecurityInterceptor是否应该向AuthenticationManager提供身份验证令牌。
    //如果令牌已经过身份验证,并且AbstractSecurityInterceptor不需要再次向AuthenticationManager提供令牌以进行重新身份验证,则为true。
    boolean isAuthenticated();

    //如果令牌应该被信任(这可能导致异常),则为true;如果令牌不应该被信任,则为false
    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

        所以,综合上述,Authentication 包含三方面信息://总结起来就是:用户信息+权限

  1. Principal:用户主体信息,该信息通常是 UserDetails 的一个实例。
  2. Credentials:认证凭据,通常是密码。在许多情况下,用户通过身份验证后会被ProviderManager清除该属性,以确保凭据不会泄露。
  3. Authorities:用户权限,由 GrantedAuthority 进行授予。

4、GrantedAuthority 拥有权限接口

        表示授予身份验证对象的权限。该接口的内容提供给授权管理器AuthorizationManager使用,以确定身份验证是否有权访问特定对象。//保存的是用户角色

//GrantedAuthority接口
public interface GrantedAuthority extends Serializable {
    String getAuthority(); //返回的是一个字符串 String role
}

        // 所以,GrantedAuthority,这个玩意是用来授权的,存储已认证用户具备的访问权限。

        通过 Authentication.getAuthorities() 方法可以获取 GrantedAuthority 的实例。此方法提供了 GrantedAuthority 对象的集合。毫无疑问,GrantedAuthority 是授予用户的权限。这些权限通常是“角色”信息,例如 ROLE_ADMINISTRATOR ROLE_HR_SUPERVISOR。这些角色将配置给 Web 授权、方法授权和域对象授权使用。当使用基于用户名/密码的身份验证时,GrantedAuthority 实例通常由 UserDetailsService 加载。

        通常,GrantedAuthority 对象是应用程序范围的权限。它们并不特定于给定的域对象。因此,不太可能使用 GrantedAuthority 来表示一个对象的具体权限,因为如果有数千个这样的权限,将很快耗尽内存(或者,会导致应用程序花费很长时间来进行验证)。// 其意思是说,GrantedAuthority 中的权限是一类权限(角色),而不是某个具体的功能,比如管理员权限能操作所有模块,而所有模块的功能汇聚在一起可能有好几百个。

5、AuthenticationManager 身份认证管理器接口

        AuthenticationManager 定义 Spring Security 的过滤器如何执行身份验证的 API。用于处理身份验证请求

public interface AuthenticationManager {

    //尝试对传递的身份验证对象进行身份验证,
    //如果成功,则返回一个完全填充的身份验证对象(包括授予的权限)。
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}

        Spring Security 的 Filters 实例会调用 AuthenticationManager 获取在 SecurityContextHolder上设置的 Authentication 对象进行身份验证。

        如果直接设置 SecurityContextHolder,就不需要使用 AuthenticationManager。//直接填充身份信息,跳过认证步骤,由此可以见所有的认证流程也只是去SecurityContextHolder中设置一个Authentication而已

        AuthenticationManager 可以有很多种实现,其中最常见的实现是 ProviderManager

6、ProviderManager 身份认证管理器的实现

        ProviderManager 是 AuthenticationManager 最常用的实现。ProviderManager 对用户的认证委托给一个 AuthenticationProvider 实例的列表。

//具体进行认证操作的实例列表
private List<AuthenticationProvider> providers;

        每个 AuthenticationProvider 都有机会表明身份验证应该是成功的、失败的,或者表明它不能做出决定,并允许下游的 AuthenticationProvider 做出决定。

        如果配置的 AuthenticationProvider 实例都不能进行身份验证,则身份验证失败并产生一个ProviderNotFoundException,这是一个特殊的 AuthenticationException,表明 ProviderManager 没有配置支持此 Authentication 对象认证的 Provider 类型。

        实际中,每个 AuthenticationProvider 都会去执行一个特定类型的身份验证。例如,一个AuthenticationProvider 可能能够验证用户名/密码,而另一个 AuthenticationProvider 可能能够验证 SAML 断言。Spring Security 同时支持多种身份验证类型,并且只公开一个通用的 AuthenticationManager Bean。//对应各种功能的过滤器

        ProviderManager 还允许配置一个父类 AuthenticationManager(可选),它在没有AuthenticationProvider 可以执行身份验证的情况下被使用。父类可以是任何类型的 AuthenticationManager,通常也是一个 ProviderManager 实例。

        多个 ProviderManager 实例可以共享同一个父 AuthenticationManager。该方式在存在多个 SecurityFilterChain 的场景中比较常见,这些安全过滤器链有一些共同的身份验证(共享的父AuthenticationManager),但也有不同的身份验证机制(不同的 ProviderManager 实例)。

        默认情况下,ProviderManager 会尝试从成功的身份验证请求返回的 Authentication 对象中清除敏感的凭据信息,防止信息(如密码)在 HttpSession 中保留的时间超过所需的时间。// Authentication 认证成功后,ProviderManager 会清除其中的认证凭据

        // 因为ProviderManager会清除Authentication认证凭据,所以缓存Authentication信息时应该注意此问题。

        解决缓存问题的两种方案:创建 Authentication 对象的副本或者禁用 ProviderManager 上的 eraseCredentialsAfterAuthentication 属性。

        【ProviderManager.authenticate 源码分析】:

//执行认证的AuthenticationProvider列表
private List<AuthenticationProvider> providers;  

//认证流程
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        int currentPosition = 0;
        int size = this.providers.size();
        //1-获取Provider列表迭代器
        Iterator var9 = this.getProviders().iterator(); 

        //2-开始循环遍历Provider列表
        while(var9.hasNext()) { 
            AuthenticationProvider provider = (AuthenticationProvider)var9.next();
            if (provider.supports(toTest)) {

                //省略...

                try {
                    //3-使用其中一个provider进行身份验证
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (InternalAuthenticationServiceException | AccountStatusException var14) {
                    this.prepareException(var14, authentication);
                    throw var14;
                } catch (AuthenticationException var15) {
                    lastException = var15;
                }
            }
        }

        //省略...(这里省略了付实例的认证,也很简单,可查看源码)

        //4-如果认证成功,在这里擦除密码/认证凭据等信息
        if (result != null) { 
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials(); //擦除认证凭据
            }

            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }

            return result;
        } else {
            //5-抛出providerNotFound异常
            if (lastException == null) {
                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }

            if (parentException == null) {
                this.prepareException((AuthenticationException)lastException, authentication);
            }

            throw lastException;
        }
    }

7、AuthenticationProvider 特定类型的身份验证接口

        用于处理特定身份验证的统一接口。

        通常可以将多个 AuthenticationProvider 实例注入到 ProviderManager 中。每个AuthenticationProvider 执行特定类型的身份验证。例如,DaoAuthenticationProvider 支持基于用户名/密码的身份验证,而 JwtAuthenticationProvider 支持验证 JWT 令牌。

public interface AuthenticationProvider {
    //执行身份验证
    Authentication authenticate(Authentication var1) throws AuthenticationException;

    //如果此AuthenticationProvider支持指定的Authentication对象,则返回true。
    boolean supports(Class<?> var1);
}

8、AuthenticationEntryPoint 身份验证方案接口

        AuthenticationEntryPoint 作用:由 ExceptionTranslationFilter 用于启动一个身份验证方案

public interface AuthenticationEntryPoint {
    //启动认证方案
    void commence(HttpServletRequest var1, HttpServletResponse var2, AuthenticationException var3) throws IOException, ServletException;
}

        ExceptionTranslationFilter 将填充 AbstractAuthenticationProcessingFilter HttpSession 属性。在调用此方法之前,使用所请求的目标URL

        实现根据需要修改 ServletResponse 上的报文头,以启动身份验证过程。AuthenticationEntryPoint 的实现可能会执行重定向到登录页面、使用 WWW-Authenticate 头响应或采取其他操作。

9、AbstractAuthenticationProcessingFilter 用户凭据验证过滤器

        AbstractAuthenticationProcessingFilter 用作验证用户凭据的基本过滤器。在对凭证进行身份验证之前,Spring Security 通常会使用 AuthenticationEntryPoint 来请求凭证。

        【AbstractAuthenticationProcessingFilter.doFilter 源码】//总流程

    //身份认证过滤器的处理流程
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            try {
                //1- 尝试进行身份验证
                Authentication authenticationResult = this.attemptAuthentication(request, response);
                if (authenticationResult == null) {
                    return;
                }
                //2-如果验证成功,创建新会话,更新sessionId
                this.sessionStrategy.onAuthentication(authenticationResult, request, response);
                if (this.continueChainBeforeSuccessfulAuthentication) {
                    chain.doFilter(request, response);
                }
                //3-调用认证成功后处理流程
                this.successfulAuthentication(request, response, chain, authenticationResult);
            } catch (InternalAuthenticationServiceException var5) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var5);
                //4-调用认证失败后处理流程
                this.unsuccessfulAuthentication(request, response, var5);
            } catch (AuthenticationException var6) {
                //4-调用认证失败后处理流程
                this.unsuccessfulAuthentication(request, response, var6);
            }
        }
    }

        接下来,AbstractAuthenticationProcessingFilter 可以对提交给它的任何身份验证请求进行身份验证

    //过滤器中尝试进行身份验证的抽象方法
    public abstract Authentication attemptAuthentication(HttpServletRequest var1, HttpServletResponse var2) throws AuthenticationException, IOException, ServletException;

    //一个具体实现类:UsernamePasswordAuthenticationFilter的实现逻辑
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            username = username != null ? username : "";
            username = username.trim();
            String password = this.obtainPassword(request);
            password = password != null ? password : "";

            //使用用户名和密码创建一个Authentication对象
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);

            //调用特定的认证管理器进行认证,通常为(ProviderManager)
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

        AbstractAuthenticationProcessingFilter 进行认证的流程图示:

        当用户提交凭据时,AbstractAuthenticationProcessingFilter HttpServletRequest 创建一个要进行身份验证的 Authentication 对象。创建的身份验证类型取决于AbstractAuthenticationProcessingFilter 的子类。例如,UsernamePasswordAuthenticationFilter HttpServletRequest 中提交的用户名和密码中创建 UsernamePasswordAuthenticationToken//看上边的源码展示

        接下来,将 Authentication 对象 传递到 AuthenticationManager 中进行身份验证。

        如果身份验证失败,则执行 Failure 流程://失败流程,有许多的拓展点

        (1)调用 SecurityContextHolder.clearContext 方法清除 Security 上下文。//ThreadLocal.remove

        (2)调用 RememberMeServices.loginFail 方法。有两个实现类 NullRememberMe 和 RememberMe。 //RememberMe会失效Cookie

        (3)调用 AuthenticationFailureHandler.onAuthenticationFailure 方法。//抛出错误信息或者重定向

        AbstractAuthenticationProcessingFilter.unsuccessfulAuthentication 源码】

    //认证失败处理流程
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {

        //1-清除SecurityContextHolder
        SecurityContextHolder.clearContext();

        this.logger.trace("Failed to process authentication request", failed);
        this.logger.trace("Cleared SecurityContextHolder");
        this.logger.trace("Handling authentication failure");

        //2-调用RememberMeServices.loginFail
        this.rememberMeServices.loginFail(request, response);

        //3-调用AuthenticationFailureHandler
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    }

        如果身份验证成功,则执行 Success 流程:

        (1)SessionAuthenticationStrategy.onAuthentication 会收到新的登录通知。

        (2)在 SecurityContextHolder 中设置通过认证的用户信息,方便后续取用。

        (3)调用 RememberMeServices.loginSuccess 方法。//通过此方法可自定义成功后的处理方式。

        (4)调用 ApplicationEventPublisher.publishEvent 方法发布一个InteractiveAuthenticationSuccessEvent 事件。//用户可通过监听此事件来做进一步操作

        (5)调用 AuthenticationSuccessHandler.onAuthenticationSuccess 方法。//处理url重定向等

        【AbstractAuthenticationProcessingFilter.successfulAuthentication 源码】

    //认证成功处理流程
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {

        //1-在 SecurityContextHolder 中设置通过认证的用户信息,方便后续取用
        SecurityContextHolder.getContext().setAuthentication(authResult);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
        }

        //2-调用 RememberMeServices.loginSuccess 方法
        this.rememberMeServices.loginSuccess(request, response, authResult);
        if (this.eventPublisher != null) {

            //3-发布认证成功事件
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        //4-调用AuthenticationSuccessHandler.onAuthenticationSuccess方法
        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }

        最后,熟知用户的认证流程,对熟练使用 Spring Security 非常重要。需要多学习,多总结。

        至此,全文到此结束。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数字孪生管控系统,智慧园区楼宇合集

智慧园区是指将物联网、大数据、人工智能等技术应用于传统建筑和基础设施&#xff0c;以实现对园区的全面监控、管理和服务的一种建筑形态。通过将园区内设备、设施和系统联网&#xff0c;实现数据的传输、共享和响应&#xff0c;提高园区的管理效率和运营效益&#xff0c;为居…

【Spring Cloud Gateway 新一代网关】—— 每天一点小知识

&#x1f4a7; S p r i n g C l o u d G a t e w a y 新一代网关 \color{#FF1493}{Spring Cloud Gateway 新一代网关} SpringCloudGateway新一代网关&#x1f4a7; &#x1f337; 仰望天空&#xff0c;妳我亦是行人.✨ &#x1f984; 个人主页——微风撞见云的博客&a…

中医药行业如何进行数字化转型?看天津同仁堂谈“有道有术有零代码”

张伯礼院士曾指出&#xff0c;中药制造的现代化水平&#xff0c;还停留在10%左右的阶段。中医药行业&#xff0c;老字号企业&#xff0c;该如何通过数字化焕发新活力&#xff1f; 天津同仁堂通过与伙伴云合作&#xff0c;零代码构建数字化系统&#xff0c;让技术与思维共同成长…

html,css初学

安装VSCODE ,插件&#xff1a;live server &#xff0c;html support html 然后为了更好地理解&#xff0c;请逐步输入&#xff0c;并及时查看效果 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>D…

PCB封装设计指导(十一)画出脚标,极性标识和特殊器件标识

PCB封装设计指导(十一)画出脚标,极性标识,特殊器件标识 定义完pin number之后,就需要画出器件的脚标,极性标识,特殊标识等丝印相关的信息了,这些说明对辅助PCB布局有很好的作用,当然对后续贴片也很有帮助。 如何添加,具体见如下说明 1.脚标一般都用数字表示,silks…

力扣热门100题之和为k的子数组【中等】

题目描述 给你一个整数数组 nums 和一个整数 k &#xff0c;请你统计并返回 该数组中和为 k 的连续子数组的个数 。 示例 1&#xff1a; 输入&#xff1a;nums [1,1,1], k 2 输出&#xff1a;2 示例 2&#xff1a; 输入&#xff1a;nums [1,2,3], k 3 输出&#xff1a;2 …

Acwing.898 数字三角形(动态规划)

题目 给定一个如下图所示的数字三角形&#xff0c;从顶部出发&#xff0c;在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点&#xff0c;一直走到底层&#xff0c;要求找出─条路径&#xff0c;使路径上的数字的和最大。 输入格式 第一行包含整数n&#xff0…

MES管理系统给汽配企业带来了哪些效益

汽车工业是一个庞大的社会经济系统工程&#xff0c;不同于普通产品&#xff0c;汽车产品是一个高度综合的最终产品&#xff0c;需要组织专业化协作的社会化大生产&#xff0c;需要相关工业产品与之配套。如何提高生产效率和产品质量成为了一个关键问题&#xff0c;而汽配企业ME…

EtherCAT转TCP/IP网关EtherCAT解决方案

你是否曾经为生产管理系统的数据互联互通问题烦恼过&#xff1f;曾经因为协议不同导致通讯问题而感到困惑&#xff1f;现在&#xff0c;我们迎来了突破性的进展&#xff01; 介绍捷米特JM-TCPIP-ECT&#xff0c;一款自主研发的Ethercat从站功能的通讯网关。它能够连接到Etherc…

RDIFramework.NET CS敏捷开发框架 V6.0发布(支持.NET6+、Framework双引擎,全网唯一)

全新RDIFramework.NET V6.0 CS敏捷开发框架发布&#xff0c;全网唯一支持.NET6&#xff0c;Framework双引擎&#xff0c;降低开发成本&#xff0c;提高产品质量&#xff0c;提升用户体验与开发团队稳定性&#xff0c;做软件就选RDIFramework.NET开发框架。 1、RDIFramework.NET…

毓恬冠佳冲刺上市:打破汽车天窗外商垄断,长安汽车为其主要客户

撰稿|行星 来源|贝多财经 7月23日&#xff0c;上海毓恬冠佳科技股份有限公司&#xff08;以下简称“毓恬冠佳”&#xff09;在深圳证券交易所的审核状态变更为“已问询”。据贝多财经了解&#xff0c;毓恬冠佳于2023年6月27日递交招股书&#xff0c;准备在创业板上市。 本次冲…

Linux---详解进程信号

进程信号 &#x1f373;信号理解&#x1f9c8;什么是信号&#xff1f;&#x1f95e;进程信号&#x1f953;查看系统信号&#x1f969;在技术角度理解信号&#x1f357;注意 &#x1f356;信号处理&#x1f9c7;信号异步机制 &#x1f354;信号产生&#x1f35f;通过终端按键产生…

vue中使用jsMind生成思维导图 截图功能踩坑

npm i jsmind先安装&#xff0c;再引入 import jsmind/style/jsmind.css import jsMind from jsmind/js/jsmind.js require(jsmind/js/jsmind.draggable.js) require(jsmind/js/jsmind.screenshot.js)正常引入是这样的&#xff0c;然后渲染也没问题 <template><div …

如何打开工业相机(海康)与halcon方式打开

使用海康相机&#xff0c;下载对应的客户端软件 地址&#xff1a;https://www.hikrobotics.com/cn/machinevision/service/download 界面如下&#xff1a; 使用 halcon 读取相机&#xff0c;需要将对应的动态链接库dll文件放入halcon的安装目录中&#xff0c;如下&#xff0c;…

全志F1C200S嵌入式驱动开发(spi-nor驱动)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 和v3s一样,f1c200s本身也支持spi-nor flash。当然,不管是norflash,还是nandflash,都是为了能够让程序脱离sd卡,直接依靠板子上面的flash,就可以完成正常地加载和运行工作。tf…

MySQL数据库优化

MySQL数据库优化 1.1 SQL及索引优化1.2 数据库表结构优化1.3 系统配置优化1.4 硬件配置优化 2 SQL及索引优化2.1 慢查日志2.1.1 检查慢查日志是否开启2.1.2 MySQL慢查日志的存储格式 2.2 MySQL慢查日志分析工具&#xff08;mysqldumpslow&#xff09;2.2.1 介绍2.2.2 用法 2.3 …

二进制子集题解

样例输入&#xff1a; 13样例输入&#xff1a; 0 1 4 5 8 9 12 13思路分析&#xff1a; 这道题大体就是进制转换然后按位 d f s dfs dfs。进制转换比较好理解&#xff0c;不懂得可以自行 b d f s ( 百度优先搜索 ) bdfs(百度优先搜索) bdfs(百度优先搜索)一下。 代码&#…

索引的数据结构

索引的数据结构 部分资料来自B站尚硅谷-宋红康老师 1. 为什么使用索引 使用索引是为了加快数据库的查询速度和提高数据库的性能。索引是数据库表中的一种数据结构&#xff0c;它可以帮助数据库快速定位并检索所需的数据。 当数据库表中的数据量较大时&#xff0c;如果没有索…

C#中简单Winform程序编译(待验证)

1、文件架构 2、MainWindow.xaml <Window x:Class"WpfApp1.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d"http://schemas.microsoft.…

【仿写spring】一、通过反射读取带有@RequestMapping与@Controller注解的类并模拟请求路径调用方法

目录 简介思路实践一、自定义注解RequestMapping&#xff0c;Controller二、路径转全限定名方法三、扫描文件夹四、通过反射来寻找有RequestMapping以及Controller的类五、获取对象实例六、通过invoke调用方法 文件结构以及测试结果1、文件结构2、TestController3、测试结果 简…