秒懂SpringBoot之全网最易懂的Spring Security教程

news2024/9/28 10:21:24

[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007

文章目录

  • 概述
  • 简介
  • 原理
  • 默认实现
  • 使用Token认证方案
    • JWT
    • 认证流程
  • 自定义Authentication Provider
    • 自定义Provider
    • 修改配置
  • 认证失败与授权失败处理
    • 认证失败处理
    • 授权失败处理
  • 支持方法级别的授权
  • 总结
  • 源码

概述

现如今Spring全家桶在Java web后端领域是寡头般的存在,其中安全相关的那员大奖就是Spring Security。我觉得它相对于其他竞争对手,例如 Apache Shiro显得过于复杂,对初学者很不友好,我第一次看见这玩意感觉这就是个垃圾,半天搞不懂,太tm复杂了。由于其是spring全家桶的一员,与spring生态集成时特别丝滑,所以用的人也不少,所以不管喜不喜欢你还是要懂他…

为什么要写这篇文章呢?因为我最近又用到了它,所以又去看了下相关的资料,在这里总结一下以便于自己和他人查阅,所以就有了这一篇全网最易懂的Spring Security入门教程,希望它可以名副其实,我也会尽量写的通俗易懂。下面跟着二狗哥开启对spring security的扫盲之旅吧…

简介

最近ChatGPT 很火,我们先让它给介绍一下Spring Security吧,下面是它的回答,不得不说这家伙很牛x。

Spring Security is a powerful and flexible security framework for Java-based applications. It is widely used to secure enterprise applications and web applications built on the Spring framework.

Spring Security provides a wide range of security features, including authentication, authorization, session management, and security for RESTful web services. It also provides support for a variety of authentication mechanisms, including form-based authentication, basic authentication, and Single Sign-On (SSO) solutions.

Some of the key features of Spring Security include:

  • Authentication: Spring Security provides a flexible authentication architecture that allows you to easily configure and integrate with various authentication mechanisms, such as LDAP, database-based authentication, and OAuth.

  • Authorization: Spring Security provides a powerful authorization model that allows you to control access to your application’s resources based on user roles and permissions.

  • Session Management: Spring Security provides built-in support for managing user sessions, including session timeout, session persistence, and session tracking.

  • RESTful Security: Spring Security provides security for RESTful web services through a variety of mechanisms, including basic authentication, token-based authentication, and OAuth.

  • Security Integration: Spring Security integrates seamlessly with other Spring framework components, making it easy to secure your application as it grows and evolves.

Overall, Spring Security is a great choice for anyone looking to build secure applications using the Spring framework. With its flexible and powerful security features, Spring Security makes it easy to secure your application and keep your users’ data safe.

总之我们只要知道这玩意是用在spring framwork 里保证程序安全的就行了。安全这个领域涵盖非常的广泛且深奥,二狗也没能力触碰太深。这里我们只涉及Authentication和Authorization两个概念

  • 认证(Authentication)

解决你是谁的问题,具体表现为注册与登录

  • 授权(Authorization)

解决你能干什么的问题,你登录后有哪些权限。

原理

如果现在让你写一个登录功能(Authentication)你怎么写?很自然的思路是不是把用户提交的信息和我们保存的信息做个比较,如果对上了就登录成功。其实spring security整体也是这样的,只是流程化后,兼顾扩展导致搞的很复杂。

Spring Security的整体原理为:

  1. 当http请求进来时,使用severlet的Filter来拦截。
  2. 提取http请求中的认证信息,例如username和password,或者Token。
  3. 从数据库(或者其他地方,例如Redis)中查询用户注册时的信息,然后进行比对,相同则认证成功,反之失败。

主体就是这么简单,然后只有抓住这个主体思路才不容易被Spring Security绕晕…

下图展示了Spring Security的一些Filter,其中UsernamePasswordAuthenticationFilter很重要,它是Authentication的开始。

在这里插入图片描述
图片来源

默认实现

咱先从最简单的开始,使用Spring Security保护一个使用Spring Boot开发的web程序。

只要在pom.xml中引入依赖Spring Security的依赖即可。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

然后我们写一个测试用的controller。

@RestController
@RequestMapping("/auth")
public class TestController {

    @GetMapping("/hello")
    public String sayHello(){
        return "hello security";
    }
}

使用浏览器访问localhost:8080/auth/hello,你就会发现弹出一个登录页面让你登录

在这里插入图片描述
我们没有写一行代码就实现了一个登录功能,酷不酷?不过用户名和秘密是啥呢?别着急这个Spring Security已经给我们生成了。

username:user
password:随机生成,会打印在你的控制台日志上。

日志:

Using generated security password: 3f5acb10-1390-4474-8892-71e7562d47ce

当然了,我们可以在application.yml中配置用户名和秘密

spring:
  security:
    user:
      name: shusheng007
      password: ss007

那这一切都是怎么发生的呢?这才是我们要关注的重点。

一个请求过来Spring Security会按照下图的步骤处理:
在这里插入图片描述

  • Filter

拦截Http请求,获取用户名和秘密等认证信息

关键方法:

public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
		throws AuthenticationException, IOException, ServletException;
  • AuthenticationManager

从filter中获取认证信息,然后查找合适的AuthenticationProvider来发起认证流程

关键方法:

Authentication authenticate(Authentication authentication) throws AuthenticationException;
  • AuthenticationProvider

调用UserDetailsService来查询已经保存的用户信息并与从http请求中获取的认证信息比对。如果成功则返回,否则则抛出异常。

关键方法:

protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException;
  • UserDetailsService

负责获取用户保存的认证信息,例如查询数据库。

关键方法:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

这些组件都是抽象的,每个都可以有不同的实现,换句话说都是可以定制,特别灵活,所以就特别复杂。具体到我们这个默认的例子中,使用的都是默认实现:

  • Filter: UsernamePasswordAuthenticationFilter
  • AuthenticationManager: ProviderManager
  • AuthenticationProvider: DaoAuthenticationProvider
  • UserDetailsService: InMemoryUserDetailsManager

业务流程是不是很清晰,之所以感觉复杂是因为经过框架的一顿设计,拉长了调用链。虽然设计上复杂了,但是如果理解了这套设计流程,终端用户使用就会简单很多,不理解的话,感觉特别复杂。

使用Token认证方案

上面那套默认实现几乎是不能用在生产环境的,我们日常也不这么用,生产环境中我们一般会结合Token来做认证。

JWT

比较流行的就是使用JWT(JSON Web Tokens),其是一个开放的工业标准。

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

有关于它的介绍可以直接点击上面的网站去查看,这里做个简单的介绍。其一共有三部分

  • Header
  • Payload
  • Signature

这三部分base64后采用.连接,例如下面就是我生成的一个jwt

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
.
eyJ1c2VybmFtZSI6InNodXNoZW5nMDA3In0
.
a4fLj9q3zdZvTCCU7VHbTL-OH_xyRir2fuLGFYH9CQ4

前两部分是base64,最后一部分是前面两部分使用SHA256的签名。你可以使用JWT(JSON Web Tokens)这个网站提供的工具来解码JWT。下图是上面那个jwt的解码结果。

在这里插入图片描述

操作JWT的库非常多,我们这里使用一个强悍的国产工具Hutool里的关于JWT的: hutool-jwt

认证流程

认证的底层逻辑仍然是一样的,就是将用户提交的凭证与我们保存的凭证对比。

  • 登录

用户使用用户名与秘密登录我们的系统,登录成功后颁发JWT给用户

  • 发起请求

用户发起请求时在Header中携带JWT,程序拦截并检查这个token是否合法,合法则放行,不合法则提示从新登录。

下面我们实现一下这个过程:

第一步: 引入依赖

 <dependencies>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
     </dependency>

     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-security</artifactId>
     </dependency>

     <dependency>
         <groupId>cn.hutool</groupId>
         <artifactId>hutool-jwt</artifactId>
         <version>${hutool-jwt.version}</version>
     </dependency>
 </dependencies>

第二步: 登录

@RestController
@RequestMapping("/user")
public class AuthController {

    @Autowired
    AuthenticationManager authenticationManager;

...

    @PostMapping("/login")
    public String login(@RequestBody SignInReq req) {
 
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(req.getUsername(), req.getPassword());
        authenticationManager.authenticate(authenticationToken);
        
         //上一步没有抛出异常说明认证成功,我们向用户颁发jwt令牌
        String token = JWT.create()
                .setPayload("username", req.getUsername())
                .setKey(MyConstant.JWT_SIGN_KEY.getBytes(StandardCharsets.UTF_8))
                .sign();

        return token;
    }
}

这里的AuthenticationManager 注入的是默认实现ProviderManager实例。UsernamePasswordAuthenticationToken 是一个Authentication,我们构建一个Authentication然后交给AuthenticationManager 去校验。一定要注意这里使用的是两个参数的构造方法,它将认证状态设置为了false,接着就需要让AuthenticationManager 去校验用户名和秘密。

    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		...
        this.setAuthenticated(false);
    }

