springboot3+springsecurity+redis 整合登录认证以及权限校验

1. 架构说明

整体架构如下(提供的对应的模块引入),围绕着springsecurity中的三大核心展开:

​ 1、Authentication:存储了认证信息,代表当前登录用户

​ 2、SeucirtyContext:上下文对象,用来获取Authentication

​ 3、SecurityContextHolder:上下文管理对象,用来在程序任何地方获取SecurityContext

重点放在FilterChain的编写上,引入认证的和授权的Filter,并通过调用AuthenticationManager.authenticate和AuthenticationProvider中 写入的加密器、userdetailservice做认证

在这里插入图片描述

2. 依赖引入

由于需要在登录时查询数据库所以需要引入对数据库操作的依赖

<dependencies>
      <!--mybatis和springboot整合-->
      <dependency>
          <groupId>org.mybatis.spring.boot</groupId>
          <artifactId>mybatis-spring-boot-starter</artifactId>
      </dependency>
      <!--Mysql数据库驱动8 -->
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
      </dependency>
      <!--persistence-->
      <dependency>
          <groupId>javax.persistence</groupId>
          <artifactId>persistence-api</artifactId>
      </dependency>
      <!--通用Mapper4-->
      <dependency>
          <groupId>tk.mybatis</groupId>
          <artifactId>mapper</artifactId>
      </dependency>
      <!--SpringBoot集成druid连接池-->
      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid-spring-boot-starter</artifactId>
      </dependency>
      <!--lombok-->
      <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <version>1.18.28</version>
          <scope>provided</scope>
      </dependency>
      <!--cloud_commons_utils-->
      <dependency>
          <groupId>com.simple.cloud</groupId>
          <artifactId>simpleCloud_api_commons</artifactId>
          <version>1.0-SNAPSHOT</version>
      </dependency>
      <!-- Spring Security依赖 -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
          <scope>provided </scope>
      </dependency>

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

3. application.yml配置文件编写

spring:
  data:
    redis: # redis使用
      host: localhost
      port: 6379
      database: 0
      timeout: 1800000
      password:
      jedis:
        pool:
          max-active: 20 #最大连接数
          max-wait: -1    #最大阻塞等待时间(负数表示没限制)
          max-idle: 5    #最大空闲
          min-idle: 0     #最小空闲

  datasource:
    type: com.alibaba.druid.pool.DruidDataSource 
    driver-class-name: com.mysql.cj.jdbc.Driver 
    # 注意修改为用户存在的数据库
    url: jdbc:mysql://localhost:3306/对应库名称?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: abc123

# ========================mybatis===================
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.atguigu.cloud.entities
  configuration:
    map-underscore-to-camel-case: true

3. config核心配置文件

注意:security以及摒弃了之前继承 WebSecurityConfigurerAdapter 的方法,鼓励开发者自己写配置类将bean注入容器中,所以之前的springboot2的整合方法已经不好用啦,但是不用慌,大体上的实现还是一样的,毕竟只是换了一个壳子😁

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig{
}

3.1 加密器引入

自定义一个Md5 加密器

@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {
    public String encode(CharSequence rawPassword) {
        return MD5Helper.encrypt(rawPassword.toString());
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(MD5Helper.encrypt(rawPassword.toString()));
    }
}

3.1.1 MD5Helper

public final class MD5Helper {

    public static String encrypt(String strSrc) {
        try {
            char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f' };
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }
}

3.1.2 config中注入依赖

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

3.2 UserDetailServer 引入

直接使用Lambda表达式在config中注入

@Bean
public UserDetailsService userDetailsService(){
    // 调用 JwtUserDetailService实例执行实际校验
    return email -> {
        //实际上用email进行匹配 从数据库中获取
        SysUser authUser = sysUserMapper.getUserByEmail(email);

        if(null == authUser) {
            throw new UsernameNotFoundException("邮箱不存在!");
        }

        return new CustomUser(authUser, Collections.emptyList());
    };
}

基于mybatis实现对数据库的查找功能 —

3.2.1 sysUserMapper.java

public interface SysUserMapper extends Mapper<SysUser> {

