【前后端分离博客】学习笔记01 --- 登录模块Sa-Token

news2024/11/16 18:06:31

前言

用于记录自己学习博客项目的流程

基于Springboot + Vue3 开发的前后端分离博客

项目源码:Blog: 基于SpringBoot + Vue3 + TypeScript + Vite的个人博客,MySQL数据库,Redis缓存,ElasticSearch全文搜索,支持QQ、Gitee、Github第三方登录,留言、友链、评论、说说、相册等功能。

 

 

一、概述

该项目使用sa-Token框架实现的登录,权限认证

Sa-Token,一个轻量级 Java 权限认证框架,让鉴权变得简单、优雅

官方文档:https://sa-token.cc

 

 

二、登录流程

点击登录,前端发送请求

 

 

三、后端代码实现 

3.1、添加依赖

引入Sa-Token依赖

        <!-- Sa-Token -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot-starter</artifactId>
            <version>${saToken.version}</version>
        </dependency>
        <!-- 
            Sa-Token 整合 Redis (使用 jackson 序列化方式) 
            优点:Session 序列化后可读性强,可灵活手动修改,缺点:兼容性稍差。
        -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-dao-redis-jackson</artifactId>
            <version>${saToken.version}</version>
        </dependency>
        <!-- Redis连接池 必须为项目提供一个 Redis 实例化方案-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

yml文件中配置信息

# Sa-Token 配置 (文档: https://sa-token.cc)
sa-token:
  # token名称 (同时也是cookie名称)
  token-name: Authorization
  # token前缀
  token-prefix: Bearer
  # token有效期,单位s 默认30天, -1代表永不过期
  timeout: 43200
  # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
  activity-timeout: -1
  # 关闭自动续签
  auto-renew: false
  # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
  is-concurrent: false
  # token风格
  token-style: uuid
  # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
  is-share: false
  # 是否从cookie中读取token
  is-read-cookie: false
  # 是否从请求体里读取token
  is-read-body: false
  # 是否从header中读取token
  is-read-header: true
  # 是否输出操作日志
  is-log: false

spring:
  # redis配置
  redis:
    # Redis服务器地址
    host: localhost
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    # password: redis密码
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池最大连接数
        max-active: 150
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: 5000ms
        # 连接池中的最大空闲连接
        max-idle: 100
        # 连接池中的最小空闲连接
        min-idle: 50

Sa-Token 默认将数据保存在内存中,此模式读写速度最快,且避免了序列化与反序列化带来的性能消耗,但是此模式也有一些缺点,比如:

  1. 重启后数据会丢失。
  2. 无法在分布式环境中共享数据。

为此,Sa-Token 提供了扩展接口,你可以轻松将会话数据存储在 RedisMemcached等专业的缓存中间件中, 做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性。

集成 Redis 后,是我额外手动保存数据,还是框架自动保存?

  • 框架自动保存。集成 Redis 只需要引入对应的 pom依赖 即可,框架所有上层 API 保持不变。

3.2、sa-Token配置类

全局侦听器


Sa-Token 提供一种侦听器机制,通过注册侦听器,你可以订阅框架的一些关键性事件,例如:用户登录、退出、被踢下线等。

 

事件触发流程大致如下:

要注册自定义的侦听器也非常简单:

  1. 新建类实现 SaTokenListener 接口。
  2. 将实现类注册到 SaTokenEventCenter 事件发布中心。

该类实现功能:

  • 获取登录用户的浏览器和系统信息、IP地址等
  • 更新用户表中信息
  • 将在线的用户信息由框架存入Redis,
/**
 * 自定义侦听器的实现
 *
 * @author DarkClouds
 * @date 2023/05/11
 */
@Component //自动将实现类注册到 SaTokenEventCenter 事件发布中心。
public class MySaTokenListener implements SaTokenListener {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private HttpServletRequest request;

