springsecurity 身份认证

news2024/11/13 14:26:51

Spring Security简介

  • 是一个功能强大且高度可定制的安全框架,它主要为Java程序提供声明式的身份验证(认证)和访问控制(授权)功能
  • 为基于Spring的企业应用系统提供了全面的安全解决方案,通过声明式的方式管理安全访问控制,减少了企业系统安全控制代码的重复编写,并提供了多种安全功能和机制。

Spring Security的核心作用包括:

  • 认证:验证系统中是否存在该用户,并确认其身份。Spring Security支持多种身份验证机制,如基于表单、HTTP基本认证、OAuth、LDAP等,并允许自定义身份验证机制。
  • 授权:控制用户能访问哪些资源。Spring Security允许定义细粒度的访问控制规则,可以通过注解、XML配置或编程方式来定义角色和权限。
  • 防护攻击:提供防护措施,如防止身份伪造等攻击手段。
  • 加密功能:对密码进行加密和匹配。
  • 会话管理:包括并发控制、超时处理和无效会话处理,并支持使用基于令牌的会话。
  • Remember Me功能:实现“记住我”功能,允许用户在登录后保持身份验证状态,并在下次访问应用程序时自动登录。
  • 集成Spring MVC:与Spring MVC有良好的集成,提供了一组可以在Spring应用上下文中配置的Bean,利用Spring IoC、DI(控制反转和依赖注入)和AOP(面向切面编程)功能,减少编写重复安全代码的工作。34
  • 细粒度授权:支持基于URL的请求授权、方法访问授权、对象访问授权,提供灵活的授权粒度。
  • CSRF防护:提供防护措施,防止跨站请求伪造攻击。

Spring Security 核心功能包括:

  • 认证:提供了多种认证方式,如表单认证、HTTP Basic认证、OAuth2认证等,可以与多种身份验证机制集成。
  • 授权:提供了多种授权方式,如角色授权、基于表达式的授权等,可以对应用程序中的不同资源进行授权。
  • 攻击防护:提供了多种防护机制,如跨站点请求伪造(CSRF)防护、注入攻击防护等。
  • 会话管理:提供了会话管理机制,如令牌管理、并发控制等。
  • 监视与管理:提供了监视与管理机制,如访问日志记录、审计等。

最核心的功能为:身份认证、授权、防护攻击

Spring Security的核心原理是拦截器(Filter)

  • Spring Security会在Web应用程序的过滤器链中添加一组自定义的过滤器,这些过滤器可以实现身份验证和授权功能。
  • 当用户请求资源时,Spring Security会拦截请求,并使用配置的身份验证机制来验证用户身份。如果身份验证成功,Spring Security会授权用户访问所请求的资源。

Spring Security的具体工作原理如下:

  • 用户请求Web应用程序的受保护资源。
  • Spring Security拦截请求,并尝试获取用户的身份验证信息。
  • 如果用户没有经过身份验证,Spring Security将向用户显示一个登录页面,并要求用户提供有效的凭据(用户名和密码)。
  • 一旦用户提供了有效的凭据,Spring Security将验证这些凭据,并创建一个已认证的安全上下文(SecurityContext)对象。
  • 安全上下文对象包含已认证的用户信息,包括用户名、角色和授权信息。
  • 在接下来的请求中,Spring Security将使用已经认证的安全上下文对象来判断用户是否有权访问受保护的资源。
  • 如果用户有权访问资源,Spring Security将允许用户访问资源,否则将返回一个错误信息。

身份认证

Spring Security的核心原理是拦截器(Filter)

  • Spring Security会在Web应用程序的过滤器链中添加一组自定义的过滤器,这些过滤器可以实现身份验证和授权功能。
  • 当用户请求资源时,Spring Security会拦截请求,并使用配置的身份验证机制来验证用户身份。如果身份验证成功,Spring Security会授权用户访问所请求的资源。