第三步: 拦截请求,校验token

@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private final static String AUTH_HEADER = "Authorization";
    private final static String AUTH_HEADER_TYPE = "Bearer";

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // get token from header:  Authorization: Bearer <token>
        String authHeader = request.getHeader(AUTH_HEADER);
        if (Objects.isNull(authHeader) || !authHeader.startsWith(AUTH_HEADER_TYPE)){
            filterChain.doFilter(request,response);
            return;
        }

        String authToken = authHeader.split(" ")[1];
        log.info("authToken:{}" , authToken);
        //verify token
        if (!JWTUtil.verify(authToken, MyConstant.JWT_SIGN_KEY.getBytes(StandardCharsets.UTF_8))) {
            log.info("invalid token");
            filterChain.doFilter(request,response);
            return;
        }

        final String userName = (String) JWTUtil.parseToken(authToken).getPayload("username");
        UserDetails userDetails = userDetailsService.loadUserByUsername(userName);

        // 注意,这里使用的是3个参数的构造方法,此构造方法将认证状态设置为true
        UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        
        //将认证过了凭证保存到security的上下文中以便于在程序中使用
        SecurityContextHolder.getContext().setAuthentication(authentication);

        filterChain.doFilter(request, response);
    }
}

JwtAuthenticationTokenFilter继承 OncePerRequestFilter,其会拦截http请求,然后检查其header: Authorization携带的 jwt。如果通过了就从jwt中获取用户名,然后到数据库(或者redis)里查询用户信息,然后生成验证通过的UsernamePasswordAuthenticationToken 一定要注意这次使用的是3个参数的构造函数,其将认证状态设置为了true

	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
          ...
		super.setAuthenticated(true); // must use super, as we override
	}

