基于Spring Boot 3 + Spring Security6 + JWT + Redis实现登录、token身份认证

news2024/11/24 11:00:21

基于Spring Boot3实现Spring Security6 + JWT + Redis实现登录、token身份认证。

  • 用户从数据库中获取。
  • 使用RESTFul风格的APi进行登录。
  • 使用JWT生成token。
  • 使用Redis进行登录过期判断。
  • 所有的工具类和数据结构在源码中都有。

系列文章指路👉
系列文章-基于Vue3创建前端项目并引入、配置常用的库和工具类

项目源码👉
/shijizhe/boot-test

文章目录

    • 依赖版本
    • 原理
    • 代码结构
      • security 配置
      • 用户登录、注册controller,用户服务
      • 用到的工具类
    • 注册 AuthController.register
    • 登录
      • 1.登录API:AuthController.login
      • 2. 登录过滤器:继承UsernamePasswordAuthenticationFilter
      • 3.身份认证:实现AuthenticationProvider
      • 4.从数据库中查询用户信息:实现UserDetailsService
      • 5. Security配置: 使用注解@EnableWebSecurity
    • token身份认证
      • 1. token身份认证过滤器: OncePerRequestFilter
    • UserAuthUtils
      • getUserId
    • 用户登出
      • 实现LogoutSuccessHandler
      • 修改Security配置 : YaSecurityConfig
    • 下一步的计划
    • 参考文章

依赖版本

  • Spring Boot 3.0.6
  • Spring Security 6.0.3

原理

这张图大家已经估计已经看过很多次了。
原理
实现登录认证的过程,其实就是对上述的类按照自己的需求进行自定义扩展的过程。具体不多讲了,别的文章里讲得比我透彻。

show you my code.

代码结构

security 配置

在这里插入图片描述

用户登录、注册controller,用户服务

在这里插入图片描述

用到的工具类

在这里插入图片描述

注册 AuthController.register

将用户密码使用BCrypt加密存储。

    @PostMapping("/register")
    @Operation(summary = "register", description = "用户注册")
    public Object register(@RequestBody @Valid UserRegisterDTO userRegisterDTO) {
        YaUser userById = userService.getUserById(userRegisterDTO.getUserId());
        if(Objects.nonNull(userById)){
            return BaseResult.fail("用户id已存在");
        }
        try {
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            YaUser yaUser = UserRegisterMapper.INSTANCE.registerToUser(userRegisterDTO);
            yaUser.setUserPassword(encoder.encode(userRegisterDTO.getUserPassword()));
            userService.insertUser(yaUser);
            return BaseResult.success("用户注册成功");
        }catch (Exception e){
            return BaseResult.fail("用户注册过程中遇到异常:" + e);
        }
    }

登录

1.登录API:AuthController.login

我们使用RESTFul风格的API来代替表单进行登录。这个接口只是提供一个Swagger调用登录接口的入口,实际逻辑由Filter控制。
在这里插入图片描述

2. 登录过滤器:继承UsernamePasswordAuthenticationFilter

拦截指定的登录请求,交给AuthenticationProvider处理。对Provider返回的登录结果进行处理。

  • 通过指定filterProcessesUrl,指定登录接口的路径。
  • 登录失败,将异常信息返回前端。
  • 登录成功,通过JwtUtils生成token,放入响应header中。并将token用户信息(json字符串)存入Redis中。
  • 通过JwtUtils生成token设置为永不过期,存入Redis的token过期时间设置为30分钟,以便后边做登录过期的判断。
/**
 * <p>
 *  拦截登陆过滤器
 * </p>
 *
 * @author Ya Shi
 * @since 2024/3/21 16:20
 */
@Slf4j
public class YaLoginFilter extends UsernamePasswordAuthenticationFilter {
    private final RedisUtils redisUtils;

    private final Long expiration;

    public YaLoginFilter(AuthenticationManager authenticationManager, RedisUtils redisUtils, Long expiration) {
        this.expiration = expiration;
        this.redisUtils = redisUtils;
        super.setAuthenticationManager(authenticationManager);
        super.setPostOnly(true);
        super.setFilterProcessesUrl("/auth/login");
        super.setUsernameParameter("userId");
        super.setPasswordParameter("userPassword");
    }

    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        log.info("YaLoginFilter authentication start");
        // 数据是通过 RequestBody 传输
        UserLoginDTO user = JSON.parseObject(request.getInputStream(), StandardCharsets.UTF_8, UserLoginDTO.class);

