课程项目设计--spring security--认证管理功能--宿舍管理系统--springboot后端

news2025/1/13 13:38:17
写在前面:
还要实习,每次时间好少呀,进度会比较慢一点
本文主要实现是用户管理相关功能。
前文项目建立

文章目录

  • 验证码功能
    • 验证码配置
    • 验证码生成工具类
    • 添加依赖
    • 功能测试
    • 编写controller接口
    • 启动项目
  • security配置
    • 拦截器配置
      • 验证码拦截器
    • jwt拦截器
    • 思考
  • 用户登录
    • jwt管理
    • 验证
  • 用户注销
  • 流程小结
    • 验证码
    • jwt令牌管理
    • 登录
    • 注销

验证码功能

验证码采用的是hutool工具的验证码
hutool官方地址

工具模板采用有来开源组织

验证码配置

yml配置

CaptchaConfig:
  #  验证码缓存过期时间(单位:秒)
  ttl: 120l
  # 验证码内容长度
  length: 4
  # 验证码宽度
  width: 120
  # 验证码高度
  height: 40
  # 验证码字体
  font-name: Verdana
  # 验证码字体大小
  fontSize: 20

配置类

/**
 * EasyCaptcha 配置类
 * 
 * @author haoxr
 * @since 2023/03/24
 */
@ConfigurationProperties(prefix = "easy-captcha")
@Configuration
@Data
public class CaptchaConfig {

    // 验证码类型
    private CaptchaTypeEnum type = CaptchaTypeEnum.ARITHMETIC;

    // 验证码缓存过期时间(单位:秒)
    @Value("${captcha.ttl}")
    private long ttl;

    // 内容长度
    @Value("${captcha.length}")
    private int length;
    // 宽度
    @Value("${captcha.width}")
    private int width;
    // 验证码高度
    @Value("${captcha.height}")
    private int height;

    // 验证码字体
    @Value("${captcha.font-name}")
    private String fontName;

    // 字体风格
    private Integer fontStyle = Font.PLAIN;

    // 字体大小
    @Value("${captcha.font-size}")
    private int fontSize;

}

验证码生成工具类

@Component
@RequiredArgsConstructor
public class EasyCaptchaProducer {
    private final CaptchaConfig captchaConfig;

    public Captcha getCaptcha() {
        Captcha captcha;
        int width = captchaConfig.getWidth();
        int height = captchaConfig.getHeight();
        int length = captchaConfig.getLength();
        String fontName = captchaConfig.getFontName();

        switch (captchaConfig.getType()) {
            case ARITHMETIC -> {
                captcha = new ArithmeticCaptcha(width, height);
                captcha.setLen(2);
            }
            case CHINESE -> {
                captcha = new ChineseCaptcha(width, height);
                captcha.setLen(length);
            }
            case CHINESE_GIF -> {
                captcha = new ChineseGifCaptcha(width, height);
                captcha.setLen(length);
            }
            case GIF -> {
                captcha = new GifCaptcha(width, height);//最后一位是位数
                captcha.setLen(length);
            }
            case SPEC -> {
                captcha = new SpecCaptcha(width, height);
                captcha.setLen(length);
            }
            default -> throw new RuntimeException("验证码配置信息错误!正确配置查看 CaptchaTypeEnum ");
        }
        captcha.setFont(new Font(fontName, captchaConfig.getFontStyle(), captchaConfig.getFontSize()));
        return captcha;
    }


}

添加依赖

        <!-- Java8 之后JavaScript引擎nashorn被移除导致验证码解析报错-->
        <dependency>
            <groupId>org.openjdk.nashorn</groupId>
            <artifactId>nashorn-core</artifactId>
            <version>${nashorn.version}</version>
        </dependency>