认证通过使用3个参数的构造函数,需要用它进行接下来的认证,则使用2个参数的构造函数。

第四步: 配置security

由于springboot 2.7.0以后弃用了WebSecurityConfigurerAdapter,所以我们直接采用最新的写法,网上教程很多都是老的写法。

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
    ...

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

    //我们自定义的拦截器
    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilter();
    }
    
...
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        //基于token,所以不需要csrf防护
        httpSecurity.csrf().disable()
                //基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //登录注册不需要认证
                .antMatchers("/user/login", "/user/register").permitAll()
                //除上面的所有请求全部需要鉴权认证
                .anyRequest()
                .authenticated();
        //禁用缓存
        httpSecurity.headers().cacheControl();
        //将我们的JWT filter添加到UsernamePasswordAuthenticationFilter前面,因为这个Filter是authentication开始的filter,我们要早于它
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        
        ...

        return httpSecurity.build();
    }
}

完成以上4步就OK了,文中省略了很多细节代码,具体还是要查看源码,你可以在文末找到源码链接文末源码。

自定义Authentication Provider

还记得Authentication Provider是干什么的吗?具体的认证过程就发生在provider中,就是那个比较用户提交的凭证和程序保存凭证的过程。默认使用的是DaoAuthenticationProvider,一般情况都够用了。

自定义Provider

下面我们自定义一个provider,我们自己获取保存的用户凭证,自己比较。