        return super.getAuthenticationManager().authenticate(
                new UsernamePasswordAuthenticationToken(user.getUserId(), user.getUserPassword())
        );
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) {
        log.info("YaLoginFilter authentication success: {}", authResult);
        // 如果验证成功, 就生成Token并返回
        UserDetails userDetails = (UserDetails) authResult.getPrincipal();
        String userId = userDetails.getUsername();
        String token = JwtUtils.generateToken(userId);
        response.setHeader(TOKEN_HEADER, TOKEN_PREFIX + token);
        // 将token存入Redis中
        redisUtils.set(REDIS_KEY_AUTH_TOKEN + userId, token, expiration);
        log.info("YaLoginFilter authentication end");
        // 将UserDetails存入redis中
        redisUtils.set(REDIS_KEY_AUTH_USER_DETAIL + userId, JSON.toJSONString(userDetails), 1, TimeUnit.DAYS);

        ServletUtils.renderResult(response, new BaseResult<>(ResultEnum.SUCCESS.code, "登陆成功"));
        log.info("YaLoginFilter authentication end");
    }

    /**
     * 如果 attemptAuthentication 抛出 AuthenticationException 则会调用这个方法
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException failed) throws IOException {
        log.info("YaLoginFilter authentication failed: {}", failed.getMessage());
        ServletUtils.renderResult(response, new BaseResult<>(ResultEnum.FAILED_UNAUTHORIZED.code, "登陆失败:" + failed.getMessage()));
    }

3.身份认证:实现AuthenticationProvider

调用UserDetailsService查询用户的账户、权限信息与登录接口输入的账户、密码对比。认证通过则返回用户信息。

/**
 * <p>
 *  自定义认证
 * </p>
 *
 * @author Ya Shi
 * @since 2024/3/21 15:00
 */

@Component
public class YaAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    YaUserDetailService userDetailService;

    @Autowired
    PasswordEncoder passwordEncoder;


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取用户输入的用户名和密码
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();
        UserDetails userDetails = userDetailService.loadUserByUsername(username);
        boolean matches = passwordEncoder.matches(password, userDetails.getPassword());
        if(!matches){
            throw new AuthenticationException("User password error."){};
        }
        return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
    }

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

4.从数据库中查询用户信息:实现UserDetailsService

从数据库中查询出用户的信息,供AuthenticationProvider登录认证时使用。

  • 用户权限这块,目前还没用到,可以忽略。用户鉴权可能后边会单独补上。
  • 为什么这里没先从Redis取用户信息?
    1. 如果权限或者用户信息变更这里取不到
    2. Redis里不建议存储用户密码。
/**
 * <p>
 *  继承UserDetailsService,实现自定义登陆认证
 * </p>
 *
 * @author Ya Shi
 * @since 2024/3/19 11:32
 */
@Service
public class YaUserDetailService implements UserDetailsService {

    @Autowired
    UserService userService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        YaUser user = userService.getUserById(username);
        if(Objects.isNull(user)){
            throw new UsernameNotFoundException("User not Found.");
        }
        List<YaUserRole> roles = userService.listRoleById(username);
        List<GrantedAuthority> authorities = new ArrayList<>(roles.size());
        roles.forEach( role -> authorities.add(new SimpleGrantedAuthority(role.getRoleId())));

        return new User(username, user.getUserPassword(), authorities);
    }
}

5. Security配置: 使用注解@EnableWebSecurity

  • 注意:Spring Security 6 配置不再继承adapterextends WebSecurityConfigurerAdapter,而是使用@EnableWebSecurity
  • YaTokenFilter是token身份认证过滤器,每次请求都会拦截,然后校验请求header中的token,这个下面会讲。
  • 配置了身份认证过滤器以后,每个请求都会被拦截,即使是在过滤链中配置了permitAll(),还是会返回请求403.
    1. 因此,针对匿名请求、静态资源和swagger请求,在WebSecurityCustomizer中配置WebSecurity.ignoring,相当于直接绕过所有的Filter
    2. 针对登录和注册请求,在身份过滤器中额外配置白名单,单独放行。
  • 自己学习的过程中,很多文章没有按照代码执行顺序去讲,登录和身份认证也是混着讲的,导致整个登录认证的流程理解起来有些困难。
/**
 * <p>
 *  Spring Security 配置文件
 * </p>
 *
 * @author Ya Shi
 * @since 2024/2/29 11:27
 */