功能测试

        Captcha captcha = easyCaptchaProducer.getCaptcha();
        try (OutputStream ops = new FileOutputStream("d://captcha.jpg")) {
            captcha.out(ops);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(captcha.text());

测试结果
在这里插入图片描述
在这里插入图片描述

编写controller接口

@Tag(name = "01-认证中心")
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthController {

    private final EasyCaptchaService easyCaptchaService;

    @Operation(summary = "获取验证码")
    @GetMapping("/captcha")
    public Result<CaptchaResult> getCaptcha() {
        CaptchaResult captcha = easyCaptchaService.getCaptcha();
        return Result.success(captcha);
    }
}

启动项目

记住这里,这是你spring security 的密码
在这里插入图片描述

生成http

通过base64转图片的在线工具可以看到
在这里插入图片描述
说明编写成功了。

security配置

在上面我们默认的是spring security 自动的密码。我们现在需要自己设置密码。

spring security 框架捏,不太好说这玩意。挺忘记了。
不过spring boot3使用的是spring security6.0版本和以前的有很大差别,6.0通过配置bean来进行。所以也还好,反正都是从头学。
首先需要配置security的配置类

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {


    // 密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    /**
     * 不走过滤器链的放行配置
     * 默认放行静态资源、登录接口、验证码接口、Swagger接口文档
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring()
                .requestMatchers(
                        "/auth/captcha",
                        "/webjars/**",
                        "/doc.html",
                        "/swagger-resources/**",
                        "/swagger-ui/**",
                        "/ws/**"
                );
    }
}
    /**
     * 认证管理器
     *
     * @param authenticationConfiguration 认证配置
     * @return 认证管理器
     * @throws Exception 异常
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(requestMatcherRegistry ->
                        requestMatcherRegistry.requestMatchers(SecurityConstants.LOGIN_PATH).permitAll()
                                .anyRequest().authenticated())
                .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
                        httpSecurityExceptionHandlingConfigurer
                                .authenticationEntryPoint(authenticationEntryPoint)
                                .accessDeniedHandler(accessDeniedHandler))
                .csrf(AbstractHttpConfigurer::disable);

        // 验证码校验过滤器
        http.addFilterBefore(new VerifyCodeFilter(), UsernamePasswordAuthenticationFilter.class);
        // JWT 校验过滤器
        http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenManager), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

这里还用到了2个拦截器

拦截器配置

验证码拦截器

需求:对登录请求进行拦截,如果是登录则需要先校验验证码是否正常,如果正确则放行。其他请求则直接放行。

public class VerifyCodeFilter extends OncePerRequestFilter {
    private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, "POST");

    public static final String VERIFY_CODE_PARAM_KEY = "verifyCode";
    public static final String VERIFY_CODE_KEY_PARAM_KEY = "verifyCodeKey";
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 如果是登录请求则校验验证码
        if (LOGIN_PATH_REQUEST_MATCHER.matches(request)){
            String code = request.getParameter(VERIFY_CODE_PARAM_KEY);
            String verifyCodeKey = request.getParameter(VERIFY_CODE_KEY_PARAM_KEY);

            // 由于这个不是bean,不能通过注入的方式获取,所以通过SpringUtil工具类获取
            RedisTemplate redisTemplate = SpringUtil.getBean("redisTemplate", RedisTemplate.class);
            String cacheCode =  Convert.toStr(redisTemplate.opsForValue().get(SecurityConstants.VERIFY_CODE_CACHE_PREFIX + verifyCodeKey));
            if (cacheCode == null) {
                // 验证码过期
                ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_TIMEOUT);
                return;
            }
            if (!StrUtil.equals(cacheCode,code)) {
                // 验证码错误
                ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_ERROR);
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
}

jwt拦截器

需求:处理登录请求以外的请求,每次需要验证jwt令牌,如果没问题则在该线程请求附加权限身份。

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, "POST");

    private final JwtTokenManager tokenManager;
    public JwtAuthenticationFilter(JwtTokenManager jwtTokenManager) {
        this.tokenManager = jwtTokenManager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        if (!LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
            String jwt = RequestUtils.resolveToken(request);
            if (StringUtils.hasText(jwt) && SecurityContextHolder.getContext().getAuthentication() == null) {
                try {
                    Claims claims = this.tokenManager.parseAndValidateToken(jwt);
                    Authentication authentication = this.tokenManager.getAuthentication(claims);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                } catch (Exception e) {
                    ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
                }
            } else {
                ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
            }
        }
        chain.doFilter(request, response);
    }
}

思考

这2个拦截器一个需要登录一个除去登录,那么是不是可以放到一个拦截器里面去。各走各的。这样也明确一点。也不用2个拦截器找了。

如果改了记得改securityFilterChain

用户登录

需求:输入用户名和密码,验证用户身份。
需要写一个类继承UserDetails
在这里插入图片描述

另一个实现类继承SysUserService(SysUserDetailsService)
在这里插入图片描述
这2个一个是存储一个是查询。然后会自动和输入的username以及password进行比对
验证流程后面总结一个spring security的文。

SysUserDetailsService作用是查询该用户名的角色信息并返回UserDetails。

查询,调用SysUserService根据用户名查询所有的
在这里插入图片描述
由于认证信息需要角色信息和权限所以我们需要联表查询角色信息。
在依据角色信息查询权限。

        select u.id userId,
               u.name username,
               u.password,
               u.role,
               u.avatar,
               u.email,
               u.status,
               r.code
        from sys_user u
                 left join sys_user_role sur on u.id = sur.user_id
                 left join sys_role r on sur.role_id = r.id
        where u.name = #{username}
          AND u.deleted = 0

然后在依据角色查询权限
不过我感觉这个type硬编码挺严重的,也算学习一下这种mybatis里面枚举了。
如果没用角色则m.id = -1让其没权限。

<select id="listRolePerms" resultType="java.lang.String">
        select distinct m.perm
        from sys_menu m
        inner join sys_role_menu rm on m.id = rm.menu_id
        inner join sys_role r on r.id = rm.role_id
        where m.type = '${@com.yu.common.enums.MenuTypeEnum@BUTTON.getValue()}'
        and m.perm is not null
        <choose>
            <when test="roles!=null and roles.size()>0">
                and r.code in
                <foreach collection="roles" item="role" open="(" close=")" separator=",">
                    #{role}
                </foreach>
            </when>
            <otherwise>
                and m.id = -1
            </otherwise>
        </choose>
    </select>

controller验证,很明确的流程就是封装输入的,然后进行验证。失败了会报错返回。
成功则生成token将权限放入redis,将角色,用户名,id封装进jwt
然后进行返回。接下来查看jwtTokenManager.createToken

    @Operation(summary = "登录")
    @PostMapping("/login")
    public Result<LoginResult> login(
            @Parameter(description = "用户名", example = "admin") @RequestParam String username,
            @Parameter(description = "密码", example = "123456") @RequestParam String password
    ) {
        // 存储username和password
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                username.toLowerCase().trim(),
                password
        );
        // 验证用户名和密码
        Authentication authentication = authenticationManager.authenticate(authenticationToken);
        // 生成token
        String accessToken = jwtTokenManager.createToken(authentication);
        // 返回token
        LoginResult loginResult = LoginResult.builder()
                .tokenType("Bearer")
                .accessToken(accessToken)
                .build();
        return Result.success(loginResult);
    }
    @Schema(description ="登录响应对象")
    @Builder
    public static record LoginResult(
            @Schema(description = "访问token")
            String accessToken,

            @Schema(description = "token 类型",example = "Bearer")
            String tokenType,

            @Schema(description = "刷新token")
            String refreshToken,

            @Schema(description = "过期时间(单位:毫秒)")
            Long expires
    ) {
    }

jwt管理

采用hutool工具包进行jwt管理,以前用过java-jwt的,这次试试hutool。

    /**
     * 创建token
     *
     * @param authentication auth info
     * @return token
     */
    public String createToken(Authentication authentication) {
        SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();

        // 角色放入JWT的claims
        Set<String> roles = userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority).collect(Collectors.toSet());

        // 权限数据多放入Redis
        Set<String> perms = userDetails.getPerms();
        redisTemplate.opsForValue().set(SecurityConstants.USER_PERMS_CACHE_PREFIX + userDetails.getUserId(), perms);

        Map<String, Object> claims = Map.of(
                JWTPayload.ISSUED_AT, DateTime.now(),
                JWTPayload.EXPIRES_AT, DateTime.now().offset(DateField.SECOND, tokenTtl),
                "jti", IdUtil.fastSimpleUUID(),
                "userId", userDetails.getUserId(),
                "username", userDetails.getUsername(),
                "authorities", roles);

        return JWTUtil.createToken(claims, getSecretKeyBytes());
    }