DelegatingFilterProxy

  • Spring提供了一个名为DelegatingFilterProxy的过滤器实现,它本身不做任何事情。
  • 它的内部有一个属性targetBeanName,真正实现过滤请求的Filter是在Application Context中找到名称为targetBeanName并且实现Filter接口的bean,在Spring Security中这个bean的默认实现是FilterChainProxy
  • 所以DelegatingFilterProxy 会将Spring中的 filter 注册到servlet中(并非一个过滤器,而是过滤器链,这些过滤器链由SecurityFilterProxy管理)

请添加图片描述

DefaultSecurityFilterChain

  • 是Spring Security中的一个重要组件,它负责Spring Security的过滤器链初始化。
  • 在Spring Security中,DefaultSecurityFilterChain 是一个Bean,它是通过@EnableWebSecurity注解自动配置的。这个Bean负责创建一组过滤器,这些过滤器提供了身份验证、授权和其他安全功能。

Spring Security启动的时候,会默认加载16个默认的滤器

SecurityProperties

以下为源代码:

  • 所有的配置以:spring.security 开头

  • 有一个内部类user,默认的用户名是user,默认的密码是使用UUID生成的,都可以在配置文件中修改
    请添加图片描述

UsernamePasswordAuthenticationFilter: 过滤器

  • 是Spring Security提供的默认身份验证过滤器,用于处理基于表单的用户名密码身份验证请求。
  • 监听POST请求的"/login"路径,接收用户名和密码等凭证,并将其封装为一个 UsernamePasswordAuthenticationToken 对象,然后通过 AuthenticationManager 进行身份验证。
  • 登录请求中携带的用户信息,一定要是 username 和 password ,如果想要变更参数名称,需要设置usernameParameterpasswordParameter两个参数的名称

Spring Security的认证流程大致可以分为两个过程,

  • 首先是用户登录认证的过程,然后是用户访问受保护资源时的授权过程。
  • 在认证过程中,用户需要提供用户名和密码,Spring Security通过UsernamePasswordAuthenticationFilter将用户名和密码封装成Authentication对象,并交由AuthenticationManager进行认证。
  • 如果认证成功,则认证结果会存储在SecurityContextHolder中。在授权过程中,Spring Security会检查用户是否有访问受保护资源的权限,如果没有则会重定向到登录页面进行认证。

Spring Security基于用户名和密码的认证模式流程

  • 拦截未授权的请求,重定向到登录页面的过程:
    • 当用户访问需要授权的资源时,Spring Security会检查用户是否已经认证(即是否已登录),如果没有登录则会重定向到登录页面。
    • 重定向到登录页面时,用户需要输入用户名和密码进行认证。
  • 表单登录的过程:
    • 用户在登录页面输入用户名和密码,提交表单。
    • Spring Security的UsernamePasswordAuthenticationFilter拦截表单提交的请求,并将用户名和密码封装成一个Authentication对象。
    • AuthenticationManager接收到Authentication对象后,会根据用户名和密码查询用户信息,并将用户信息封装成一个UserDetails对象。
    • 如果查询到用户信息,则将UserDetails对象封装成一个已认证的Authentication对象并返回,如果查询不到用户信息,则抛出相应的异常。
    • 认证成功后,用户会被重定向到之前访问的资源。如果之前访问的资源需要特定的角色或权限才能访问,则还需要进行授权的过程。

基于内存的用户认证

使用配置类来配置账号密码,访问效果和在配置文件中配置效果相同,但是使用配置类后,配置文件中的配置就不生效了

@Configuration
// 开启spring Security的自定义配置,在springBoot项目中可以省略此注解,因为Security-stater默认开启了自定义配置
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        // 这里测试的是基于内存的情况,实际情况并不会使用
        InMemoryUserDetailsManager man = new InMemoryUserDetailsManager();
        // 使用InMemoryUserDetailsManager管理user对象
        man.createUser(User.withDefaultPasswordEncoder().username("lisi").password("123465789")
                .roles("admin").build());
        // 将用户的信息以bean的形式存入spring容器
        return man;
    }

}