    // 根据email获取用户
    SysUser getUserByEmail(String email);
}

3.2.2 sysUserMapper.xml 实现类

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.simple.cloud.mapper.SysUserMapper">
  <resultMap id="BaseResultMap" type="com.simple.cloud.entities.SysUser">
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="username" jdbcType="VARCHAR" property="username" />
    <result column="email" jdbcType="VARCHAR" property="email" />
    <result column="password" jdbcType="VARCHAR" property="password" />
    <result column="phone" jdbcType="VARCHAR" property="phone" />
    <result column="name" jdbcType="VARCHAR" property="name" />
    <result column="is_deleted" jdbcType="TINYINT" property="isDeleted" />
  </resultMap>

  <!-- // 根据email获取用户
    SysUser getUserByEmail(String email);-->
  <select id="getUserByEmail" resultMap="BaseResultMap">
    SELECT * FROM system_user WHERE email = #{email} AND is_deleted = 0
  </select>
</mapper>

3.3 config中引入AuthenticationProvider

有了我们的加密器和userdetail后就可以引入AuthenticationProvider

在安全框架中,AuthenticationProvider 是一个接口或类,用于处理身份验证逻辑。它通常用于验证用户的身份信息,例如用户名和密码、令牌或其他认证凭据。

当用户尝试进行身份验证时,系统会将用户的认证请求传递给相应的 AuthenticationProvider。该提供者负责根据配置的规则和算法来验证用户提供的凭据是否有效。如果验证成功,AuthenticationProvider 通常会创建一个表示经过身份验证的主体(如用户)的 Authentication 对象,并返回该对象以供进一步处理。这个 Authentication 对象包含了主体的相关信息,如用户名、角色、权限等。

/**
 * 调用loadUserByUsername获得UserDetail信息,在AbstractUserDetailsAuthenticationProvider里执行用户状态检查
 *
 * @return
 */
@Bean
public AuthenticationProvider authenticationProvider() {
    DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
    // DaoAuthenticationProvider 从自定义的 userDetailsService.loadUserByUsername 方法获取UserDetails
    authProvider.setUserDetailsService(userDetailsService());
    // 设置密码编辑器
    authProvider.setPasswordEncoder(passwordEncoder());
    return authProvider;
}

3.4 AuthenticationManager

上面一点提到了Authentication ,其中包含了

​ 1、Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象

​ 2、Credentials:用户凭证,一般是密码(在获取用户权限后会销毁,以保证安全,防止泄露)

​ 3、Authorities:用户权限

而在security中使用AuthenticationManager 来对其执行校验,所以需要引入相应的bean

/**
 * 登录时需要调用AuthenticationManager.authenticate执行一次校验
 *
 * @param config
 * @return
 * @throws Exception
 */
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
    return config.getAuthenticationManager();
}

3.5 两个过滤器

在完成上面的配置之后就到了实际处理请求的filter了,我们知道springsecurity就是由一系列的filter构成,具体引入的如下两个

3.5.1 TokenLoginFilter 登录认证

本案例中用email , password做为登录的loginVo(email换成username也是可以的)

@Bean
public TokenLoginFilter authenticationTokenLoginFilter() {
		//这里通过上下文获取容器中的AuthenticationManager
     return new TokenLoginFilter(applicationContext.getBean(AuthenticationManager.class),redisTemplate);
 }
3.5.1.1 TokenLoginFilter 具体实现 继承UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter 是 Spring Security 中的一个过滤器,用于处理基于用户名和密码的身份验证请求。它通常与 AuthenticationProvider 一起使用(里面调用的就是刚刚定义的userdetailserivce),以验证用户提供的凭据是否有效。

当用户尝试通过用户名和密码进行身份验证时,系统会将认证请求传递给 UsernamePasswordAuthenticationFilter。该过滤器负责从请求中提取用户名和密码,并将它们封装成一个 Authentication 对象。然后,它会调用配置的 AuthenticationProvider 来验证这个 Authentication 对象。