验证

http测试:
之前测试挺头疼的。
需要先发送验证码的。
然后去base64转图片(后面直接打印了结果了)
进行测试
在这里插入图片描述
成功
在这里插入图片描述
后面去vue3前端测了。用的是有来开源vue3-element-admin修改。
成功了!
在这里插入图片描述

用户注销

从jwt中获取我们设置的jti唯一表示
然后需要将redis中的删除就可以了

    @Operation(summary = "注销", security = {@SecurityRequirement(name = SecurityConstants.TOKEN_KEY)})
    @DeleteMapping("/logout")
    public Result<String> logout(HttpServletRequest request) {
        String token = RequestUtils.resolveToken(request);
        if (StrUtil.isNotBlank(token)) {
            Claims claims = jwtTokenManager.getTokenClaims(token);
            String jti = StrUtil.toString(claims.getClaim("jti"));

            Date expiration = jwtTokenManager.getExpiration(claims);
            if (expiration != null) {
                // 有过期时间,在token有效时间内存入黑名单,超出时间移除黑名单节省内存占用
                long ttl = (expiration.getTime() - System.currentTimeMillis());
                redisTemplate.opsForValue().set(SecurityConstants.BLACK_TOKEN_CACHE_PREFIX + jti, null, ttl, TimeUnit.MILLISECONDS);
            } else {
                // 无过期时间,永久加入黑名单
                redisTemplate.opsForValue().set(SecurityConstants.BLACK_TOKEN_CACHE_PREFIX + jti, null);
            }
        }
        SecurityContextHolder.clearContext();
        return Result.success("注销成功");
    }