UsernamePasswordAuthenticationFilter: 过滤器

  • 是Spring Security提供的默认身份验证过滤器,用于处理基于表单的用户名密码身份验证请求。
  • 监听POST请求的"/login"路径,接收用户名和密码等凭证,并将其封装为一个 UsernamePasswordAuthenticationToken 对象,然后通过 AuthenticationManager 进行身份验证。
  • 登录请求中携带的用户信息,一定要是 username 和 password ,如果想要变更参数名称,需要设置usernameParameterpasswordParameter两个参数的名称
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .anyRequest().authenticated()
                )
            	// 配置自定义表单的参数名称
                .formLogin(form->form.
                        usernameParameter("account").
                        passwordParameter("pass").
                        failureUrl("/error") //检验出错时路由的地址
                )
                .httpBasic(withDefaults());
        
        return http.build();
    }

请添加图片描述
修改参数名后,请求示例:

请添加图片描述

密码加密算法

hash算法

  • 只能加密,不能解密,即使密码泄漏也只能暴力破解

彩虹表

  • 彩虹表攻击(Rainbow Table Attack)是一种密码破解技术,用于破解密码哈希的安全性。它是一种预先计算密码哈希和其对应明文的巨大数据表的方法。虽然彩虹表攻击不是直接暴力破解密码,但它可以更快地破解加密密码,尤其是当使用弱密码时。
  • 密码通常不会以明文形式存储在数据库中,而是将其哈希后的值存储在数据库中。哈希是一种单向函数,将输入数据转换为固定长度的哈希值。例如,常用的哈希函数如MD5、SHA-1、SHA-256等。因此,当用户登录时,系统会将其提供的密码进行哈希,然后将其与数据库中存储的哈希值进行比较。
  • 彩虹表攻击的基本原理如下:
    • 预计算阶段:攻击者使用密码哈希函数和彩虹表生成算法,预先计算大量的哈希值和对应的明文密码,并将结果存储在一个庞大的彩虹表中。这个表可以包含数亿条数据项。
    • 破解阶段:当攻击者获得了存储在目标系统中的密码哈希值后,他们可以在彩虹表中查找匹配项。如果哈希值在彩虹表中找到了匹配,攻击者就可以通过查找对应的明文密码来获取用户的原始密码。
  • 彩虹表攻击的限制:
    • 彩虹表攻击的成功率受到彩虹表的规模和生成算法的影响。如果目标密码很复杂,彩虹表需要更大的规模才能成功破解。
    • 一些安全措施可以减轻彩虹表攻击的影响,如“盐”(Salt)。盐是一个随机值,附加在用户密码上然后一同进行哈希。这使得即使两个用户使用相同的密码,由于不同的盐,他们的哈希值也是不同的。
    • 或者使用更强大、更缓慢的哈希函数(如bcrypt、scrypt)可以大幅降低彩虹表攻击的成功率。
  • 但是随着计算机性能的提升,hash算法越来越容易被破解,所以Spring Security 采用自适应单向函数来存储密码

在Spring Security 中

  • 使用的是一种自适应单向函数 (Adaptive One-way Functions)来处理密码问题
  • 这种自适应单向函数在进行密码匹配时,会有意占用大量系统资源(例如CPU、内存等),这样可以增加恶意用户攻击系统的难度。
  • 由于自适应单向函数有意占用大量系统资源,因此每个登录认证请求都会大大降低应用程序的性能,
  • 通常把 Spring Secuity 验证密码验证调整到约1秒钟左右,这样可以大大减缓密码被破解的风险,增强系统的安全性。
  • 在Spring Securiy 中自适应单项函数有: bcrypt、PBKDF2、sCrypt 以及 argon2 ,默认为 bcrypt