@Configuration
@EnableWebSecurity // 开启网络安全注解
public class YaSecurityConfig {

    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;

    @Autowired
    private RedisUtils redisUtils;

    @Value("${ya-app.auth.jwt.expiration:1800}")
    private Long expiration;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 禁用basic明文验证
                .httpBasic().disable()
                // 禁用csrf保护
                .csrf().disable()
                // 禁用session
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 身份认证过滤器
                .authenticationManager(authenticationManager(authenticationConfiguration))
                .authenticationProvider(new YaAuthenticationProvider())
                .authorizeHttpRequests(authorizeHttpRequests ->
                        authorizeHttpRequests
                                // 允许OPTIONS请求访问
                                .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                                // 允许登录/注册接口访问
                                .requestMatchers(HttpMethod.POST, "/auth/login").permitAll()
                                .requestMatchers(HttpMethod.POST, "/auth/register").permitAll()
                                // 允许匿名接口访问
                                .requestMatchers("/anon/**").permitAll()
                                // 允许swagger访问
                                .requestMatchers("/swagger-ui/**").permitAll()
                                .requestMatchers("/doc.html/**").permitAll()
                                .requestMatchers("/v3/api-docs/**").permitAll()
                                .requestMatchers("/webjars/**").permitAll()
                                .anyRequest().authenticated()
                )
                .addFilterAt(new YaLoginFilter(authenticationManager(authenticationConfiguration), redisUtils, expiration), UsernamePasswordAuthenticationFilter.class)
                // 让校验Token的过滤器在身份认证过滤器之前
                .addFilterBefore(new YaTokenFilter(redisUtils, expiration), YaLoginFilter.class)
                // 禁用默认登出页
                .logout().disable();
        return http.build();
    }


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

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring()
                .requestMatchers("/webjars/**")
                .requestMatchers("/swagger-ui/**", "/doc.html/**", "/v3/api-docs/**")
                .requestMatchers("/anon/**");
    }

    /**
     * 使用BCrypt加密密码
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

token身份认证

1. token身份认证过滤器: OncePerRequestFilter

  • 对于注册、登录请求,直接放行。
  • 认证失败的几种情况:
    1. 未登录: 未携带token
    2. 凭证异常: 携带错误token
    3. 登录过期: 携带正确的token,但是token在Redis中不存在
    4. 账号在别处登录: 携带正确的token,但是token与Redis中的token不一致。
  • token认证成功后,重新设置Redis中的token的有效时间,实现token续期。查询Redis中的用户信息,如果没有,使用UserDetailsService的服务重新查询出信息,存入缓存中。
    *调用 SecurityContextHolder.getContext().setAuthentication()将用户信息存入Security上下文中,完成身份认证。
/**
 * <p>
 *  每次请求过滤token
 * </p>
 *
 * @author Ya Shi
 * @since 2024/3/21 16:52
 */
@Slf4j
public class YaTokenFilter extends OncePerRequestFilter {

    private final RedisUtils redisUtils;

    private final Long expiration;

    private static final Set<String> WHITE_LIST = Stream.of(
            "/auth/register",
            "/auth/login"
            ).collect(Collectors.toSet());