流程小结

验证码

获取随机验证码

  • 验证码接口放行,无视security
  • 存放redis用,key = SecurityConstants.VERIFY_CODE_CACHE_PREFIX +verifyCodeKey(生成)

验证验证码

  • 拦截登录请求
  • 查询redis
    • 如果null,则过期
    • 如果错误,则返回
    • 正确放行

jwt令牌管理

  • 拦截所有除了登录的请求
  • 从jwt中解析获取Authentication
  • 放入线程中

登录

  • 框架校验

    • 获取认证信息,依据user和role表获取角色基本信息和角色
    • 依据角色获取权限
    • Authentication存放id,用户名,密码,是否启用,权限,角色,数据权限
  • 依据Authentication生成jwt

    • 存放jti随机id,userid,用户名,角色信息,权限数据
    • 过期时间5小时

注销

  • 拉黑jwt的jti

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

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

相关文章

台积电美国厂施工现场混乱,真令人头痛 | 百能云芯

近日&#xff0c;英伟达公司的财报表现异常亮眼&#xff0c;摩根士丹利不仅点名了台积电成为最大的受益者&#xff0c;还预测每售出一颗H100英伟达芯片&#xff0c;台积电就能获得900美元的利润。然而&#xff0c;美国媒体却曝出了一则不利的消息&#xff0c;称美国亚利桑那州的…

Hbase文档--架构体系