如果 AuthenticationProvider 验证成功,即用户名和密码匹配,那么 UsernamePasswordAuthenticationFilter 会创建一个表示经过身份验证的主体(如用户)的 Authentication 对象,并将其存储在安全上下文中。这样,后续的请求就可以访问到已认证的用户信息。

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    private RedisTemplate redisTemplate;

    public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.setAuthenticationManager(authenticationManager);
        this.setPostOnly(false);
        //指定登录接口及提交方式,可以指定任意路径
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/system/loginCon/login","POST"));
    }

    /**
     * 登录认证
     * @param req
     * @param res
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
        try {
            LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);

            Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getEmail(), loginVo.getPassword());
            return this.getAuthenticationManager().authenticate(authenticationToken);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 登录成功
     * @param request
     * @param response
     * @param chain
     * @param auth
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        //获取当前用户
        CustomUser customUser = (CustomUser)auth.getPrincipal();
        //生成token
        String token = JWTHelper.createToken(customUser.getSysUser().getId(),
                customUser.getSysUser().getEmail());

        //获取当前用户权限数据,放到Redis里面 key:id   value:当前用户
        //设置90s过期 即是登录之后会立即请求用户信息调用info() 这里存入的数据只是为了再info()处做双向认证
        //上面的info后面会讲到 , 在具体的业务中请求用户信息包括了权限信息
        redisTemplate.opsForValue().set(customUser.getSysUser().getId()+"——BUSINESS_KEY",
                JSON.toJSONString(customUser.getSysUser()) , 90 , TimeUnit.SECONDS);

        //返回token给前端(或者登录请求的调用者)
        Map<String,Object> map = new HashMap<>();
        map.put("token",token);

        ResponseUtil.out(response, ResultData.success(map));
    }

    /**
     * 登录失败
     * @param request
     * @param response
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {

        ResponseUtil.out(response, ResultData.fail(ResultCodeEnum.RC401.getCode(), e.getMessage()));
    }
}

3.5.2 TokenAuthenticationFilter 权限认证

@Bean
public TokenAuthenticationFilter authenticationJwtTokenFilter() {
    return new TokenAuthenticationFilter(redisTemplate);
}
3.5.2.1 TokenAuthenticationFilter 具体实现 继承OncePerRequestFilter

OncePerRequestFilter是Spring框架中的一个抽象类,它用于确保在一次请求中只执行一次过滤操作。这个类主要用于实现自定义的过滤器,继承OncePerRequestFilter类并重写doFilterInternal方法来实现具体的过滤逻辑。

public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private RedisTemplate redisTemplate;

    public TokenAuthenticationFilter(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        logger.info("uri:"+request.getRequestURI());
        //如果是登录接口,直接放行
        if("/system/loginCon/login".equals(request.getRequestURI())) {
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        if(null != authentication) {
        		// 放到security上下文中,有需要的地方就可以获取到
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } else {
            ResponseUtil.out(response, ResultData.fail(ResultCodeEnum.RC401.getCode(), ResultCodeEnum.RC401.getMessage()));
        }
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        //请求头是否有token
        String token = request.getHeader("token");
        if(!StringUtils.isEmpty(token)) {
            String email = JWTHelper.getUsername(token);
            Long userId = JWTHelper.getUserId(token);

            if(!StringUtils.isEmpty(email)) {
                //当前用户信息放到ThreadLocal里面
                LoginUserInfoHelper.setUserId(userId);
                LoginUserInfoHelper.setEmail(email);

                //通过 userId 从redis获取权限数据
                //后续在info中放入 一开始从redis拿到的数据都为null
                List<String> authStringList = (List<String>) redisTemplate.opsForValue().get(userId+"_LOGIN_BUSINESS_STORAGE_");

                //把redis获取字符串权限数据转换要求集合类型 List<SimpleGrantedAuthority>
                if(authStringList != null) {
                    List<SimpleGrantedAuthority> authList = new ArrayList<>();
                    for (String val : authStringList) {
                        authList.add(new SimpleGrantedAuthority(val));
                    }
                    //由于授权信息必须是GrantedAuthority对象所以得转换一下
                    return new UsernamePasswordAuthenticationToken(email,null, authList);
                } else {
                    return new UsernamePasswordAuthenticationToken(email,null, new ArrayList<>());
                }
            }
        }
        return null;
    }
}

3.6 security过滤规则

有了上面两个过滤器后,我们应该以什么样的规则来使用呢? 这就引出config中最重要的一环 SecurityFilterChain的bean注入

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // 禁用basic明文验证
        .httpBasic().disable()
        // 前后端分离架构不需要csrf保护
        .csrf().disable()
        // 禁用默认登录页
        .formLogin().disable()
        // 禁用默认登出页
        .logout().disable()
        // 前后端分离是无状态的,不需要session了,直接禁用。
        .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                // 允许所有OPTIONS请求
                .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 允许直接访问授权登录接口
                .requestMatchers(HttpMethod.POST, "/system/loginCon/login").permitAll()
                // 允许 SpringMVC 的默认错误地址匿名访问
                .requestMatchers("/error").permitAll()
                // 允许任意请求被已登录用户访问,不检查Authority
                .anyRequest().authenticated())
        .authenticationProvider(authenticationProvider())
        // 加我们自定义的过滤器,替代UsernamePasswordAuthenticationFilter
        .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
        // 登录过滤器
        .addFilter(authenticationTokenLoginFilter());

    return http.build();
}

这里就偷个懒些不写logout的实现😭

3.8 获取用户权限信息的方法info()

前面的filter中多次提到这个info方法,那具体实现就放在这里啦,主要的目的是获取到权限信息,而前端调用info接口也可以获取到相应的登录用户信息,前端做他的权限控制,后端也做权限控制双重保障
由于前面在登录成功后TokenLoginFiltersuccessfulAuthentication方法向redis中放入了数据(90s过期时间)理想情况下,前端登录请求一成功,返回状态码200,此时会直接调用info方法再次发送请求,请求相应的用户具体信息(菜单按钮权限等用于渲染页面)而后端会把相应的按钮权限放入redis中,后续有请求匹配就会查找是否有相应的权限来决定是否执行

 /**
  * 获取用户信息
  * @return
  */
 @PostMapping("/info")
 public ResultData info(HttpServletRequest request){
     String token = request.getHeader("token");

     //获取用户id
     Long userId = JWTHelper.getUserId(token);
     //根据id从redis中获取当前用户
     SysUser sysUser = JSON.parseObject((String) redisTemplate.opsForValue().get(userId + "——BUSINESS_KEY"), SysUser.class);

     if(sysUser == null)
         return ResultData.fail(ResultCodeEnum.RC401.getCode(), "当前用户未登录,请登录后再试");

     //获取用户的角色
     List<SysRole> sysRoles = sysUserService.selectAllByUserId(userId);
     sysUser.setRoleList(sysRoles);

     //根据id获取所有菜单列表
     List<RouterVo> routerList = sysMenuService.getAllRouterListByUserId(userId);

     //根据id获取所有按钮列表
     List<String> permsList = sysMenuService.getAllMenuListByUserId(userId);

     //map中插入相应的值
     Map<String, Object> map = new HashMap<>();
     map.put("routers",routerList);
     map.put("buttons",permsList);
     map.put("roles",sysUser.getRoleList());
     map.put("name",sysUser.getName());

     // 存放权限信息到redis中 , springsecurity通过 userId 做为key获取权限列表
     redisTemplate.opsForValue().set(sysUser.getId()+"_LOGIN_BUSINESS_STORAGE_", permsList);

     return ResultData.success(map);
 }