public class JwtAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private  PasswordEncoder passwordEncoder;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = String.valueOf(authentication.getPrincipal());
        String password = String.valueOf(authentication.getCredentials());

        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if(passwordEncoder.matches(password,userDetails.getPassword())){
           return new UsernamePasswordAuthenticationToken(username, password, userDetails.getAuthorities());
        }

        throw new BadCredentialsException("Error!!");
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.equals(authentication);
    }
}

值的注意的是我这里仍然使用了UserDetailsService 这个Spring Security提供的接口来获取程序保存的用户凭证。我们这里也可以不使用它,而直接使用我们自己定义的类,例如自己写个查询用户信息的service即可。

修改配置

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilter();
    }

    @Bean
    public JwtAuthenticationProvider jwtAuthenticationProvider(){
        return new JwtAuthenticationProvider();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        //由于使用的是JWT,这里不需要csrf防护
        httpSecurity.csrf().disable()
                //基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //对登录注册允许匿名访问
                .antMatchers("/user/login", "/user/register").permitAll()
                .anyRequest()// 除上面外的所有请求全部需要鉴权认证
                .authenticated();
        //禁用缓存
        httpSecurity.headers().cacheControl();
        //使用自定义provider
        httpSecurity.authenticationProvider(jwtAuthenticationProvider());
        //添加JWT filter
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        ...

        return httpSecurity.build();
    }
}

我们使用

httpSecurity.authenticationProvider(jwtAuthenticationProvider());

将自定义的provider交给AuthenticationManager使其生效。

认证失败与授权失败处理

当认证失败或者授权失败时我们怎么处理呢?

认证失败处理

首先我们提供一个AuthenticationEntryPoint接口的实现,response的内容根据你的需求修改

@Component
public class MyUnauthorizedHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println("认证失败");
        response.getWriter().flush();
    }
}

将其配置到config中

...
public class WebSecurityConfig {

    @Autowired
    private MyUnauthorizedHandler unauthorizedHandler;
    ...

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        ...
        httpSecurity.exceptionHandling()
                .authenticationEntryPoint(unauthorizedHandler);

        return httpSecurity.build();
    }
}

授权失败处理

与认证失败处理方法类似。

首先我们提供一个AccessDeniedHandler 接口的实现,response的内容根据你的需求修改

@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println("禁止访问");
        response.getWriter().flush();
    }
}

将其配置到WebSecurityConfig 中

...
public class WebSecurityConfig {

    @Autowired
    private MyAccessDeniedHandler accessDeniedHandler;
    
    ...

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        ...
        httpSecurity.exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler);

        return httpSecurity.build();
    }
}

支持方法级别的授权

Spring Security支持方法级别的授权。什么意思呢,例如你有一个API只能是admin调用,其他角色不允许。

我们在web配置类添加一个注解,如下所示。

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
    ...
}

然后在controller里使用@PreAuthorize注解,里面使用spring表达式(SpEL)来设置条件,例如下面的api只允许admin角色方法。

@PreAuthorize("hasRole('admin')")
@GetMapping("/users/{id}")
public String getUserDetail(@PathVariable String id){
    return "用户详情:" + id;
}

那怎么判断当前请求用户是什么角色呢?还记得我们在认证通过时传入UsernamePasswordAuthenticationToken 构造函数的第三个参数吗?它是一个Collection<? extends GrantedAuthority> authorities,里面保存的就是此用户的角色。

 UsernamePasswordAuthenticationToken authentication =
         new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());

顺便在此强调一下:UsernamePasswordAuthenticationToken 仅仅是一个凭证,里面保存了用户的信息,例如username,password,权限等。

总结

Spring Security总的来说很难上手,初学者一定要抓大放小,抓住主要流程,其他的就比较容易理解了。

源码

一如既往,你可以从首发文章末尾找到源码地址:秒懂SpringBoot之全网最易懂的Spring Security教程

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

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

相关文章

PMP好考吗,有多大的价值?

关于PMP考试题型及考试内容&#xff0c;PMP考试共200道单选题&#xff0c;其中25道题不计分&#xff0c;会被随机抽查&#xff0c;答对106道题以上通过考试&#xff0c;参考比例106/175&#xff0c;60.57%估计答对&#xff08;10625&#xff09;道题及上即可通过&#xff0c;参…