    public YaTokenFilter(RedisUtils redisUtils, Long expiration) {
        this.redisUtils = redisUtils;
        this.expiration = expiration;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("YaTokenFilter doFilterInternal start");
        final String authorization = request.getHeader(AuthConstants.TOKEN_HEADER);
        log.info("YaTokenFilter ya-auth-token: {}", authorization);

        // 白名单
        if (WHITE_LIST.contains(request.getServletPath())) {
            chain.doFilter(request, response);
            return;
        }

        // 1.请求头中没有携带token
        if (StrUtil.isBlank(authorization)) {
            ServletUtils.renderResult(response, new BaseResult<>(ResultEnum.FAILED_UNAUTHORIZED));
            return;
        }

        // 携带token
        final String token = authorization.replace(AuthConstants.TOKEN_PREFIX, "");
        String userId;
        // 2.提供的token异常
        try {
            userId =  JwtUtils.extractUserId(token);
        }catch (Exception e){
            log.error("YaTokenFilter doFilterInternal 解析jwt异常:{}", e.toString());
            ServletUtils.renderResult(response, new BaseResult<>(ResultEnum.FAILED_UNAUTHORIZED.code, "凭证异常"));
            return;
        }

        String redisToken = redisUtils.getString(AuthConstants.REDIS_KEY_AUTH_TOKEN + userId);
        // 3.token过期
        if(StrUtil.isBlank(redisToken)){
            ServletUtils.renderResult(response, new BaseResult<>(ResultEnum.FAILED_UNAUTHORIZED.code, "登录已过期,请重新登录过期。"));
            return;
        }

        // 4.提供的token是合法的,但是redis中的token又被使用登录功能重新刷新了一下,导致不一致。
        if(!Objects.equals(redisToken, token)){
            ServletUtils.renderResult(response, new BaseResult<>(ResultEnum.FAILED_UNAUTHORIZED.code, "账号在别处登陆。"));
            return;
        }
        // token续期
        redisUtils.set(REDIS_KEY_AUTH_TOKEN + userId, token, expiration);
        // 获取用户信息和权限
        String userDetailStr =  redisUtils.getString(AuthConstants.REDIS_KEY_AUTH_USER_DETAIL + userId);
        UserDetails userDetails;
        if(Objects.isNull(userDetailStr)){
            userDetails = yaUserDetailService().loadUserByUsername(userId);
            redisUtils.set(REDIS_KEY_AUTH_USER_DETAIL + userId, JSON.toJSONString(userDetails), 1, TimeUnit.DAYS);
        }else{
            userDetails = initUser(userDetailStr);
        }
        SecurityContextHolder.getContext().setAuthentication(
                new UsernamePasswordAuthenticationToken(
                        userDetails, userDetails.getPassword(), userDetails.getAuthorities()
                )
        );
        log.info("YaTokenFilter doFilterInternal end");
        chain.doFilter(request, response);
    }

    private YaUserDetailService yaUserDetailService(){
        return SpringContextUtils.getBean(YaUserDetailService.class);
    }

    private User initUser(String userJsonStr){
        JSONObject userJson = JSON.parseObject(userJsonStr);

        String userId = userJson.getString("username");
        JSONArray authArray = userJson.getJSONArray("authorities");

        List<GrantedAuthority> authorities = new ArrayList<>(authArray.size());
        for(int i=0; i< authArray.size();i++){
            JSONObject authObj = authArray.getJSONObject(i);
            authorities.add(new SimpleGrantedAuthority(authObj.getString("authority")));
        }
        return new User(userId, "[PROTECTED]", authorities);
    }

}

UserAuthUtils

已经登录的用户,可以从Security的上下文中获取用户的账号、基本信息、权限等。可以将其封装为工具类。因为练手的用户表较为简单,也没有部分、员工、角色、权限等概念,因此仅封装了getUserId做抛砖引玉的作用。可以根据实际使用自己封装更多的方法。

getUserId

public static String getUserId() {
        if (Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
            return null;
        }
        UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (Objects.isNull(userDetails)) {
            return null;
        }
        return userDetails.getUsername();
    }

用户登出

JWT本身是无状态的,但是我们后端将jwt存到redis里,相当于手动使JWT变得有状态了。那么我们在登出时就需要清空Redis中的jwt。

实现LogoutSuccessHandler

/**
 * <p>
 *  登出成功
 * </p>
 *
 * @author Ya Shi
 * @since 2024/3/28 10:47
 */
@Slf4j
public class YaLogoutSuccessHandler implements LogoutSuccessHandler {
    private final RedisUtils redisUtils;

    public YaLogoutSuccessHandler(RedisUtils redisUtils) {
        this.redisUtils = redisUtils;
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        final String authorization = request.getHeader(AuthConstants.TOKEN_HEADER);
        // 1.请求头中没有携带token
        if (StrUtil.isBlank(authorization)) {
            ServletUtils.renderResult(response, BaseResult.successWithMessage("没有登录信息,无需退出"));
            return;
        }

        // 携带token
        final String token = authorization.replace(AuthConstants.TOKEN_PREFIX, "");
        String userId;
        // 2.提供的token异常
        try {
            userId =  JwtUtils.extractUserId(token);
        }catch (Exception e){
            log.error("YaLogoutHandler logout 解析jwt异常:{}", e.toString());
            ServletUtils.renderResult(response, new BaseResult<>(ResultEnum.FAILED_UNAUTHORIZED.code, "凭证异常"));
            return;
        }
        // 清空Redis
        redisUtils.delete(REDIS_KEY_AUTH_TOKEN + userId);
        log.info("YaLogoutSuccessHandler onLogoutSuccess");
        ServletUtils.renderResult(response, BaseResult.successWithMessage("退出登录成功"));
    }
}

