Spring Security认证:获得认证、持久化认证模块详解

news2024/11/27 21:40:56

参考:认证 :: Spring Security Reference (springdoc.cn)

本文将讲述Spring Security中与认证相关的各个基本模块,力求对整个认证框架提供完善的认知。

众所周知,认证Authentication实际上就是一个代表身份的类,实例就是具体的身份,那么与这个身份有关的操作有哪些呢:

  1. 用户名/密码 认证:通过用户提供的用户名/密码信息构建一个认证实例用于校验;校验后提供用户一个认证代表用户的身份。
    1. 读取用户名/密码(从用户的请求中获得账号密码)
    2. 密码存储(获得数据库等空间中的用户信息,进行验证操作)
  2. 持久化认证:让用户未来的请求也能携带或使用当前的身份(认证实例)。
  3. 会话管理:管理用户的登录状态,比如限制同时可登录的数量、自己存储认证而不是让 Spring Security 负责。(自行了解:认证持久性和会话(Session)管理 :: Spring Security Reference (springdoc.cn))
  4. 其他如remember me、预认证等(自行了解:认证 :: Spring Security Reference (springdoc.cn))

一、获得认证

验证用户最常见的方法之一是验证用户名和密码。 Spring Security 为此进行验证提供了全面的支持。

1.1读取用户名和密码

之前的Spring Security:总体架构中,我们已经讲过,Spring Security是依赖于一个个过滤器实现所有功能模块的。读取用户名和密码,自然靠的也是过滤器。

1.1.1读取表单登录参数

Spring Security提供了对通过HTML表单提供用户名和密码的支持,并通过UsernamePasswordAuthenticationFilter类从用户请求中获得表单的属性。

先讲一下总体流程(可参考Spring Security:认证默认流程解析),然后讲一部分源码。

前置流程

  1. 一个用户向一个需要授权的资源发出一个未经认证的请求
  2. AuthorizationFilter 在判断有无权限后抛出 AccessDeniedException 异常。
  3. 该异常被 ExceptionTranslationFilter 捕获,并判断当前用户认证为匿名认证(即未认证用户)后,调用 Start Authentication 函数,使用一个AuthenticationEntryPoint(默认为LoginUrlAuthenticationEntryPoint的实例)将请求重定向到登录页面
  4. 浏览器请求登录页面。
  5. 后端渲染登录界面,并返回动态网页。
认证流程

  1. 用户提交他们的用户名和密码后,UsernamePasswordAuthenticationFilter 从请求中提取用户名和密码,并创建一个Authentication(UsernamePasswordAuthenticationToken的实例)。
  2. UsernamePasswordAuthenticationFilter 调用 AuthenticationManager(ProviderManager 的一个实例)对 Authentication 进行校验。
  3. 如果认证失败:
    1. 清除 SecurityContextHolder 中当前的 SecurityContext。
    2. 调用 RememberMeServices的loginFail 函数。
    3. 调用 AuthenticationFailureHandler 进行处理。
  4. 如果认证成功:
    1. SessionAuthenticationStrategy 被通知有新的登录。
    2. 将 Authentication 保存到 SecurityContextHolder 中当前的 SecurityContext。
    3. 调用 RememberMeServices 的 loginSuccess 函数。
    4. ApplicationEventPublisher 发布 InteractiveAuthenticationSuccessEvent 事件。
    5. AuthenticationSuccessHandler 被调用,通常这会重定向到原先 ExceptionTranslationFilter 中保存的原请求。
配置方式

在SecurityFilterChain中启用表单登录的最小配置:

public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .formLogin(withDefaults());
    // ...
}

在SecurityFilterChain中设置一个自定义的登录表单:用于更改重定向到的登录页面

public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .formLogin(form -> form
            .loginPage("/login")
            .permitAll()
        );
    // ...
}