PasswordEncoder 自适应单项函数具体实现:

  • BCryptPasswordEncoder 使用 bcrypt 算法对密码进行加密,为了提高密码的安全性,bcrypt算法故意降低运行速度,以增强密码破解的难度。同时 BCryptP asswordEncoder “为自己带盐”开发者不需要额外维护一个“盐” 字段,使用 BCryptPasswordEncoder 加密后的字符串就已经“带盐”了,即使相同的明文每次生成的加密字符串都不相同。
  • Argon2PasswordEncoder 使用 Argon2 算法对密码进行加密,Argon2 曾在 Password Hashing Competition 竞赛中获胜。为了解决在定制硬件上密码容易被破解的问题,Argon2也是故意降低运算速度,同时需要大量内存,以确保系统的安全性。
  • Pbkdf2PasswordEncoder 使用 PBKDF2 算法对密码进行加密,和前面几种类似,PBKDF2算法也是一种故意降低运算速度的算法,当需要 FIPS (Federal Information Processing Standard,美国联邦信息处理标准)认证时,PBKDF2 算法是一个很好的选择。
  • SCryptPasswordEncoder 使用scrypt 算法对密码进行加密,和前面的几种类似,serypt 也是一种故意降低运算速度的算法,而且需要大量内存。

BCryptPasswordEncoder

  • encode(“password”),中有一个随机数参与了运算,所以即使同样的密码,多次加密得到的密文也不一样
  • matches(“password”,“盐值”);

DelegatingPasswordEncoder

  • 如果不自定义容器的PasswordEncoder,默认会通过DelegatingPasswordEncoder来创建加密器

  • 实现了PasswordEncoder,可以生成不同加密算法的加密器,默认使用的加密算法是BCryptPasswordEncoder,生成的密文会带上加密算法名称的前缀( {前缀}+密文 )

  • 因为需要根据密文的前缀来确定是那种加密算法

如何对密码进行加密

  • 在 Spring Security 中对密码进行加密通常使用的是密码编码器(PasswordEncoder)。
  • PasswordEncoder 的作用是将明文密码加密成密文密码,以便于存储和校验。Spring Security 提供了多种常见的密码编码器,例如 BCryptPasswordEncoder、SCryptPasswordEncoder、StandardPasswordEncoder 等。
  • 引入依赖,创建配置类后,在使用密码的地方调用 passwordEncoder.encode() 方法对密码进行加密即可

基于数据库的认证

注入密码的加密方式

  • 在Spring Security 5.7.0-M2中,Spring就废弃了WebSecurityConfigurerAdapter
@Configuration
// 以省略其他无关内容
public class SecurityConfig {

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

}

自定义DbUserDetailsManager

  • 参考InMemoryUserDetailsManager实现UserDetailsManager UserDetailsPasswordService
@Slf4j
@Component
public class DbUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {

    @Autowired
    private UsersMapper usersMapper;