    /**
     * 每次登录时触发
     */
    @Override
    public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
        // 查询用户昵称
        User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
                .select(User::getAvatar, User::getNickname)
                .eq(User::getId, loginId));
        // 解析browser和os
        Map<String, String> userAgentMap = UserAgentUtils.parseOsAndBrowser(request.getHeader("User-Agent"));
        // 获取登录ip地址
        String ipAddress = IpUtils.getIpAddress(request);
        // 获取登录地址
        String ipSource = IpUtils.getIpSource(ipAddress);
        // 获取登录时间
        LocalDateTime loginTime = LocalDateTime.now(ZoneId.of(SHANGHAI.getZone()));
        OnlineVO onlineVO = OnlineVO.builder()
                .id((Integer) loginId)
                .token(tokenValue)
                .avatar(user.getAvatar())
                .nickname(user.getNickname())
                .ipAddress(ipAddress)
                .ipSource(ipSource)
                .os(userAgentMap.get("os"))
                .browser(userAgentMap.get("browser"))
                .loginTime(loginTime)
                .build();
        // 更新用户登录信息
        User newUser = User.builder()
                .id((Integer) loginId)
                .ipAddress(ipAddress)
                .ipSource(ipSource)
                .loginTime(loginTime)
                .build();
        userMapper.updateById(newUser);
        // 用户在线信息存入tokenSession
        SaSession tokenSession = StpUtil.getTokenSessionByToken(tokenValue);
        //由框架自动将用户信息存放到了Redis中
        tokenSession.set(ONLINE_USER, onlineVO);
    }

    /**
     * 每次注销时触发
     */
    @Override
    public void doLogout(String loginType, Object loginId, String tokenValue) {
        // 删除缓存中的用户信息
        StpUtil.logoutByTokenValue(tokenValue);
    }

    /**
     * 每次被踢下线时触发
     */
    @Override
    public void doKickout(String loginType, Object loginId, String tokenValue) {
    }

    /**
     * 每次被顶下线时触发
     */
    @Override
    public void doReplaced(String loginType, Object loginId, String tokenValue) {
        // 删除缓存中的用户信息
        StpUtil.logoutByTokenValue(tokenValue);
    }

    /**
     * 每次被封禁时触发
     */
    @Override
    public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {

    }

    /**
     * 每次被解封时触发
     */
    @Override
    public void doUntieDisable(String loginType, Object loginId, String service) {

    }

    /**
     * 每次二级认证时触发
     */
    @Override
    public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {

    }

    /**
     * 每次退出二级认证时触发
     */
    @Override
    public void doCloseSafe(String loginType, String tokenValue, String service) {

    }

    /**
     * 每次创建Session时触发
     */
    @Override
    public void doCreateSession(String id) {

    }

    /**
     * 每次注销Session时触发
     */
    @Override
    public void doLogoutSession(String id) {

    }

    /**
     * 每次Token续期时触发
     */
    @Override
    public void doRenewTimeout(String tokenValue, Object loginId, long timeout) {

    }
}

全局过滤器


利用Sa-Token全局过滤器来实现路由拦截器鉴权 

  1. 相比于拦截器,过滤器更加底层,执行时机更靠前,有利于防渗透扫描。
  2. 过滤器可以拦截静态资源,方便我们做一些权限控制。
  3. 部分Web框架根本就没有提供拦截器功能,但几乎所有的Web框架都会提供过滤器机制。

同拦截器一样,为了避免不必要的性能浪费,Sa-Token全局过滤器默认处于关闭状态,若要使用过滤器组件,首先你需要注册它到项目中

 

该类实现功能:

  • 注册分页拦截器
  • 注册Redis限流器
  • 注册 Sa-Token 的注解拦截器,打开注解式鉴权功能

/**
 * SaToken配置
 * 全局过滤器
 * @author DarkClouds
 * @date 2023/05/10
 */
@Component
public class SaTokenConfig implements WebMvcConfigurer {

    @Autowired
    private AccessLimitInterceptor accessLimitInterceptor;

    private final String[] EXCLUDE_PATH_PATTERNS = {
            "/swagger-resources",
            "/webjars/**",
            "/v2/api-docs",
            "/doc.html",
            "/favicon.ico",
            "/login",
            "/oauth/*",
    };