然后在实现这个登录表单:文件位置为src/main/resources/templates/login.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
    <head>
        <title>Please Log In</title>
    </head>
    <body>
        <h1>Please Log In</h1>
        <div th:if="${param.error}">
            Invalid username and password.</div>
        <div th:if="${param.logout}">
            You have been logged out.</div>
        <form th:action="@{/login}" method="post">
            <div>
            <input type="text" name="username" placeholder="Username"/>
            </div>
            <div>
            <input type="password" name="password" placeholder="Password"/>
            </div>
            <input type="submit" value="Log in" />
        </form>
    </body>
</html>
  • 表单以 post 方法请求 /login。
  • 在一个名为 username 的参数中指定用户名。
  • 在一个名为 password 的参数中指定密码。
  • 如果发现名为 error 的HTTP参数,表明用户未能提供一个有效的用户名或密码。
  • 如果发现名为 logout 的HTTP参数,表明用户已经成功注销。

如果使用了Spring MVC,需要将 GET /login 映射到我们创建的登录模板。

@Controller
class LoginController {
    @GetMapping("/login")
    String login() {
        return "login";
    }
}

接下来讲解一下源码:(看源码前请保证已阅读总体架构、认证架构、默认流程示意图)

AbstractAuthenticationProcessingFilter

UsernamePasswordAuthenticationFilter的父类,doFilter函数的实现实际上是写在AbstractAuthenticationProcessingFilter中的,大致代码为:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain){
    //...
    try {
        //...
        Authentication authenticationResult = attemptAuthentication(request, response);//调用UsernamePasswordAuthenticationFilter中实现
        //...
        successfulAuthentication(request, response, chain, authenticationResult);//认证成功的处理流程
    }catch(AuthenticationException ex){
        unsuccessfulAuthentication(request, response, ex);//认证失败的处理流程
    }
}
UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter类实现了attemptAuthentication方法,用于获得login表单中参数,并进行验证,大致代码为:

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
      throws AuthenticationException {
   if (this.postOnly && !request.getMethod().equals("POST")) {//判断是否为post请求类型
      throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
   }
   String username = obtainUsername(request);//提取username参数
   username = (username != null) ? username.trim() : "";
   String password = obtainPassword(request);//提取password参数
   password = (password != null) ? password : "";
   UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
         password);//封装为Authentication
   setDetails(request, authRequest);
   return this.getAuthenticationManager().authenticate(authRequest);//对封装的Authentication进行验证
}

1.1.2读取Basic认证参数

Spring Security同样提供了对 Basic HTTP Authentication(HTTP基础认证) 的支持。

前置流程

  1. 一个用户向一个需要授权的资源发出一个未经认证的请求。(与表单登录一样)
  2. AuthorizationFilter 在判断有无权限后抛出 AccessDeniedException 异常。(与表单登录一样)
  3. 该异常被 ExceptionTranslationFilter 捕获,并判断当前用户认证为匿名认证(即未认证用户)后,调用 Start Authentication 函数,使用一个AuthenticationEntryPoint(更改为 BasicAuthenticationEntryPoint 的实例)发送一个 WWW-Authenticate 头。(注意:不同于原先会在 ExceptionTranslationFilter 中存储请求,Basic HTTP Authentication 的情况下浏览器会自动重放它最初的请求。)
认证流程

  1. 用户提交他们的用户名和密码后,BasicAuthenticationFilter 从请求中提取用户名和密码,并创建一个Authentication(UsernamePasswordAuthenticationToken的实例)。
  2. BasicAuthenticationFilter 调用 AuthenticationManager(ProviderManager 的一个实例)对 Authentication 进行校验。
  3. 如果认证失败:
    1. 清除 SecurityContextHolder 中当前的 SecurityContext。
    2. 调用 RememberMeServices的loginFail 函数。
    3. 调用 AuthenticationFailureHandler 进行处理,触发 WWW-Authenticate 再次发送。
  1. 如果认证成功:
    1. 将 Authentication 保存到 SecurityContextHolder 中当前的 SecurityContext。
    2. 调用 RememberMeServices 的 loginSuccess 函数。
    3. 调用 FilterChain.doFilter(request,response) 来继续执行其余的应用逻辑。
配置方式

在SecurityFilterChain中启用 HTTP Basic 认证 的最小配置:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        // ...
        .httpBasic(withDefaults());
    return http.build();
}