    /**
     * 根据传入的账号,获取用户信息
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("username:" + username);
        // 用户名称不唯一,这里的账号名account,不是用户名
        // 这里user实现了Security的UserDetails对象,去数据库查询用户信息
        UsersDO user = usersMapper.selectByAccount(username);
        if (user == null || StringUtils.isEmpty(user.getPassword())) {
            throw new UsernameNotFoundException(username + ":用户不存在");
        }
        return user;
    }


    public UserDetails updatePassword(UserDetails user, String newPassword) {
        return null;
    }

    public void createUser(UserDetails user) {

    }

    public void updateUser(UserDetails user) {

    }

    public void deleteUser(String username) {

    }

    public void changePassword(String oldPassword, String newPassword) {

    }

    public boolean userExists(String username) {
        return false;
    }

}

自定以的user类

@Data
@TableName("tm_user")
// 实现了Security的UserDetails对象
public class UsersDO implements UserDetails {
    private String id;
    private String account;
    private String username;
    private String password;
    private String sex;
    private String age;
    private String phoneNumber;
    private String departmentNo;
    private String position;
    private String described;
    private String cname;

    @TableLogic
    @TableField(value = "DELETED", fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
    private String deleted;

    @Override
    public boolean isEnabled() {
        // 通过逻辑删除字段判断是否启用
        return Integer.valueOf(this.getDeleted()) == 0 ? true : false;
    }

    public boolean isAccountNonExpired() {
        //用户是否未过期
        return true;
    }

    public boolean isCredentialsNonExpired() {
        // 用户凭证是否未过期
        return true;
    }

    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 权限列表
        return null;
    }

    public boolean isAccountNonLocked() {
        // 用户是否没有被锁定
        return true;
    }
}

Security 的默认配置

  • 这里的SecurityFilterChain就是默认的过滤器链
  • authorizeHttpRequests 是开启授权保护
    • anyRequest() 表示对所有请求开启授权保护
    • authenticated() 对以认证的请求自动授权
  • formLogin 使用表单授权方式,生成默认的登录、登出页
  • httpBasic 使用基本授权方式,不会生成默认的登录、登出页,使用的是浏览器提供的用户名密码的输入弹框
    /**
     * 这里是 Security 的默认配置
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .anyRequest().authenticated()
                )
                .formLogin(withDefaults())
                .httpBasic(withDefaults());
        // 为例测试方便,关闭 csrf 攻击防护 (不关闭,所有的post的请求都会被拦截)
        http.csrf(a -> a.disable());
        return http.build();
    }

自定义认证

自定义认证

  • AuthenticationSuccessHandler 接口的作用是做用户认证成功后执行的操作处理器
  • AuthenticationFailureHandler接口的作用是做用户认证失败后执行的操作处理器
  • LogoutSuccessHandler 接口的作用是做用户登场后执行的操作
  • AuthenticationEntryPoint 请求未认证处理逻辑
  • 跨域处理:http.cors(withDefaults());
  • 会话并发处理:SessionInformationExpiredStrategy可以控制一个账号的最大登录数量

自定义AuthenticationSuccessHandlerAuthenticationFailureHandlerLogoutSuccessHandler

@Configuration
@Slf4j
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler,
        LogoutSuccessHandler, AuthenticationEntryPoint {


    /**
     * 认证成功
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        log.info("返回成功-------------->");
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> res = new HashMap<>();
        res.put("code", "0");
        res.put("message", "登录成功");
        Map<String, Object> data = new HashMap<>();
        // 用户身份信息
        data.put("principal", authentication.getPrincipal());
        // 用户凭证信息
        data.put("credentials", authentication.getCredentials());
        // 用户的权限信息
        data.put("authorities", authentication.getAuthorities());
        res.put("data", data);
        response.getWriter().println(JSONUtil.toJsonStr(res));
    }

    /**
     * 认证失败
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.info("返回失败-------------->");
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> res = new HashMap<>();
        res.put("code", "-1");
        res.put("message", "登录失败");
        Map<String, Object> data = new HashMap<>();
        data.put("message", exception.getMessage());
        data.put("localizedMessage", exception.getLocalizedMessage());
        res.put("data", data);
        response.getWriter().println(JSONUtil.toJsonStr(res));
    }

    /**
     * 登出逻辑
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("执行登出逻辑-------------->");
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> res = new HashMap<>();
        res.put("code", "0");
        res.put("message", "注销成功");
        Map<String, Object> data = new HashMap<>();
        // 用户身份信息
        data.put("principal", authentication.getPrincipal());
        // 用户凭证信息
        data.put("credentials", authentication.getCredentials());
        // 用户的权限信息
        data.put("authorities", authentication.getAuthorities());
        res.put("data", data);
        response.getWriter().println(JSONUtil.toJsonStr(res));
    }

    /**
     * 请求未认证
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.info("执行请求未认证逻辑-------------->");
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> res = new HashMap<>();
        res.put("code", "-1");
        res.put("message", "请求未认证");
        Map<String, Object> data = new HashMap<>();
        data.put("message", authException.getMessage());
        data.put("localizedMessage", authException.getLocalizedMessage());
        res.put("data", data);
        response.getWriter().println(JSONUtil.toJsonStr(res));
    }
            
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        log.info("返回成功-------------->");
        HttpServletResponse response = event.getResponse();
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> res = new HashMap<>();
        res.put("code", "-1");
        res.put("message", "该账号已从其他设备登录");
        Map<String, Object> data = new HashMap<>();
        res.put("data", data);
        response.getWriter().println(JSONUtil.toJsonStr(res));
    }

}

最后需要在formLogin中注册自定义的配置

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests(authorize -> {
                    // 放行请求
                    authorize.requestMatchers("/assets/**"
                                    , "/login"
                                    , "/test/**"
                            ).permitAll()
                            .anyRequest().authenticated();
                }
        );
        // 配置登录自定义表单的参数名称  formLogin(withDefaults()) :默认的登录表单
        http.formLogin(form -> form.
                        // loginPage("/login").permitAll().
                                usernameParameter("account").
                        passwordParameter("pass").
                        failureUrl("/error") //检验出错时路由的地址
                        .successHandler(new MyAuthenticationSuccessHandler())
                        .failureHandler(new MyAuthenticationSuccessHandler())

                )
                // 配置登出
                .logout(logout -> logout.logoutSuccessHandler(new MyAuthenticationSuccessHandler()))
                .httpBasic(withDefaults());
        // 用户未认证
//        http.exceptionHandling(exc -> exc.authenticationEntryPoint(new MyAuthenticationSuccessHandler()));
        http.sessionManagement(session ->{
            // 一个账号最多允许几处登录,以及对应的处理逻辑
            session.maximumSessions(1).expiredSessionStrategy(new MyAuthenticationSuccessHandler());
        });
        // 为例测试方便,关闭 csrf 攻击防护 (不关闭,所有的post的请求都会被拦截)
//        http.csrf(a -> a.disable());
        // 开启跨域请求
        http.cors(withDefaults());
        return http.build();
    }

最终成功登录不在自动跳转页面,而是返回了预定义的json

请添加图片描述
登录失败也会返回预定义的错误信息
请添加图片描述

注销返回结果

请添加图片描述

请求未认证页面,Spring Security默认情况下这里会跳转到登录页

请添加图片描述
一个账号,在多个客户端登录,后登录的会挤掉先登录的

在这里插入图片描述

Authentication 包含用户认证信息

  • SecurityContextHolder中包含了SecurityContext对象,SecurityContext中包含了Authentication对象

  • 可以获取当前验证通过的用户信息

  • 其中包含:

    • principal:用户身份,如果是用户/密码认证,这个属性就是UserDetails实例
    • credentials:通常就是密码,在大多数情况下,在用户验证通过后就会被清除,以防密码泄露。
    • authorities:用户权限
    /**
     * 获取当前登录用户信息
     */
    @GetMapping("/get")
    public BaseResultModel get() {
        SecurityContext securityContext =SecurityContextHolder.getContext();
        Authentication authentication = securityContext.getAuthentication();
        return BaseResultModel.success(authentication);
    }

登录不同的用户,获取当前用户的信息

请添加图片描述

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

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

相关文章

Windows中安装python/cmd中执行python命令无效

1、问题阐述? 本文章提供非常详细的安装教程。 本文章适合于不会安装python或者安装了python后,在cmd中执行python命令无效的情况。 2、下载python python下载官网地址:Download Python | Python.org 在下面的框子中选择你需要的版本 如果是windows选择如下包 如下版本…

基于Jetpack Compose实现的Android Preference

基于Jetpack Compose实现的Android Preference Jetpack Compose实现的Android偏好Preference实现,实现了CheckBoxPreference、EditTextPreference、SingleChoicePreference、SliderPreference等常见的 Preference 类型, 代码如下: 基类Preference :@Composable fun Prefer…

JMeter中进行JDBC协议压测

在使用Jmeter进行性能测试的时候,不仅是需要对业务的接口进行性能并发测试,有的时候还需要专门对服务器进行压测,比如本次给大家介绍的对数据库进行压测. 在JMeter中进行JDBC协议压测&#xff0c;你需要做以下几个步骤&#xff1a; 如何选择JDBC驱动程序 (1).掌握项目所采用…

泉盛UV-K5扩容2Mbit EEPROM

泉盛UV-K5扩容2Mbit EEPROM 步骤 分离前面板与背板。 拆下电池&#xff0c;底部有个空隙&#xff0c;从缝隙撬开背板。分离前面板时注意喇叭连接线&#xff0c;不要扯断了。 分离屏幕。 先从箭头位置向上挑起&#xff0c;屏幕稍微松动即可左右晃动&#xff0c;直至完全取出。注…

记一次docker容器安装MySQL,navicat无法连接报错(10060错误)

今天在云服务器上使用docker部署mysql 8.0.11时&#xff0c;遇到了一个诡异的问题&#xff0c;在云服务器的docker容器内可以连接上mysql&#xff0c;然而在自己电脑上连接mysql时报错&#xff1a;Can‘t connect to MySQL server on localhost (10060) 下面是网上搜寻的几种可…

知识图谱与LLMs:实时图分析(通过其关系的上下文理解数据点)

大型语言模型 (LLM) 极大地改变了普通人获取数据的方式。不到一年前&#xff0c;访问公司数据需要具备技术技能&#xff0c;包括熟练掌握各种仪表板工具&#xff0c;甚至深入研究数据库查询语言的复杂性。然而&#xff0c;随着 ChatGPT 等 LLM 的兴起&#xff0c;随着所谓的检索…

第17集《修习止观坐禅法要》

请大家打开讲义第四十二面&#xff0c;丁六、正修行第六。 这个是我们小止观正宗分的第六科&#xff0c;前面的五科是一个方便&#xff0c;是我们在修习止观的一个前方便&#xff0c;这一科是说明修习止观的正式的方法。 这个方便跟正修的关系&#xff0c;我们可以讲一个譬喻…

keepalive:

keepalive&#xff1a; 调度器的高可用 vip地址在主备之间的切换&#xff0c;主在工作时&#xff0c;vip地址只在主上&#xff0c;主停止工作&#xff0c;vip漂移到备服务器。 在主备的优先级不变的情况下&#xff0c;主恢复工作&#xff0c;vip会飘回到主服务器。 1、配优…

音视频开发—使用FFmpeg将YUV文件编码成H264裸流文件 C语言实现

文章目录 1.准备工作2.压缩编码工作流程3.详细步骤1. 初始化日志和参数检查2. 输入/输出文件的打开3. 查找和初始化编码器4. 打开编码器5. 帧内存的分配和初始化6. 设置转换上下文&#xff08;SWS&#xff09;7. 读取和转换数据8. 编码过程9. 资源清理 4.完整示例代码 1.准备工…

AI大模型探索之旅:深潜大语言模型的训练秘境

在人工智能的浩瀚星空中&#xff0c;大语言模型无疑是最耀眼的星辰之一&#xff0c;它们以无与伦比的语言理解与生成能力&#xff0c;引领着智能交互的新纪元。本文将带您踏上一场探索之旅&#xff0c;深入大语言模型的训练秘境&#xff0c;揭开其背后复杂而精妙的全景画卷。 …

Qt Quick qml自定义控件:qml实现电池控件

qml入门进阶专栏地址:https://blog.csdn.net/yao_hou/category_9951228.html?spm=1001.2014.3001.5482 本篇博客介绍如何使用qml来实现电池控件,效果图如下: 下面给出实现代码 Battery.qml /*电池组件*/import QtQuick 2.15 import QtQuick.Controls 2.15Rectangle {id: b…

Maven学习笔记——如何在pom.xml中通过坐标为项目导入jar包

注意&#xff1a;我们只导入了一个jar包坐标&#xff0c;但右边项目中确多出来了好几个jar包&#xff0c;这是因为我们导入的该jar包所依赖其他jar包&#xff0c;maven自动帮我们导入了进来

Android-- 集成谷歌地图

引言 项目需求需要在谷歌地图&#xff1a; 地图展示&#xff0c;设备点聚合&#xff0c;设备站点&#xff0c;绘制点和区域等功能。 我只针对我涉及到的技术做一下总结&#xff0c;希望能帮到开始接触谷歌地图的伙伴们。 集成步骤 1、在项目的modle的build.gradle中添加依赖如…

Java软件设计模式-单例设计模式

目录 1.软件设计模式的概念 2.设计模式分类 2.1 创建型模式 2.2 结构型模式 2.3 行为型模式 3.单例设计模式 3.1 单例模式的结构 3.2 单例模式的实现 3.2.1 饿汉式-方式1&#xff08;静态变量方式&#xff09; 3.2.2 懒汉式-方式1&#xff08;线程不安全&#xff09; 3.…

【linux】安装cuda11.0、cuDNN教程,简单易懂,包教包会

【linux】安装cuda11.0、cuDNN教程&#xff0c;简单易懂&#xff0c;包教包会 【创作不易&#xff0c;求点赞关注收藏】 文章目录 【linux】安装cuda11.0、cuDNN教程&#xff0c;简单易懂&#xff0c;包教包会一、版本情况介绍二、安装cuda1、到官网找到对应版本进行安装2、对…

【openwrt】Openwrt系统新增普通用户指南

文章目录 1 如何新增普通用户2 如何以普通用户权限运行服务3 普通用户如何访问root账户的ubus服务4 其他权限控制5 参考 Openwrt系统在默认情况下只提供一个 root账户&#xff0c;所有的服务都是以 root权限运行的&#xff0c;包括 WebUI也是通过root账户访问的&#xff0c;…

使用EndNote添加参考文献,如何区分中英文文献的et al和等?

一、背景 我们在用EndNote添加参考文献时&#xff0c;如遇到超过3个作者&#xff0c;需列出前三位作者&#xff0c;其余用“et al”代替。 但中文文献用“et al”显示不合适&#xff0c;如下图所示&#xff0c;需要用“等”代替。 二、中文参考文献大于3个作者&#xff0c;用等…

5G数字化转型redcap助您“轻”装上阵

RedCap&#xff08;Reduced Capability&#xff09;技术&#xff0c;也称为NR-Light&#xff0c;是针对5G网络的一种轻量化技术规范&#xff0c;旨在为具有较低性能要求的设备提供5G连接。 RedCap技术特点 低成本 降低芯片组和设备成本&#xff1a;RedCap通过减少终端带宽、收…

【Playwright+Python】系列 Pytest 插件在Playwright中的使用

一、命令行使用详解 使用 Pytest 插件在Playwright 中来编写端到端的测试。 1、命令行执行测试 pytest --browser webkit --headed 2、使用 pytest.ini 文件配置 内容如下&#xff1a; [pytest] # Run firefox with UIaddopts --headed --browser firefox效果&#xff1…

STM32入门开发操作记录(三)——按键控制LED

目录 一、模块化二、LED交替闪烁1. LED.c2. LED.h3. 主函数 三、按键控制LED1. Key.c2. Key.h3. LED.c4. LED.h5. 主函数 一、模块化 前篇介绍了如何向项目添加模块&#xff0c;本篇将进一步介绍模块的编写与封装。随着模块的增加&#xff0c;需要用到Manage Project Items&…