4. 最终版config配置类

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig{
    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private ApplicationContext applicationContext;
    @Resource
    private SysUserMapper sysUserMapper;


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

    @Bean
    public TokenAuthenticationFilter authenticationJwtTokenFilter() {
        return new TokenAuthenticationFilter(redisTemplate);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 禁用basic明文验证
            .httpBasic().disable()
            // 前后端分离架构不需要csrf保护
            .csrf().disable()
            // 禁用默认登录页
            .formLogin().disable()
            // 禁用默认登出页
            .logout().disable()
            // 前后端分离是无状态的,不需要session了,直接禁用。
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                    // 允许所有OPTIONS请求
                    .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                    // 允许直接访问授权登录接口
                    .requestMatchers(HttpMethod.POST, "/system/loginCon/login").permitAll()
                    // 允许 SpringMVC 的默认错误地址匿名访问
                    .requestMatchers("/error").permitAll()
                    // 允许任意请求被已登录用户访问,不检查Authority
                    .anyRequest().authenticated())
            .authenticationProvider(authenticationProvider())
            // 加我们自定义的过滤器,替代UsernamePasswordAuthenticationFilter
            .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
            .addFilter(authenticationTokenLoginFilter());

        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService(){
        // 调用 JwtUserDetailService实例执行实际校验
        return email -> {
            //实际上用email进行匹配 从数据库中获取
            SysUser authUser = sysUserMapper.getUserByEmail(email);

            if(null == authUser) {
                throw new UsernameNotFoundException("邮箱不存在!");
            }

            return new CustomUser(authUser, Collections.emptyList());
        };
    }

    /**
     * 调用loadUserByUsername获得UserDetail信息,在AbstractUserDetailsAuthenticationProvider里执行用户状态检查
     *
     * @return
     */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        // DaoAuthenticationProvider 从自定义的 userDetailsService.loadUserByUsername 方法获取UserDetails
        authProvider.setUserDetailsService(userDetailsService());
        // 设置密码编辑器
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    /**
     * 登录时需要调用AuthenticationManager.authenticate执行一次校验
     *
     * @param config
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public TokenLoginFilter authenticationTokenLoginFilter() {
        return new TokenLoginFilter(applicationContext.getBean(AuthenticationManager.class),redisTemplate);
    }
}

到此结束👍希望对各位能有帮助

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

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

相关文章

基于一种改进小波阈值的微震信号降噪方法(MATLAB)

微震是指岩体由于在人为扰动或自然原因下受力变形&#xff0c;发生破裂过程中能量积聚而释放的弹性波或应力波。微震信号具有信噪比低、不稳定性、瞬时性和多样性等特点。因此&#xff0c;在任何损坏之前都会出现微小的裂缝&#xff0c;这种微小的裂缝是由岩层中应力和应变的变…

vue使用screenfull实现全屏模式

vue实现全屏模式可以通过第三方依赖screenfull完成效果。 实现效果&#xff1a;查看源码 首先需要安装第三方依赖 // npm npm install screenfull//yarn yarn add screenfull// pnpm pnpm install screenfull代码实现&#xff1a; <div class"flex-center w100 h…

TC8002D(3W音频功放IC)是一颗带关断模式的音频功放IC

一、概述 TC8002D是一颗带关断模式的音频功放IC。在5V输入电压下工作时&#xff0c;负载(3Ω)上的平均功率为3W&#xff0c;且失真度不超过10%。而对于手提设备而言&#xff0c;当VDD作用于关断端时&#xff0c;TC8002D将会进入关断模式&#xff0c;此时的功耗极低&…

机器学习算法之KNN分类算法【附python实现代码!可运行】

一、简介 在机器学习中&#xff0c;KNN&#xff08;k-Nearest Neighbors&#xff09;分类算法是一种简单且有效的监督学习算法&#xff0c;主要用于分类问题。KNN算法的基本思想是&#xff1a;在特征空间中&#xff0c;如果一个样本在特征空间中的k个最相邻的样本中的大多数属…

常见的一些RELAXED MODEL CONCEPTS

释放一致性(release consistency, RC) RC的核心观点是&#xff1a;使用 FENCE 围绕所有同步操作是多余的 同步获取 (acquire) 只需要一个后续的 FENCE&#xff0c;同步释放 (release) 只需要一个前面的 FENCE。 对于表 5.4 的临界区示例&#xff0c;可以省略 FENCE F11、F14…

Linux-笔记 修改开发板默认时区

1. 时区文件 使用命令date -R查看当前的默认时区&#xff0c;date - R命令会自动解析/etc/localtime 文件&#xff0c;而该文件又是指向“ /usr/share/zoneinfo/$主时区/$次时区 ”&#xff0c;当需要更改到指定的时区只要将/etc/localtime 文件软链接到 ”/usr/share/zoneinf…

Vue的省份联动

Vue的省份联动 一、安装依赖库 npm install element-china-area-data -Snpm install element-ui --save全局使用elemntui组件库 import ElementUI from element-ui; import element-ui/lib/theme-chalk/index.css;Vue.use(ElementUI);二 、代码如下 <template><div…

HarmonyOS开发之ArkTS使用:用户登录页面应用

目录 目录 前言 关于HarmonyOS 环境准备 新建项目 设计用户登录页面 1. 布局设计 2. 编写ArkTS代码 运行和测试 结束语 前言 随着HarmonyOS&#xff08;鸿蒙操作系统&#xff09;的不断发展&#xff0c;越来越多的开发者开始投入到这个全新的生态系统中&#xff0c;而…

BeyondCompare4 下载\安装\免费使用

1. 官网 下载 Download Beyond Compare Free Trial 2. 安装&#xff08;无脑下一步&#xff09; 3.永久免费使用 修改注册表 A、在搜索栏中输入 regedit &#xff0c;打开注册表 B、 删除项目&#xff1a;计算机 \HKEY_CURRENT_USER\Software\ScooterSoftware\Beyond Compar…

物联网实战--平台篇之(五)账户界面

目录 一、界面框架 二、首页(未登录) 三、验证码登录 四、密码登录 五、帐号注册 六、忘记密码 本项目的交流QQ群:701889554 物联网实战--入门篇https://blog.csdn.net/ypp240124016/category_12609773.html 物联网实战--驱动篇https://blog.csdn.net/ypp240124016/cat…

10. Django Auth认证系统

10. Auth认证系统 Django除了内置的Admin后台系统之外, 还内置了Auth认证系统. 整个Auth认证系统可分为三大部分: 用户信息, 用户权限和用户组, 在数据库中分别对应数据表auth_user, auth_permission和auth_group.10.1 内置User实现用户管理 用户管理是网站必备的功能之一, D…

远动通讯屏,组成和功能介绍

远动通讯屏&#xff0c;组成和功能介绍 远动通讯屏是基于电网安全建设而投入的远方监控厂站信息、远方切除电网负荷的设备&#xff1b;主经是由远动装置、通讯管理机、交换机、GPS对时装置、数字通道防雷器、模拟通道防雷器、屏柜及附件等设备组成。变电站远动通讯系统是指对广…

安装oh-my-zsh(命令行工具)

文章目录 一、安装zsh、git、wget二、安装运行脚本1、curl/wget下载2、手动下载 三、切换主题1、编辑配置文件2、切换主题 四、安装插件1、zsh-syntax-highlighting&#xff08;高亮语法错误&#xff09;2、zsh-autosuggestions&#xff08;自动补全&#xff09; 五、更多优化配…

顺序表的实现(迈入数据结构的大门)(2)

目录 顺序表的头插(SLPushFront) 此时&#xff1a;我们有两个思路&#xff08;数组移位&#xff09; 顺序表的头删(学会思维的变换)(SLPopFront) 顺序表的尾插(SLPushBack) 有尾插就有尾删 既然头与尾部的插入与删除都有&#xff0c;那必然少不了指定位置的插入删除 查找…

汽车之家,如何在“以旧换新”浪潮中大展拳脚?

北京车展刚刚落幕&#xff0c;两重利好正主导汽车市场持续升温&#xff1a;新能源渗透率首破50%&#xff0c;以及以旧换新详细政策进入落地期。 图源&#xff1a;中国政府网 在政策的有力指引下&#xff0c;汽车产业链的各个环节正经历着一场深刻的“连锁反应”。在以旧换新的…

\boldsymbol无法使用

检查是否导入了 unicode-math 宏包、 没有加粗效果 正常加粗了 2024-5-9-15点35分

(八)JSP教程——application对象

application对象是一个比较重要的对象&#xff0c;服务器在启动后就会产生这个application对象&#xff0c;所有连接到服务器的客户端application对象都是相同的&#xff0c;所有的客户端共享这个内置的application对象&#xff0c;直到服务器关闭为止。 可以使用application对…

【SpringBoot记录】自动配置原理(1):依赖管理

前言 我们都知道SpringBoot能快速创建Spring应用&#xff0c;其核心优势就在于自动配置功能&#xff0c;它通过一系列的约定和内置的配置来减少开发者手动配置的工作。下面通过最简单的案例分析SpringBoot的功能特性&#xff0c;了解自动配置原理。 SpringBoot简单案例 根据S…

Linux下的SPI通信

SPI通信 一. 1.SPI简介: SPI 是一种高速,全双工,同步串行总线。 SPI 有主从俩种模式通常由一个主设备和一个或者多个从设备组从。SPI不支持多主机。 SPI通信至少需要四根线,分别是 MISO(主设备数据输入,从设备输出),MOSI (主设数据输出从设备输入),SCLK(时钟信号),CS/SS…

leetcode尊享面试100题(549二叉树最长连续序列||,python)

题目不长&#xff0c;就是分析时间太久了。 思路使用dfs深度遍历&#xff0c;先想好这个函数返回什么&#xff0c;题目给出路径可以是子-父-子的路径&#xff0c;那么1-2-3可以&#xff0c;3-2-1也可以&#xff0c;那么考虑dfs返回两个值&#xff0c;对于当前节点node来说&…
最新文章