1.1.3摘要认证

请自行了解:摘要(Digest)认证 :: Spring Security Reference (springdoc.cn)。

1.2密码存储与验证

Spring Security提供了优秀的分层机制,之前提过的任何读取用户名和密码的方式都可以和任意密码存储与验证的方式搭配。

不过要讲密码的存储与验证,就必须先知道DaoAuthenticationProvider、UserDetailsService、PasswordEncoder、UserDetails等的概念。

1.2.1基础概念

UserDetails

顾名思义,是详细的用户信息,其内部封装了用户的核心安全数据,通常包含(用户名、密码、是否可用、是否过期、是否被锁定、权限/角色集合)。

常用于验证一个 Authentication 是否有效。

UserDetailsService

用于提供 UserDetails 的 service 类,会被 DaoAuthenticationProvider 调用,用来检索一个 Authentication 对应的实际用户类的用户名、密码和其他属性。

(注意:如果想要自定义认证方式,除了直接使用 AuthenticationManagerBuilder 或 AuthenticationProvider Bean 来提供新的AuthenticationProvider之外,也可以复用 DaoAuthenticationProvider,并使用自定义的 UserDetailsService Bean 来更改自定义认证的逻辑。)

PasswordEncoder

用来对用户密码明文进行加密的工具。

DaoAuthenticationProvider

DaoAuthenticationProvider的工作原理:

  1. Filter 将用户名/密码封装成了 Authentication,以备校验使用。
  2. Filter 调用 ProviderManager 进行校验,而 ProviderManager 会使用 DaoAuthenticationProvider 来进行校验。
  3. DaoAuthenticationProvider 从 UserDetailsService 中查找 UserDetails。
  4. DaoAuthenticationProvider 使用 PasswordEncoder 来验证上一步返回的 UserDetails 上的密码。
  5. 当认证成功时,将 Authentication 的 principal(委托人)属性设置为之前返回的 UserDetails,并返回 Authentication。

看一下默认情况下的源码:

DaoAuthenticationProvider 的父类是 AbstractUserDetailsAuthenticationProvider,ProviderManager 调用验证的方法 authenticate 的实现也是写在 AbstractUserDetailsAuthenticationProvider 中的:

public Authentication authenticate(Authentication authentication){
    //...
    user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
    //...
}

retrieveUser的实现在 DaoAuthenticationProvider 中:

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication){
    //...
    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
    //...
}

调用了 UserDetailsService(InMemoryUserDetailsManager 的实例)的 loadUserByUsername 方法,用来获得 UserDetails:

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
   UserDetails user = this.users.get(username.toLowerCase());//获得UserDetails
   if (user == null) {
      throw new UsernameNotFoundException(username);
   }
   return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
         user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}

这样就获得了 UserDetails,接下来就要 DaoAuthenticationProvider(AbstractUserDetailsAuthenticationProvider)继续验证 Authentication 了:

public Authentication authenticate(Authentication authentication){
    //...
    user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);//刚刚运行到这
    //...
    additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);//现在运行这个
}

additionalAuthenticationChecks 实现在 DaoAuthenticationProvider中:

protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication){
   //...
   String presentedPassword = authentication.getCredentials().toString();//获得密码
   if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {//调用passwordEncoder进行验证,不通过就抛异常
      this.logger.debug("Failed to authenticate since password does not match stored value");
      throw new BadCredentialsException(this.messages
            .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
   }
}

passwordEncoder的代码不重要,忽略,现在 additionalAuthenticationChecks 已经执行完了,继续看 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法:

public Authentication authenticate(Authentication authentication){
    //...
    user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
    //...
    additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);//刚刚运行到这
    //...
    this.userCache.putUserInCache(user);//将验证后的Authentication存入缓存
    //...
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

createSuccessAuthentication 会返回一个验证成功的 Authentication 对象,并使用PasswordEncoder做好加密处理:

protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
    //...
    String presentedPassword = authentication.getCredentials().toString();
    String newPassword = this.passwordEncoder.encode(presentedPassword);
    user = this.userDetailsPasswordService.updatePassword(user, newPassword);
    //...
    return super.createSuccessAuthentication(principal, authentication, user);//超类的工作请自行了解
}