修改Security配置 : YaSecurityConfig

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	http  ... // 前面的配置忽略
	.logout().logoutUrl("/auth/logout").logoutSuccessHandler(new YaLogoutSuccessHandler(redisUtils));
	return http.build();
}

下一步的计划

  • 用户鉴权
  • 排查permitAll()失效的问题。
  • 做一个练手用的用户中心,提供统一的注册、登录、认证、鉴权服务,供其他的应用调用。
  • 把前期已经实现的基础的配置和工具类封装为jar包,供以后的程序使用。

参考文章

  • SpringBoot3.0 + SpringSecurity6.0+JWT
  • SpringSecurity (3) SpringBoot + JWT 实现身份认证和权限验证
  • Spring Security 6.0(spring boot 3.0) 下认证配置流程
  • SpringSecurity的PermitAll、WebSecurityCustomizer和授权
  • springsecurity的http.permitall与web.ignoring的区别

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

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

相关文章

Vidmore Video Fix for Mac 视频修复工具

Vidmore Video Fix for Mac是一款功能强大且易于使用的视频修复工具&#xff0c;专为Mac用户设计。它凭借先进的视频修复技术&#xff0c;能够帮助用户解决各种视频问题&#xff0c;如视频文件损坏、无法播放、格式不支持等。 软件下载&#xff1a;Vidmore Video Fix for Mac v…

DevSecOps平台架构系列-微软云Azure DevSecOps平台架构

目录 一、概述 二、Azure DevOps和黄金管道 2.1 概述 2.2 Azure DevOps架构说明 2.2.1 架构及管道流程图 2.2.2 架构内容 2.2.2.1 Azure Boards 2.2.2.2 Azure Repos 2.2.2.3 Azure Test Plans 2.2.2.4 Azure Pipelines 2.2.2.5 Azure Application Insights 2.2.2.6…

5、双亲委派机制

双亲委派机制指的是&#xff1a;当一个类加载器接收到加载类的任务时&#xff0c;会自底向上查找是否加载过&#xff0c; 再由顶向下进行加载。 详细流程&#xff1a; 每个类加载器都有一个父类加载器。父类加载器的关系如下&#xff0c;启动类加载器没有父类加载器&#xff1…

MySql实战--普通索引和唯一索引,应该怎么选择

在前面的基础篇文章中&#xff0c;我给你介绍过索引的基本概念&#xff0c;相信你已经了解了唯一索引和普通索引的区别。今天我们就继续来谈谈&#xff0c;在不同的业务场景下&#xff0c;应该选择普通索引&#xff0c;还是唯一索引&#xff1f; 假设你在维护一个市民系统&…

网络编程--高并发服务器(二)

这里写目录标题 线程池高并发服务器UDP服务器TCP与UDP机制的对比TCP与UDP优缺点比较UDP的C/S模型实现思路模型分析实现思路&#xff08;对照TCP的C/S模型&#xff09; 二级目录 一级目录二级目录二级目录二级目录 一级目录二级目录二级目录二级目录 一级目录二级目录二级目录二…

Siemens S7-1500TCPU 运动机构系统功能简介

目录 引言&#xff1a; 1.0 术语定义 2.0 基本知识 2.1 运动系统工艺对象 2.2 坐标系与标架 3.0 运动机构系统类型 3.1 直角坐标型 3.2 轮腿型 3.3 平面关节型 3.4 关节型 3.5 并联型 3.6 圆柱坐标型 3.7 三轴型 4.0 运动系统的运动 4.1 运动类型 4.1.1 线性运动…

一阶低通滤波器特性对比

分析y[n]qx[n](1-q)y[n-1] 和 1/&#xff08;Ts1&#xff09; 两款常用滤波器的区别 代码下载链接&#xff1a; https://download.csdn.net/download/RNG_uzi_/89048367

C波段卫星与5G的干扰排查及解决方案

作者介绍 一、方案背景 目前造成C波段卫星信号受5G信号干扰有以下几个原因&#xff1a; ●C波段&#xff08;3.4-4.2GHz&#xff09;和电信5G频段&#xff08;3.4-3.7GHz&#xff09;间存在频谱重叠。 ●地面终端接收到的卫星信号通常比蜂窝信号弱几个数量级&#xff0c;同频…

Vue 03 组件通信

Vue学习 Vue 0301 浏览器本地存储localStorageSessionStorage案例 todolist的完善 02 组件自定义事件Custom Events基本使用解绑自定义事件注意事项①② 总结案例 todolist的完善 03 全局事件总线GlobalEventBus案例 todolist的完善 04 消息的订阅与发布案例 todolist的完善 05…