阿丹&#xff1a; 基础概念了解之后了解目标知识的架构体系&#xff0c;就能事半功倍。 架构体系 关键组件介绍&#xff1a; HBase – Hadoop Database&#xff0c;是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统&#xff0c;利用HBase技术可在廉价PC Server上搭建起…

vue3 vite使用 monaco-editor 报错

报错&#xff1a;Unexpected usage at EditorSimpleWorker.loadForeignModule 修改配置&#xff1a; "monaco-editor-webpack-plugin": "^4.2.0",删除不用 版本&#xff1a; "monaco-editor": "^0.28.1", 修改如下&#xff1a; opti…

Python“牵手”虾皮(Shopee)商品详情API接口运用场景及功能介绍,虾皮API接口申请指南

虾皮&#xff08;Shopee&#xff09;电商API接口是针对虾皮提供的电商服务平台&#xff0c;为开发人员提供了简单、可靠的技术来与虾皮电商平台进行数据交互&#xff0c;实现一系列开发、管理和营销等操作。其中包括商品详情API接口&#xff0c;通过这个API接口商家可以获取商品…

SLS日志解析配置

分隔符模式 INFO|2023-04-10T11:05:30.12808:00|X.X.X.X|ACCESS_ALLOWED|1 模式&#xff1a;分隔符模式 日志样例&#xff1a;贴文档说明中的样例&#xff0c;或者直接在SLS历史日志里找一行 分隔符&#xff1a;竖线 日志抽取内容Key用文档中说明的变量名 是否接受部分字段&am…

CMIP6中的模式比较计划、CMIP6数据下载、单点降尺度、统计方法的区域降尺度、基于WRF模式的动力降尺度及气候变化、生态、水文等典型案例

CMIP6数据被广泛应用于全球和地区的气候变化研究、极端天气和气候事件研究、气候变化影响和风险评估、气候变化的不确定性研究、气候反馈和敏感性研究以及气候政策和决策支持等多个领域。这些数据为我们理解和预测气候变化&#xff0c;评估气候变化的影响和风险&#xff0c;以及…

Docker容器与虚拟化技术:Harbor私有仓库部署与迁移

目录 一、理论 1.本地私有仓库 2.Harbor 二、实验 1.Docker搭建本地私有仓库 2.docker-compose部署及配置 3.harbor部署及配置 4.登录创建项目 5.在其他客户端上传镜像 6. harbor维护 7.移除 Harbor 服务容器同时保留镜像数据/数据库&#xff0c;并进行迁移 三、问题…

把Android手机变成电脑摄像头

一、使用 DroidCam 使用 DroidCam&#xff0c;你可以将手机作为电脑摄像头和麦克风。一则省钱&#xff0c;二则可以在紧急情况下使用&#xff0c;比如要在电脑端参加一个紧急会议&#xff0c;但电脑却没有摄像头和麦克风。 DroidCam 的安卓端分为免费的 DroidCam 版和收费的 …

服务器数据恢复-服务器RAID6硬盘故障离线的数据恢复案例

服务器数据恢复环境&#xff1a; 服务器中有一组由6块磁盘组建的RAID6磁盘阵列。服务器作为WEB服务器使用&#xff0c;上面运行了MYSQL数据库以及存放了网站代码和其他数据文件。 服务器故障&#xff1a; 在服务器运行过程中该raid6阵列中有两块磁盘先后离线&#xff0c;但是管…

火山引擎DataLeap基于Apache Atlas自研异步消息处理框架

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 字节数据中台DataLeap的Data Catalog系统通过接收MQ中的近实时消息来同步部分元数据。Apache Atlas对于实时消息的消费处理不满足性能要求&#xff0c;内部使用Flin…

Mysql索引、事务与存储引擎 (索引)