至此,DaoAuthenticationProvider 的方法执行结束,ProviderManager 已获得一个验证完成、代表用户身份的新 Authentication。

1.2.2存储在内存中的用户名/密码

Spring Security 的 InMemoryUserDetailsManager 实现了 UserDetailsService,为存储在内存中的基于用户名/密码的认证提供支持。

让我们回忆一下,UserDetailsService 干了什么?当然是提供UserDetails。InMemoryUserDetailsManager 是一种直接在内存中存放 UserDetails 的 UserDetailsService 实现类。

通过 InMemoryUserDetailsManager 的构造方法,我们可以直接创建 UserDetails 类用于验证:

@Bean
public UserDetailsService users() {
    UserDetails user = User.builder()
        .username("user")
        .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER")
        .build();
    UserDetails admin = User.builder()
        .username("admin")
        .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER", "ADMIN")
        .build();
    return new InMemoryUserDetailsManager(user, admin);
}

这种方式需要提前加密 Password,当然也可以现场调用PasswordEncoder进行加密:

@Bean
public UserDetailsService users() {
    // The builder will ensure the passwords are encoded before saving in memory
    UserBuilder users = User.withDefaultPasswordEncoder();
    UserDetails user = users
        .username("user")
        .password("password")
        .roles("USER")
        .build();
    UserDetails admin = users
        .username("admin")
        .password("password")
        .roles("USER", "ADMIN")
        .build();
    return new InMemoryUserDetailsManager(user, admin);
}

1.2.3存储在数据库中的用户名/密码

Spring Security 的 JdbcDaoImpl 实现了 UserDetailsService,以提供对基于用户名和密码的认证的支持。

(实际上就是 Spring Security 自己提供了一套使用 JDBC 进行检索的框架,当然我们也可以自己实现 UserDetailsService 进行查询数据库并提供 UserDetails。)

要求数据库格式

默认格式:

create table users(
    username varchar_ignorecase(50) not null primary key,
    password varchar_ignorecase(500) not null,
    enabled boolean not null
);
create table authorities (
    username varchar_ignorecase(50) not null,
    authority varchar_ignorecase(50) not null,
    constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);

如果是按组进行权限分配的形式:

create table groups (
    id bigint generated by default as identity(start with 0) primary key,
    group_name varchar_ignorecase(50) not null
);

create table group_authorities (
    group_id bigint not null,
    authority varchar(50) not null,
    constraint fk_group_authorities_group foreign key(group_id) references groups(id)
);

create table group_members (
    id bigint generated by default as identity(start with 0) primary key,
    username varchar(50) not null,
    group_id bigint not null,
    constraint fk_group_members_group foreign key(group_id) references groups(id)
);
设置数据源

必须创建一个数据源用以获得用户信息:

@Bean
DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(H2)
        .addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
        .build();
}

(设置了一个嵌入式的数据源,用默认的 user schema 进行初始化。)

常用数据源设置方法:

@Bean
public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost:3306/db_name");
    dataSource.setUsername("username");
    dataSource.setPassword("password");
    return dataSource;
}
配置JdbcDaoImpl

案例:

@EnableWebSecurity
@Configuration
public class MySecurityConfig {
    public DataSource dataSource() {//创建数据源的函数
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/securitytest?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true");
        dataSource.setUsername("XXXXXX");
        dataSource.setPassword("XXXXXX");
        return dataSource;
    }

    public JdbcDaoImpl myJdbcDaoImpl(){//创建JdbcDaoImpl的函数
        JdbcDaoImpl jdbcDao = new JdbcDaoImpl();
        jdbcDao.setDataSource(dataSource());
        jdbcDao.setUsersByUsernameQuery("SELECT username, password, enabled FROM users WHERE username = ?");
        jdbcDao.setAuthoritiesByUsernameQuery("SELECT username, authority FROM authorities WHERE username = ?");
        return jdbcDao;
    }