go的通信Channel

go的通道channel是用于协程之间数据通信的一种方式 一、channel的结构 go源码&#xff1a;GitHub - golang/go: The Go programming language src/runtime/chan.go type hchan struct {qcount uint // total data in the queue 队列中当前元素计数&#xff0c;…

设计模式-设配器模式

目录 &#x1f38a;1.适配器模式介绍 &#x1f383;2.适配器类型 &#x1f38f;3.接口适配器 &#x1f390;4.类的适配器 &#x1f38e;5.优缺点 1.适配器模式介绍 适配器模式&#xff08;Adapter Pattern&#xff09;是作为两个不兼容的接口之间的桥梁。这种类型的设…

进阶了解C++(6)——二叉树OJ题

Leetcode.606.根据二叉树创建字符串&#xff1a; 606. 根据二叉树创建字符串 - 力扣&#xff08;LeetCode&#xff09; 难度不大&#xff0c;根据题目的描述&#xff0c;首先对二叉树进行一次前序遍历&#xff0c;即&#xff1a; class Solution { public:string tree2str(Tr…

【管理咨询宝藏59】某大型汽车物流战略咨询报告

本报告首发于公号“管理咨询宝藏”&#xff0c;如需阅读完整版报告内容&#xff0c;请查阅公号“管理咨询宝藏”。 【管理咨询宝藏59】某大型汽车物流战略咨询报告 【格式】PDF 【关键词】HR调研、商业分析、管理咨询 【核心观点】 - 重新评估和调整商业模式&#xff0c;开拓…

代码随想录——移除元素(Leetcode27)

题目链接 暴力&#xff1a;&#xff08;没有改变元素相对位置&#xff09; class Solution {public int removeElement(int[] nums, int val) {int len nums.length;for(int i 0; i < len; i){if(nums[i] val){for(int j i 1; j < len; j){nums[j-1] nums[j];}i…

ESCTF-密码赛题WP

*小学生的爱情* Base64解码获得flag *中学生的爱情* 社会主义核心价值观在线解码得到flag http://www.atoolbox.net/Tool.php?Id850 *高中生的爱情* U2FsdG开头为rabbit密码,又提示你密钥为love。本地toolfx密码工具箱解密。不知道为什么在线解密不行。 *大学生的爱情* …

LLM - 大语言模型的指令微调(Instruction Tuning) 概述

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://blog.csdn.net/caroline_wendy/article/details/137009993 大语言模型的指令微调(Instruction Tuning)是一种优化技术&#xff0c;通过在特定的数据集上进一步训练大型语言模型(LLMs)&a…

STM32 PWM通过RC低通滤波转双极性SPWM测试

STM32 PWM通过RC低通滤波转双极性SPWM测试 &#x1f4cd;参考内容《利用是stm32cubemx实现双极性spwm调制 基于stm32f407vet6》&#x1f4fa;相关视频链接&#xff1a;https://www.bilibili.com/video/BV16S4y147hB/?spm_id_from333.788 双极性SPWM调制讲解以及基于stm32的代码…

线程的状态:操作系统层面和JVM层面

在操作系统层面&#xff0c;线程有五种状态 初始状态&#xff1a;线程被创建&#xff0c;操作系统为其分配资源。 可运行状态(就绪状态)&#xff1a;线程被创建完成&#xff0c;进入就绪队列&#xff0c;参与CPU执行权的争夺。或因为一些原因&#xff0c;从阻塞状态唤醒的线程…

Deno 1.42:使用 JSR 更好地进行依赖管理

3 月 28 日&#xff0c;Deno 宣布 1.42 版本正式推出。Deno 的愿景是简化编程&#xff0c;其中一个重要方面就是管理依赖关系。虽然 npm 已发展成为最成功的开源注册表&#xff0c;但使用和发布模块却变得越来越复杂。 基于 npm 的成功&#xff0c;JSR 提供​​了一个现代化的…

如何使用PMKIDCracker对包含PMKID值的WPA2密码执行安全测试

关于PMKIDCracker PMKIDCracker是一款针对无线网络WPA2密码的安全审计与破解测试工具&#xff0c;该工具可以在不需要客户端或去身份验证的情况下对包含了PMKID值的WPA2无线密码执行安全审计与破解测试。 PMKIDCracker基于纯Python 3开发&#xff0c;旨在帮助广大安全研究人员…