全国进入裁员潮,到底是大厂难混?还是我技不如人?

前言 面对裁员&#xff0c;每个人的心态不同。他们有的完全没有料想到自己会被裁&#xff0c;有的却对裁员之事早有准备。大多数人&#xff0c;我想是焦虑失落的吧。 01 “降本增效”&#xff0c;HR怒提裁员刀 小默 | 32岁 芯片行业 人力资源 1月份&#xff0c;身处芯片行业H…

额度系统设计

一、额度生命周期额度生效/失效&#xff1a;授信的时候风控返回用户额度&#xff0c;当额度有效期到期之后额度失效&#xff1b;额度预扣/占用/释放&#xff1a; 当客户来提款的时候&#xff0c;只要提款金额小于授信额度(可用额度)时&#xff0c;先预扣&#xff0c;处理完系统…

如何让APP在Google Play中成为特色

Google Play覆盖了 190 多个地区&#xff0c;数十亿的用户&#xff0c;所以开发者都会希望APP在应用商店中获得推荐位。 Google Play 上的精选热门应用类型&#xff1a;热门游戏或应用&#xff0c;畅销应用&#xff0c;安装量增长的应用&#xff0c;产生最多收入的应用。 那么…

Pdfium.Net SDK 4.78.2704 完美Crack/Ptach

不限制时&#xff0c;/不限PDF体积、、、、、// version: 4.78.2704 | file size: 52.7 Mb Pdfium .Net SDK C# PDF 库 从头开始或从一堆扫描图像创建 PDF 编辑、合并、拆分和操作 PDF&#xff0c;提取文本和图像 嵌入独立的 Winforms 或 WPF PDF 查看器 支持&#xff1a;.Net…

软件性能测试方案怎么编写?权威的性能测试报告如何申请?

软件性能测试是通过自动化的测试工具模拟多种正常、峰值以及异常负载条件来对系统的各项性能指标进行测试。性能测试在软件的质量保证中起着重要的作用&#xff0c;它包括的测试内容丰富多样。负载测试和压力测试都属于性能测试&#xff0c;两者可以结合进行。 一、软件性能测…

java中调用配置文件中的数据库路径及账号密码

项目场景&#xff1a; 有的时候因为项目的需求,所以需要隐藏数据库的路径,账号密码 解决方案&#xff1a; 话不多说直接上代码 这个分情况而定的: 在jdbc框架中获取方法: 1.获取数据库 Class.forName("oracle.jdbc.OracleDriver"); 2.获取路径,账号,密码 Properti…

pytorch零基础实现语义分割项目(一)——数据概况及预处理

语义分割之数据加载项目列表前言数据集概况数据组织形式数据集划分数据预处理均值与方差结尾项目列表 语义分割项目&#xff08;一&#xff09;——数据概况及预处理 语义分割项目&#xff08;二&#xff09;——标签转换与数据加载 前言 在本专栏的上一个项目中我们介绍了…

Python 使用pandas处理Excel —— 快递订单处理 数据匹配 邮费计算

问题背景 有表A&#xff0c;其数据如下 关键信息是邮寄地址和单号。 表B&#xff1a; 关键信息是运单号和重量 我们需要做的是&#xff0c;对于表A中的每一条数据&#xff0c;根据其单号&#xff0c;在表B中查找到对应的重量。 在表A中新增一列重量&#xff0c;将刚才查到的…

防水防汗耳机什么品牌好?四款防水效果不错的蓝牙耳机推荐

近年来蓝牙耳机中可谓是火爆全网&#xff0c;非常受到大家追捧&#xff0c;当然&#xff0c;也随着蓝牙耳机的增长&#xff0c;很多人不知道蓝牙耳机该如何选择&#xff0c;其实蓝牙耳机不止要音质表现好&#xff0c;佩戴体验好&#xff0c;还有着防水性能不能差&#xff0c;不…

window字符集与利用向导创建mfc

1.字节对应英语1个字符对应1个字节 多字节中文1个字符对应多个字节 宽字节 Unicode utf-8 3个 GBK2个2.多字节转换 为宽字节TEXT是由自适应编码的转换 TCHER 自适应编码的转换 _T是由自适应编码转换&#xff0c;L("")多字节转宽字节3.统计字符串长度统计字符串长度 c…