    @Bean
    public PasswordEncoder nullPassword(){//由于我数据库中存的是明文密码,需要将PasswordEncoder的matches重写为直接比较,而不是原先的加密输入密码后比较
        return new PasswordEncoder() {
            private final PasswordEncoder delegate = new Pbkdf2PasswordEncoder("mySecret", 10000, 128, Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA512);
            @Override
            public String encode(CharSequence rawPassword) {
                return delegate.encode(rawPassword);
            }
            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return rawPassword.toString().equals(encodedPassword);
            }
        };
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorize -> authorize
                        .anyRequest().authenticated()
                )
                .formLogin(Customizer.withDefaults());
        http.userDetailsService(myJdbcDaoImpl());//添加一个UserDetailsService用来验证,保证可以根据数据库中账号密码进行登录
        return http.build();
    }
}

JdbcUserDetailsManager 扩展了 JdbcDaoImpl,通过 UserDetailsManager 接口提供对 UserDetails 的管理,提供了一组便利的方法来管理用户的创建、更新、删除等操作。相关的应用可自行了解。

1.2.4LDAP 认证

请自行了解:LDAP 认证 :: Spring Security Reference (springdoc.cn)

二、持久化认证

Spring Security 会将 Authentication 存入到 SecurityContext 中,用以代表用户的身份。

那么当用户在未来再次发起请求时,该如何将未来的请求与当前的 SecurityContext 联系起来呢?

在Spring Security中,用户与未来请求的关联是通过 SecurityContextRepository 实现的。

2.1常用配置

SecurityContextRepository 是一个接口,通常使用其实现类 DelegatingSecurityContextRepository、HttpSessionSecurityContextRepository、RequestAttributeSecurityContextRepository、NullSecurityContextRepository。

  • DelegatingSecurityContextRepository:一个代理类,这个类可以包含很多的 SecurityContextRepository 的具体实现,然后通过这些具体实现将认证保存。(默认包含下面两者)
  • HttpSessionSecurityContextRepository:将 SecurityContext 与 HttpSession 相关联,即将 Authentication 存入 HttpSession。
  • RequestAttributeSecurityContextRepository:将 SecurityContext 保存为请求属性(request attribute),以确保 SecurityContext 可用于跨调度(dispatch)类型发生的单个请求,这些调度类型可能会清除 SecurityContext。
  • NullSecurityContextRepository:不做任何事情。

设置方式:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // ...
        .securityContext((securityContext) -> securityContext
            .securityContextRepository(new DelegatingSecurityContextRepository(
                new RequestAttributeSecurityContextRepository(),
                new HttpSessionSecurityContextRepository()
            ))
        );
    return http.build();
}

2.2基本原理

与持久化认证的过滤器有两个:SecurityContextPersistenceFilter 和 SecurityContextHolderFilter,不过SecurityContextPersistenceFilter已经弃用了,这里就只讲 SecurityContextHolderFilter 了。

2.2.1SecurityContextHolderFilter

SecurityContextHolderFilter的作用:使用 SecurityContextRepository 加载 SecurityContext。

  1. SecurityContextHolderFilter 从S ecurityContextRepository 加载 SecurityContext 并将其设置在 SecurityContextHolder 上。
  2. 接下来,继续运行过滤器链剩余部分。

注意:SecurityContextHolderFilter 只加载 SecurityContext,并不保存 SecurityContext。如果使用的是 SecurityContextHolderFilter 而不是 SecurityContextPersistenceFilter,需要明确地保存 SecurityContext。

保存方式:(参考Spring Security:认证架构)

SecurityContext context = SecurityContextHolder.createEmptyContext(); //创建一个空的 SecurityContext 
context.setAuthentication(authentication); //设置认证信息到安全上下文中
SecurityContextHolder.setContext(context); //在 SecurityContextHolder 上设置 SecurityContext。

2.2.2SecurityContextPersistenceFilter

自行了解,可参考:持久化认证 :: Spring Security Reference (springdoc.cn)。

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

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

相关文章

第一章 01Java入门-常见的CMD命令