    private final long timeout = 600;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册分页拦截器
        registry.addInterceptor(new PageableInterceptor());
        // 注册Redis限流器
        registry.addInterceptor(accessLimitInterceptor);
        // 注册 Sa-Token 的注解拦截器,打开注解式鉴权功能
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
    }

    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
                // 拦截路径
                .addInclude("/**")
                // 放开路径
                .addExclude(EXCLUDE_PATH_PATTERNS)
                // 前置函数:在每次认证函数之前执行
                .setBeforeAuth(obj -> {
                    SaHolder.getResponse()
                            // 允许指定域访问跨域资源
                            .setHeader("Access-Control-Allow-Origin", "*")
                            // 允许所有请求方式
                            .setHeader("Access-Control-Allow-Methods", "*")
                            // 有效时间
                            .setHeader("Access-Control-Max-Age", "3600")
                            // 允许的header参数
                            .setHeader("Access-Control-Allow-Headers", "*");
                    // 如果是预检请求,则立即返回到前端
                    SaRouter.match(SaHttpMethod.OPTIONS)
                            .free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
                            .back();
                })
                // 认证函数: 每次请求执行
                .setAuth(obj -> {
                    // 检查是否登录
                    SaRouter.match("/admin/**").check(r -> StpUtil.checkLogin());
                    // 刷新token有效期
                    if (StpUtil.getTokenTimeout() < timeout) {
                        StpUtil.renewTimeout(1800);
                    }
                })
                //  异常处理函数:每次认证函数发生异常时执行此函数
                .setError(e -> {
                    // 设置响应头
                    SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
                    if (e instanceof NotLoginException) {
                        return JSONUtil.toJsonStr(Result.fail(UNAUTHORIZED.getCode(), UNAUTHORIZED.getMsg()));
                    }
                    return SaResult.error(e.getMessage());
                });
    }

}

自定义权限验证


/**
 * 自定义权限验证接口扩展
 *
 * @author DarkClouds
 * @date 2023/05/10
 */
@Component // 保证此类被SpringBoot扫描,完成Sa-Token的自定义权限验证扩展
@RequiredArgsConstructor
public class StpInterfaceImpl implements StpInterface {

    private final MenuMapper menuMapper;

    private final RoleMapper roleMapper;

    /**
     * 返回一个账号所拥有的权限码集合
     *
     * @param loginId   登录用户id
     * @param loginType 登录账号类型
     * @return 权限集合
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 声明权限码集合
        List<String> permissionList = new ArrayList<>();
        // 遍历角色列表,查询拥有的权限码
        for (String roleId : getRoleList(loginId, loginType)) {
            //如果该用户角色为管理员,则拥有所有权限
            if (ADMIN.equals(roleId)) {
                permissionList.add("*");
                return permissionList;
            }
            SaSession roleSession = SaSessionCustomUtil.getSessionById("role-" + roleId);
            List<String> list = roleSession.get("Permission_List", () -> menuMapper.selectPermissionByRoleId(roleId));
            permissionList.addAll(list);
        }
        // 返回权限码集合
        return permissionList;
    }

    /**
     * 返回一个账号所拥有的可用角色标识集合
     *
     * @param loginId   登录用户id
     * @param loginType 登录账号类型
     * @return 角色集合
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        SaSession session = StpUtil.getSessionByLoginId(loginId);
        // 从数据库查询这个账号id拥有的角色列表
        return session.get("Role_List", () -> roleMapper.selectRoleListByUserId(loginId));
    }

}

3.3、用户登录退出业务代码

1、创建LoginDTO类,用于接收前端发送的参数

/**
 * 登录信息
 *
 * @author DarkClouds
 * @date 2023/05/09
 */
@Data
@ApiModel(description = "登录信息")
public class LoginDTO {

    /**
     * 用户名
     */
    @NotBlank(message = "用户名不能为空")
    @ApiModelProperty(value = "用户名")
    private String username;

    /**
     * 用户密码
     */
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, message = "密码不能少于6位")
    @ApiModelProperty(value = "用户密码")
    private String password;


}

2、创建结果返回类,用于统一返回信息给前端

/**
 * 结果返回类
 *
 * @author DarkClouds
 * @date 2023/05/09
 */
@Data
@ApiModel(description = "结果返回类")
public class Result<T> {

    /**
     * 返回状态
     */
    @ApiModelProperty(value = "返回状态")
    private Boolean flag;

    /**
     * 状态码
     */
    @ApiModelProperty(value = "状态码")
    private Integer code;

    /**
     * 返回信息
     */
    @ApiModelProperty(value = "返回信息")
    private String msg;

    /**
     * 返回数据
     */
    @ApiModelProperty(value = "返回数据")
    private T data;

    public static <T> Result<T> success() {
        return buildResult(true, null, SUCCESS.getCode(), SUCCESS.getMsg());
    }