Ambari2.7.5集群搭建详细流程

0 说明 本文基于本地虚拟机从零开始搭建ambari集群 1 前置条件 1.1 本地虚拟机环境 节点角色ambari-1ambari-server ambari-agentambari-2ambari-agentambari-3ambari-agent 1.2 安装包 1.3 修改主机名并配置hosts文件 hostnamectl set-hostname ambari-1 hostnamectl se…

2022 年度回顾|The Sandbox 开放元宇宙的发展历程

2023 年又会为大家带来什么呢&#xff1f; 2022 年是很值得庆祝的一年。回顾这一年&#xff0c;The Sandbox 开放游戏元宇宙达成诸多里程碑。我们努力让各方面都更接近我们的愿景&#xff1a;一个开放的、去中心化的元宇宙&#xff0c;通过真正的数位所有权赋予创作者权力。社区…

mysql 通过客户端执行now()函数,时差为8小时

1.场景演示 假设当前北京时间是&#xff1a;2023-02-17 19:31:37。明显执行出来的结果和实际时间晚8小时。 所用Mysql版本为&#xff1a; 解决方式&#xff1a; 需要在my.conf文件中的[mysqld]下添加 default-time-zoneAsia/Shanghai 由于这个mysql8.0是通过 docker 安装的&…

【Python合集】我见过最有趣好玩强大的代码都在这里,涨见识啦~建议收藏起来慢慢学。(墙裂推荐)

前言 大家好&#xff0c;我是栗子同学啦~ 所有文章完整的素材源码都在&#x1f447;&#x1f447; 粉丝白嫖源码福利&#xff0c;请移步至CSDN社区或文末公众hao即可免费。 Python 凭借语法的易学性&#xff0c;代码的简洁性以及类库的丰富性&#xff0c;赢得了众多开发者的喜爱…

Linux延时队列工作原理与实现

当进程要获取某些资源&#xff08;例如从网卡读取数据&#xff09;的时候&#xff0c;但资源并没有准备好&#xff08;例如网卡还没接收到数据&#xff09;&#xff0c;这时候内核必须切换到其他进程运行&#xff0c;直到资源准备好再唤醒进程。 waitqueue (等待队列) 就是内核…

【初探人工智能ChatGPT】2、雏形开始长成

【初探人工智能ChatGPT】2、雏形开始长成【初探人工智能ChatGPT】2、雏形开始长成安装Flask封装Web接口雏形设置接收参数功能验证聊天写代码代码补全生成图片写在后面笔者初次接触人工智能领域&#xff0c;文章中错误的地方还望各位大佬指正&#xff01; 【初探人工智能ChatGPT…

NIFI大数据进阶_内嵌ZK模式集群1_搭建过程说明---大数据之Nifi工作笔记0015

集群可以使用nifi内嵌的zookeeper来搭建集群,也可以使用外部的自己装的zookeeper来 搭建集群. 因为nifi是依赖zookeeper集群来进行工作的,为了避免运维人员还需要额外的去搭建,维护一个 zookeeper集群,所以nifi,就内嵌了一个zookeeper集群. 可以看到有两个属性需要配置,第一…

OpenGL - 如何理解 VAO 与 VBO 之间的关系

系列文章目录 LearnOpenGL 笔记 - 入门 01 OpenGLLearnOpenGL 笔记 - 入门 02 创建窗口LearnOpenGL 笔记 - 入门 03 你好&#xff0c;窗口LearnOpenGL 笔记 - 入门 04 你好&#xff0c;三角形 文章目录系列文章目录1. 前言2. 渲染管线的入口 - 顶点着色器2.1 顶点着色器处理过…

关于IIC通讯协议的有关问题

问题&#xff1a;如何正确使用IIC这么优秀的通讯协议呢&#xff1f;解决:第一步&#xff1a;知道起始信号和终止信号当SCL为1的时候&#xff0c;SDA从1变成0&#xff0c;这个就是起始信号&#xff0c;说明可以开始传输&#xff1b;当SCL为1的时候&#xff0c;SDA从0变成1&#…