前言 学习常见的CMD命令 一、常见的CMD命令 常见的CMD命令主要包括切换盘符、查看当前路径下的内容、进入单级目录、回到上一级目录、进入多级目录、回退到盘符目录、清屏以及退出命令提示符窗口等。 二、代码演示 先从默认的C盘符切换到E盘符(E:),然后再查看E盘符下面的…

Redis的四种部署方案

这篇文章介绍Reids最为常见的四种部署模式&#xff0c;其实Reids和数据库的集群模式差不多&#xff0c;可以分为 Redis单机模式部署、Redis主从模式部署、Redis哨兵模式部署、Cluster集群模式部署&#xff0c;其他的部署方式基本都是围绕以下几种方式在进行调整到适应的生产环境…

【C++】多态 ⑩ ( 不建议将所有函数都声明为 virtual 虚函数 | 多态的理解层次 | 父类指针和子类指针步长 )

文章目录 一、不建议将所有函数都声明为 virtual 虚函数二、多态的理解层次三、父类指针和子类指针步长 对象可以直接获取到自身封装的 普通函数 , 如果要访问虚函数 , 需要增加一次寻址操作 , 因此 这里建议不需要将有 多态 需求的函数声明为 虚函数 ; C 中 指向某类型对象的…

有什么好用的仓库配件管理软件?如何实现企业配件管理智能化?

在当今高度信息化的商业环境中&#xff0c;企业运营的效率和管理的重要性日益凸显。对于许多企业来说&#xff0c;仓库配件管理是一个关键的环节&#xff0c;它不仅涉及到物品的存储和分发&#xff0c;还与企业的成本控制和运营流程紧密相关。然而&#xff0c;管理仓库配件是一…

C++快餐——C++11(3)

应该在脖子上长一个自己的脑袋 文章目录 function包装器bind绑定普通函数绑定成员函数std::bind优点 线程库thread类创建线程线程管理线程标识 原子性操作库(atomic)lock_guard与unique_lockstd::lock_guardstd::unique_lock mutex的种类总结 function包装器 C11引入了一个名为…

ubuntu启动报错error: proc_thermal_add, will cont

如题&#xff0c;ubuntu启动报错error: proc_thermal_add, will cont 截图如下&#xff1a; 困扰了我很久&#xff0c;差点就打算重装系统&#xff0c;准备放弃了&#xff0c;但是感谢国外的老哥&#xff0c;写了一篇非常详细的解决方案&#xff0c;我搬过来。 解决方案&#…

基础课14——文本标注

人工智能界流传一句俏皮话&#xff1a;“有多少人工&#xff0c;就有多少智能。” 1.人工智能数据标注介绍 2018年9月&#xff0c;百度智能云与山西综改示范区达成合作&#xff0c;在太原共同建立了百度&#xff08;山西&#xff09;人工智能基础数据产业基地&#xff08;简称…

AI时代,ChatGPT与文心一言选哪一个?

&#x1f388;个人公众号:&#x1f388; :✨✨✨ 可为编程✨ &#x1f35f;&#x1f35f; &#x1f511;个人信条:&#x1f511; 为与不为皆为可为&#x1f335; 你们平时都是在什么情况下使用GPT的呢&#xff1f;为何使用&#xff1f;都使用什么平台的&#xff1f; 针对以上问…

高防CDN的特点与作用

高级防护CDN是一种专门设计用于提供出色的网络安全功能的内容分发网络。它通过采用分布式节点、智能路由和强大的安全机制&#xff0c;旨在确保网站在面临各种网络攻击时保持安全&#xff0c;同时提供内容的快速传递和优化功能。那么&#xff0c;高级防护CDN有哪些独特的特点和…

win10pycharm和anaconda安装和环境配置教程

windows10 64位操作系统下系统运行环境安装配置说明 下载和安装Anaconda&#xff0c;链接https://www.anaconda.com/download 下载完后&#xff0c;双击exe文件 将anaconda自动弹出的窗口全部关掉即可&#xff0c;然后配置高级系统变量 根据自己的路径&#xff0c;配置…

前端移动高级web详细解析五