    public static <T> Result<T> success(T data) {
        return buildResult(true, data, SUCCESS.getCode(), SUCCESS.getMsg());
    }

    public static <T> Result<T> fail(String message) {
        return buildResult(false, null, FAIL.getCode(), message);
    }

    public static <T> Result<T> fail(Integer code, String message) {
        return buildResult(false, null, code, message);
    }

    private static <T> Result<T> buildResult(Boolean flag, T data, Integer code, String message) {
        Result<T> r = new Result<>();
        r.setFlag(flag);
        r.setData(data);
        r.setCode(code);
        r.setMsg(message);
        return r;
    }

}

3、创建UserMapper接口操作数据库

/**
 * 用户映射器
 *
 * @author DarkClouds
 * @date 2023/05/09
 */
@Repository
public interface UserMapper extends BaseMapper<User> {


}

4、编写Service,ServiceImpl实现业务代码

/**
 * 登录业务接口
 *
 * @author DarkClouds
 * @date 2023/05/09
 */
public interface LoginService {
    /**
     * 用户登录
     *
     * @param login 登录参数
     * @return token
     */
    String login(LoginDTO login);
}


/**
 *  登录业务接口实现类
 *
 * @author DarkClouds
 * @date 2023/05/09
 */
@Service
//代替@Autowired,需要加上final修饰
@RequiredArgsConstructor
public class LoginServiceImpl implements LoginService {

    private final UserMapper userMapper;

    @Override
    public String login(LoginDTO login) {

        User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
                .select(User::getId)
                .eq(User::getUsername, login.getUsername())
                .eq(User::getPassword, SecurityUtils.sha256Encrypt(login.getPassword())));
        // 进行断言,如果查询结果为空,则抛出异常
        Assert.notNull(user,"用户不存在或密码错误");
        // 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
        StpUtil.checkDisable(user.getId());
        // 通过校验后,再进行登录
        StpUtil.login(user.getId());
        //返回ToKen
        return StpUtil.getTokenValue();
    }
}

StpUtil.login(Object id)  ----  直接调用框架方法实现登录

会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等

 

只此一句代码,便可以使会话登录成功,实际上,Sa-Token 在背后做了大量的工作,包括但不限于:

  1. 检查此账号是否之前已有登录
  2. 为账号生成 Token 凭证与 Session 会话
  3. 通知全局侦听器,xx 账号登录成功
  4. 将 Token 注入到请求上下文
  5. 等等其它工作……

Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端

StpUtil.login(id) 方法利用了 Cookie 自动注入的特性,省略了手写返回 Token 的代码

5、controller实现

/**
 * 登录控制器
 *
 * @author DarkClouds
 * @date 2023/05/09
 */
@Api(tags = "登录模块")
@RestController
@RequiredArgsConstructor
public class LoginController {

    private final LoginService loginService;

    /**
     * 用户登录
     *
     * @param loginDTO 登录参数
     * @return {@link String} Token
     */
    @ApiOperation(value = "用户登录")
    @PostMapping("/login")
    public Result<String> login(@Validated @RequestBody LoginDTO loginDTO) {
        return Result.success(loginService.login(loginDTO));
    }

    /**
     * 用户退出
     */
    @SaCheckLogin //登录校验 —— 只有登录之后才能进入该方法。
    @ApiOperation(value = "用户退出")
    @GetMapping("/logout")
    public Result<?> logout() {
        StpUtil.logout();
        return Result.success();
    }

}

 StpUtil.logout(); ---  当前会话注销登录

 

3.4、注解鉴权

注解鉴权 —— 优雅的将鉴权与业务代码分离!

  • @SaCheckLogin: 登录校验 —— 只有登录之后才能进入该方法。
  • @SaCheckRole("admin"): 角色校验 —— 必须具有指定角色标识才能进入该方法。
  • @SaCheckPermission("user:add"): 权限校验 —— 必须具有指定权限才能进入该方法。
  • @SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法。
  • @SaCheckBasic: HttpBasic校验 —— 只有通过 Basic 认证后才能进入该方法。
  • @SaIgnore:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。
  • @SaCheckDisable("comment"):账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。

Sa-Token 使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态
因此,为了使用注解鉴权,你必须手动将 Sa-Token 的全局拦截器注册到你项目中

        // 注册 Sa-Token 的注解拦截器,打开注解式鉴权功能
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");