一、索引 1、索引的概念&#xff1a; 索引就是一种帮助系统能够更加快速的查找信息的数据结构。 2.索引的作用&#xff1a; ①数据库利用各种快速定位技术&#xff0c;能够大大加快查询速度&#xff0c;这是创建索引的最主要的原因。 ②当表很大或查询涉及到多个表时&#xff0…

Linux安装软件每次靠百度,这次花了些时间,终于算是搞明白了

Linux下安装命令虽然经常使用&#xff0c;但也仅仅是会使用&#xff0c;每次再用时依然的百度 。于是就花了些时间整体的梳理了一番&#xff0c;以便于更好的理解。 1.安装流程介绍 在Linux下安装软件&#xff0c;其实也是遵循着和Windows一样的安装流程。 首先&#xff0c;…

巨人互动|Facebook海外户Facebook游戏全球发布实用策略

Facebook是全球最大的社交媒体平台之一&#xff0c;拥有庞大的用户基数和广阔的市场。对于游戏开发商而言&#xff0c;利用Facebook进行全球发布是一项重要的策略。下面小编将介绍一些实用的策略帮助开发商在Facebook上进行游戏全球发布。 巨人互动|Facebook海外户&Faceboo…

淘宝API技术解析,实现按图搜索淘宝商品

淘宝提供了开放平台接口&#xff08;API&#xff09;来实现按图搜索淘宝商品的功能。您可以通过以下步骤来实现&#xff1a; 1. 获取开放平台的访问权限&#xff1a;首先&#xff0c;您需要在淘宝开放平台创建一个应用&#xff0c;获取访问淘宝API的权限。具体的申请步骤和要求…

1.6 服务器处理客户端请求

客户端进程向服务器进程发送一段文本&#xff08;MySQL语句&#xff09;&#xff0c;服务器进程处理后再向客户端进程发送一段文本&#xff08;处理结果&#xff09;。 从图中我们可以看出&#xff0c;服务器程序处理来自客户端的查询请求大致需要经过三个部分&#xff0c;分别…

前端需要理解的 React 知识

1 框架通识 1.1 MVVM、MVC和MVP MVC、MVP 和 MVVM 是三种常见的软件架构设计模式。主要通过分离关注点的方式来组织代码结构&#xff0c;优化开发效率。 MVC将应用抽象为数据层&#xff08;Model&#xff09;、视图层&#xff08;View&#xff09;、逻辑层&#xff08;contr…

解锁Selenium的力量:不仅仅是Web测试

Selenium简介 Selenium&#xff0c;作为Web应用测试的领军者&#xff0c;已经成为了无数开发者和测试人员的首选工具。它不仅仅是一个自动化测试工具&#xff0c;更是一个强大的Web应用交互框架。 Selenium的起源与发展 Selenium的历史可以追溯到2004年&#xff0c;由Jason Hu…

二叉树、红黑树、B树、B+树

二叉树 一棵二叉树是结点的一个有限集合&#xff0c;该集合或者为空&#xff0c;或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。 二叉树的特点&#xff1a; 每个结点最多有两棵子树&#xff0c;即二叉树不存在度大于2的结点。二叉树的子树有左右之分&#xf…

【算法刷题之哈希表(2)】

目录 1.leetcode-454. 四数相加 II2.leetcode-383. 赎金信&#xff08;1&#xff09;暴力解法&#xff08;2&#xff09;哈希法 3.leetcode-205. 同构字符串&#xff08;1&#xff09;哈希法&#xff08;2&#xff09;直接对比查找 4.leetcode-128. 最长连续序列5.总结 1.leetc…

抖音小程序商城开发制作源码 含多套模板+部署搭建教程

分享一个抖音小程序商城的制作源码&#xff0c;含多套模板、模块化自由DIY功能和完整的搭建部署教程。程序支持除抖音小程序商城制作外&#xff0c;还支持一键同步微信、支付宝、百度、今日头条端小程序。 抖音小程序商城的基本架构包括前端页面和后端管理平台两部分。前端页面…