响应式布局方案 媒体查询 Bootstrap框架 01-媒体查询 基本写法 max-width&#xff1a;最大宽度&#xff08;小于等于&#xff09; min-width&#xff1a;最小宽度&#xff08;大于等于&#xff09; 书写顺序 min-width&#xff08;从小到大&#xff09; max-width&…

IDEA上也能用Postman了?

Postman是大家最常用的API调试工具&#xff0c;国产API调试工具 Apipost 推出IDEA插件&#xff0c;写完代码就可以调试接口并一键生成接口文档&#xff01;而且还可以根据已有的方法帮助您快速生成 url 和 params。Apipost Helper API 调试工具 API 管理工具 API 搜索工具。…

领先实践|IDEO 最佳设计思维和策略框架

设计思维是一种以人为本的创新方法&#xff0c;它从人类的角度出发&#xff0c;考虑技术上可行和经济上可行的内容。框架可以成为实现设计思维、策略和系统设计的有用工具。本文由此展开阐述 IDEO 的最佳设计思维和策略框架。 01. 设计思维框架 1.1 设计思维过程 设计思维?是…

【python】爬取豆瓣电影排行榜TOP250存储到CSV文件中

一、导入必要的模块&#xff1a; 代码首先导入了需要使用的模块&#xff1a;requests、lxml和csv。 import requests from lxml import etree import csv 如果出现模块报错 进入控制台输入&#xff1a;建议使用国内镜像源 pip install 模块名称 -i https://mirrors.aliyun.co…

Failed to launch task: 文件”Setup”不存在 Mac安装Adobe软件报错解决方案

在安装 Adobe 软件时&#xff0c;软件提示 Failed to launch task: 文件”Setup”不存在 &#xff0c;这个时候怎么处理呢&#xff1f; 解决方法如下&#xff1a; 1、安装 AnitCC 或 或 Creative Cloud 环境&#xff0c;保证软件所需要的环境 2、如果安装后也不起作用&#x…

[ThinkPHP]源码阅读:Model的获取器

目录 1、ThinkPHP组件版本 2、业务Model代码 3、阅读框架源码 4、跳过获取器获取原始数据写法 1、ThinkPHP组件版本 topthink/think-orm v2.0.58 topthink/think-helper v3.1.6 2、业务Model代码 原理&#xff1a;Model通过调用toArray方法使用自定义的获取器 3、阅读框架…

GZ035 5G组网与运维赛题第8套

2023年全国职业院校技能大赛 GZ035 5G组网与运维赛项&#xff08;高职组&#xff09; 赛题第8套 一、竞赛须知 1.竞赛内容分布 竞赛模块1--5G公共网络规划部署与开通&#xff08;35分&#xff09; 子任务1&#xff1a;5G公共网络部署与调试&#xff08;15分&#xff09; 子…

Web Woeker和Shared Worker的使用以及案例

文章目录 1、前言2、介绍 Web Worker3、使用须知及兼容性3.1、使用须知3.2、兼容性 4、使用 Web Worker4.1、创建 Web Worker4.2、与主线程通信4.3、终止 Web Worker4.4、监听错误信息 5、使用 Shared Worker4.5、调试 Shared Worker 6、使用中的一些坑6.1、Web Woeker 中引入了…

第4章_运算符

文章目录 1. 算术运算符1.1 加法与减法运算符1.2 乘法与除法运算符1.3 求模运算符 2. 比较运算符2.1 等号运算符2.2 安全等于运算符2.3 不等于运算符2.4 空运算符2.5 非空运算符2.6 最小值运算符2.7 最大值运算符2.8 BETWEEN AND运算符2.9 IN运算符2.10 NOT IN运算符2.11 LIKE运…

k8s中kubectl命令式对象、命令式对象配置、声明式对象配置管理资源介绍

目录 一.kubernetes资源管理简介 二.三种资源管理方式优缺点比较 三.命令式对象管理介绍 1.kubectl命令语法格式 2.资源类型 &#xff08;1&#xff09;通过“kubectl api-resources”来查看所有的资源 &#xff08;2&#xff09;每列含义 &#xff08;3&#xff09;常…