使用注解鉴权

// 登录校验:只有登录之后才能进入该方法 
@SaCheckLogin                        
@RequestMapping("info")
public String info() {
    return "查询用户信息";
}

// 角色校验:必须具有指定角色才能进入该方法 
@SaCheckRole("super-admin")        
@RequestMapping("add")
public String add() {
    return "用户增加";
}

// 权限校验:必须具有指定权限才能进入该方法 
@SaCheckPermission("user-add")        
@RequestMapping("add")
public String add() {
    return "用户增加";
}

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

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

相关文章

20230509MTCNN2

卷积切分图片 怎么切分图片? 使用opencv,PIL切分图片有什么问题? 慢 使用 卷积来切分图片 卷积的运算过程 类似于切图 卷积 对输入图片的尺寸 有没有 的要求? 就是 输入的图片尺寸 必须大于 卷积核的大小 test1.py import torch from torch import nn""&quo…

springboot + vue3实现视频播放Demo

文章目录 学习链接前言ffmpeg安装ffmpeg配置环境变量分割视频文件 后台配置WebConfig 前端代码video.js示例安装依赖视频播放组件效果 Vue3-video-play示例安装依赖main.js中使用视频播放组件效果 学习链接 ffmpeg官网 长时长视频java存储及vue播放解决方法 【 攻城略地 】vue…

BitKeep逆势崛起:千万用户的信任,终点还未到来

在全球范围内&#xff0c;BitKeep钱包如今已拥有超过千万忠实用户。 当我得知这一令人震撼的数字时&#xff0c;既感到惊讶&#xff0c;同时也觉得这是意料之中的事情。几年来关注BitKeep的发展历程&#xff0c;我深切地感受到了这家公司的蓬勃壮大。回顾2018年他们发布的第一个…

linux0.12-8-9-fork.c

[362页] 1、 verify_area函数给其他文件使用的&#xff0c;跳转开始位置&#xff1b; 2、 copy_mem函数复制内存页表&#xff1b; 3、 copy_process函数是fork.c主要函数&#xff1b; 4、find_empty_process函数就2个作用&#xff1a;在一个范围内找last_pid和找空槽&#xff1…

如何利用互联网优势进行茶叶销售?

茶叶是中国传统文化的重要组成部分&#xff0c;具有丰富的文化内涵和高度的营养价值。如今&#xff0c;随着互联网的普及&#xff0c;越来越多的茶叶销售商&#xff08;文章编辑ycy6221&#xff09;开始利用互联网的优势来开拓市场。本文将介绍如何利用互联网优势进行茶叶销售。…

SecureCRT的下载安装

亲测成功了&#xff0c;按照下面的步骤完成即可&#xff01; 下载安工具包包地址连接&#xff1a;网盘地址点击即可 提取码&#xff1a;0lp7 1、下载SecureCRT 2、从百度网盘下载SecureCRT&#xff0c;页面如下 3、安装SecureCRT 4、激活SecureCRT 第一步&#xff1a;打开安装…

自学Java怎么找工作?好程序员学员大厂面试经验分享!

简历要详细&#xff1a; 简历中的项目用到的技术点和个人负责的模块尽量写详细一些。其次&#xff0c;根据自己项目中用到的熟悉的技术点&#xff0c;在个人技能介绍模块去突出&#xff0c;面试官基本会根据你简历上写的去提问的&#xff0c;这样我们回答起来就会更加得心应手。…

【多线程初阶四】单例模式阻塞队列

目录 &#x1f31f;一、单例模式 &#x1f308;1、饿汉模式 &#x1f308;2、懒汉模式&#xff08;重点&#xff01;&#xff09; &#x1f31f;二、工厂模式 &#x1f31f;三、阻塞式队列 &#x1f308;1、阻塞队列是什么&#xff1f; &#x1f308;2、…

如何注册appuploader账号​

如何注册appuploader账号​ 我们上一篇讲到appuploader的下载安装&#xff0c;要想使用此软件呢&#xff0c;需要注册账号才能使用&#xff0c;今​ 天我们来讲下如何注册appuploader账号来使用软件。​ 1.Apple官网注册Apple ID​ 首先我们点击首页左侧菜单栏中的“常见网…

为什么企业选择局域网即时通讯软件?局域网即时通讯软件哪家好?

在当今互联网普及的时代&#xff0c;企业内部的沟通对企业管理有着非常重要的意义&#xff0c;即时通讯软件已成为企业工作中广泛采用的沟通工具。 然而&#xff0c;随着企业内部敏感信息通过互联网泄露的频繁发生&#xff0c;例如在工作期间&#xff0c;企业员工自发地频繁使…

盘点四款免费在线采购管理系统

今天来盘点五款免费在线采购管理系统。中小型企业在选择采购管理系统时成本是需要考虑的重要因素之一&#xff0c;因此免费在线的采购管理系统是最合适的第一步选择&#xff0c;本文将为您盘点免费在线采购管理系统&#xff1a;1.简道云&#xff1b;2.甄云&#xff1b;3.携客云…

【正点原子STM32连载】 第九章 STM32启动过程分析 摘自【正点原子】STM32F103 战舰开发指南V1.2

1&#xff09;实验平台&#xff1a;正点原子stm32f103战舰开发板V4 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id609294757420 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html 第九章…

Redis可持久化详解1

目录 Redis可持久化 以下是RDB持久化的代码示例&#xff1a; 面试常问 1什么是Redis的持久化机制&#xff1f; 2Redis支持哪些持久化机制&#xff1f;它们有什么区别&#xff1f; 3Redis的RDB持久化机制的原理是什么&#xff1f; 4Redis的AOF持久化机制的原理是什么&…

《三》包管理工具 npm

包管理工具 npm&#xff1a; npm&#xff1a;Node Package Manager&#xff0c;Node 包管理器&#xff0c;目前已经不仅仅作为 Node 的包管理工具&#xff0c;也作为前端的包管理工具来管理包。 npm 管理的包是存放在一个名为 registry 的仓库中的&#xff0c;发布一个包时是…

分享推荐32位MCU低成本替换8/16位升级完美之选——MM32G0001

灵动微在嵌入式闪存技术上做了优化&#xff0c;采用更稳定和经大规模量产验证的12寸晶圆工艺平台&#xff0c;对产品的功能、性能和成本进行了全方位的打磨&#xff0c;在保持MM32品质目标的前提下&#xff0c;推出了这款极具性价比、低成本的MM32G0001系列MCU产品。 不同于市…

Nuxt学习笔记

创建项目 npx create-nuxt-app projectName SSR 渲染流程 客户端发送 URL 请求到服务端&#xff0c;服务端读取对应的 URL 的模板信息&#xff0c;在服务端做出 HTML 和数据的渲染&#xff0c;渲染完成之后返回整个 HTML 结构给客户端。所以用户在浏览首屏的时候速度会比较快…

Scala学习(四)

文章目录 1.闭包2.函数式编程递归和尾递归2.1递归2.2 尾递归 3.控制抽象3.1 值调用3.2 名调用 4.惰性函数 1.闭包 如果一个函数&#xff0c;访问到了它的外部(局部)变量的值&#xff0c;那么这个函数和它所处的环境称之为闭包 //闭包练习def sumX(x:Int){def sumY(y:Int):Int{…

闲谈【Stable-Diffusion WEBUI】的插件:模型工具箱:省空间利器

文章目录 &#xff08;零&#xff09;前言&#xff08;一&#xff09;模型工具箱&#xff08;Model Toolbox&#xff09;&#xff08;1.1&#xff09;基本使用界面&#xff08;1.2&#xff09;高阶使用界面&#xff08;1.3&#xff09;自动修剪模型 &#xff08;零&#xff09;…

MyBatis基础介绍

目录 MyBatis是什么 怎么学MyBatis 第一个MyBatis查询 MyBatis的定位 创建数据库和表 搭建MyBatis环境 添加MyBatis框架支持 设置MyBatis的配置信息 设置数据库连接的相关信息 配置MyBatis xml的保存路径和xml命名规范 MyBatis模式开发 创建一个实体类 创建MyBatis…

AI换脸系统开发源码交付

AI换脸系统软件的发展趋势包括以下几个方面&#xff1a; 定制化和智能化&#xff1a;随着用户需求的不断增加&#xff0c;AI换脸系统将向更加定制化和智能化的方向发展&#xff0c;通过数据分析和用户画像等手段&#xff0c;为用户提供更加个性化的服务。 多模态应用&a…