【Redis】2、Redis应用之【根据 Session 和 Redis 进行登录校验和发送短信验证码】

news2024/11/17 12:28:09

目录

  • 一、基于 Session 实现登录
    • (1) 发送短信验证码
      • ① 手机号格式后端校验
      • ② 生成短信验证码
    • (2) 短信验证码登录、注册
    • (3) 登录验证
      • ① 通过 SpringMVC 定义拦截器
      • ② ThreadLocal
    • (4) 集群 Session 不共享问题
  • 二、基于 Redis 实现共享 session 登录
    • (1) 登录之后,缓存 token 到客户端
    • (2) 每次请求都携带 token
    • (3) 短信验证码
    • (4) 短信验证码登录、注册
    • (5) 免登录
    • (6) 刷新登录有效期

🌼 文章基于 B 站黑马程序员视频教程编写
🌼 做笔记便于日后复习

一、基于 Session 实现登录

在这里插入图片描述

(1) 发送短信验证码

在这里插入图片描述

① 手机号格式后端校验

手机号校验的正则表达式

/**
 * 正则表达式
 */
public abstract class RegexPatterns {
    
    /**
     * 手机号正则
     */
    public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";

    /**
     * 邮箱正则
     */
    public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";

    /**
     * 密码正则:4~32 位的字母、数字、下划线
     */
    public static final String PASSWORD_REGEX = "^\\w{4,32}$";
    
    /**
     * 验证码正则, 6位数字或字母
     */
    public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";

}

校验工具类:

public class RegexUtils {
    /**
     * 是否是无效手机格式
     *
     * @param phone 要校验的手机号
     * @return true:符合,false:不符合
     */
    public static boolean isPhoneInvalid(String phone) {
        return mismatch(phone, RegexPatterns.PHONE_REGEX);
    }

    /**
     * 是否是无效邮箱格式
     *
     * @param email 要校验的邮箱
     * @return true:符合,false:不符合
     */
    public static boolean isEmailInvalid(String email) {
        return mismatch(email, RegexPatterns.EMAIL_REGEX);
    }

    /**
     * 是否是无效验证码格式
     *
     * @param code 要校验的验证码
     * @return true:符合,false:不符合
     */
    public static boolean isCodeInvalid(String code) {
        return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);
    }

    // 校验是否不符合正则格式
    private static boolean mismatch(String str, String regex) {
        if (StrUtil.isBlank(str)) {
            return true;
        }
        return !str.matches(regex);
    }
}

② 生成短信验证码

     <dependency>
         <groupId>cn.hutool</groupId>
         <artifactId>hutool-all</artifactId>
         <version>5.7.17</version>
     </dependency>

🌼 hutool 工具的详细使用: https://doc.hutool.cn/pages/index/


    /**
     * 发送短信验证码
     *
     * @param phone   手机号
     * @param session 用户缓存验证码
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 校验手机号格式是否符合手机号的规范
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误");
        }

        // 生成 6 位数字短信验证码
        String code = RandomUtil.randomNumbers(6);

        // 把短信验证码保存到服务端 session 中
        session.setAttribute("code", code);

        // 发送短信验证码给手机号 phone
        log.info("向 {} 手机号发送了验证码:{}", phone, code);

        return Result.ok("发送验证码成功");
    }

(2) 短信验证码登录、注册

在这里插入图片描述

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        Result result = requestParamsValidate(loginForm, session);

        if (!result.getSuccess()) { // 手机号或验证码校验失败
            return result;
        }

        // 根据手机号查询用户
        String phone = loginForm.getPhone();
        User user = query().eq("phone", phone).one();

        // 根据手机号在数据中没有查询到用户信息
        if (user == null) {
            // 注册用户
            Result saveResult = saveUserByPhone(phone);
            if (saveResult.getSuccess()) { // 注册用户成功
                user = (User) saveResult.getData();
            } else {
                return saveResult;
            }
        }

        // 保存用户信息到 session
        session.setAttribute("user", user);

        return Result.ok("登录成功");
    }

    private Result saveUserByPhone(String phone) {
        User newUser = new User();
        newUser.setNickName("USER_" + RandomUtil.randomString(9));
        newUser.setPhone(phone);
        if (save(newUser)) {
            return Result.ok(newUser);
        }

        return Result.fail("服务器忙, 用户保存到数据库失败");
    }

    private Result requestParamsValidate(LoginFormDTO loginForm, HttpSession session) {
        // 校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误");
        }

        // 校验验证码是否存在
        Object cacheCode = session.getAttribute("code");
        String paramCode = loginForm.getCode();

        if (RegexUtils.isCodeInvalid(paramCode) || !paramCode.equals(cacheCode)) {
            return Result.fail("验证码错误");
        }

        return Result.ok();
    }

(3) 登录验证

在这里插入图片描述

在这里插入图片描述

🌿 根据 Cookie 中的 JSESSIONID 获取到 Session
🌿 然后从 Session 中获取到信息


🌿 登录校验需要在拦截器(Interceptor)中完成
🌿 SpringMVC 的 Interceptor 可以拦截 Controller,在请求到达 Controller 之前做一些事情

① 通过 SpringMVC 定义拦截器

  • 实现(implements)HandlerInterceptor 接口
  • 可覆盖该接口中的三个默认方法

🌼 preHandle:在 Controller 的处理方法之前调用(当该方法的返回值为 true 的时候 才执行 Controller 里面的内容)
🌱通常在 preHandle 中进行初始化、请求预处理等操作(可进行登录验证
🌱preHandle 返回 true 才会执行后面的调用。若返回 false,不会调用 Controller 处理方法、postHandle 和 afterCompletion
🌱当有多个拦截器时,preHandle 按照正序执行

🌼 postHandle:在 Controller 的处理方法之DispatcherServlet 进行视图渲染之调用
🌱可在 postHandle 中进行请求后续加工处理操作
🌱当有多个拦截器时,postHandle 按照序执行

🌼 afterCompletion:在 DispatcherServlet 进行视图渲染之后调用
🌱一般在这里进行资源回收操作
🌱当有多个拦截器时,afterCompletion 按照逆序执行


配置拦截器:

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    /**
     * 配置拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns( // 这些请求不可拦截
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

② ThreadLocal

🌼 ThreadLocal 可以解释成线程的局部变量
🌼一个 ThreadLocal 的变量只有当前自身线程可以访问,别的线程都访问不了,那么自然就避免了线程竞争
🌼ThreadLocal 提供了一种与众不同的线程安全方式,它不是在发生线程冲突时想办法解决冲突,而是彻底避免了冲突的发生

/**
 * 用于保存 UserDTO 的 ThreadLocal 的封装
 */
public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user) {
        tl.set(user);
    }

    public static UserDTO getUser() {
        return tl.get();
    }

    public static void removeUser() {
        tl.remove();
    }
}

登录拦截器:

/**
 * 登录校验拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        HttpSession session = request.getSession();
        Object user = session.getAttribute("user");

        if (null == user) {
            response.setStatus(401);
            return false; // 拦截
        }

        UserDTO userDTO = user2UserDto((User) user);

        // 保存用户信息到 ThreadLocal 中
        UserHolder.saveUser(userDTO);
        return true;
    }

    private UserDTO user2UserDto(User user) {
        UserDTO userDTO = new UserDTO();
        userDTO.setIcon(user.getIcon());
        userDTO.setId(user.getId());
        userDTO.setNickName(user.getNickName());
        return userDTO;
    }

    /**
     * 资源释放
     */
    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        UserHolder.removeUser();
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {

    }
}

    @GetMapping("/me")
    public Result me() {
        // 获取当前登录的用户并返回
        UserDTO userDto = UserHolder.getUser();
        return Result.ok(userDto);
    }

🌼 /me 这个 Controller 执行完毕后,LoginInterceptor 的 afterCompletion 会被调用

(4) 集群 Session 不共享问题

在这里插入图片描述
在这里插入图片描述

二、基于 Redis 实现共享 session 登录

在这里插入图片描述
在这里插入图片描述

🎉 登录之后,每次发起请求都要携带 token(用户登录凭证)

(1) 登录之后,缓存 token 到客户端

     login() {
           const {radio, phone, code} = this.form

           if (!radio) {
               this.$message.error("请先确认阅读用户协议!");
               return
           }
           if (!phone || !code) {
               this.$message.error("手机号和验证码不能为空!");
               return
           }
           if (phone.length !== 11) {
               this.$message.error("手机号格式错误!");
               return
           }

           axios.post("/user/login", this.form)
               .then(({data}) => {
                   if (data) {
                       // 保存用户信息到 session
                       sessionStorage.setItem("token", data);
                   }
                   // 跳转到首页
                   location.href = "/info.html"
               })
               .catch(err => {
                   console.log(err)
                   this.$message.error(err)
               })
       },

(2) 每次请求都携带 token

let commonURL = "/api";
// 设置后台服务地址
axios.defaults.baseURL = commonURL
axios.defaults.timeout = 2000

// request 拦截器,将用户 token 放入头中
let token = sessionStorage.getItem("token")

// 请求拦截器
axios.interceptors.request.use(
    config => {
        // 如果 token 存在, 将用户 token 放入头中(key 是 authorization)
        if (token) config.headers['authorization'] = token
        return config
    },
    error => {
        console.log(error)
        return Promise.reject(error)
    }
)

// 响应拦截器
axios.interceptors.response.use(function (response) {
    // 判断执行结果
    if (!response.data.success) {
        return Promise.reject(response.data.errorMsg)
    }
    return response.data;
}, function (error) {
    // 一般是服务端异常或者网络异常
    console.log(error)
    if (error.response.status == 401) {
        // 未登录,跳转
        setTimeout(() => {
            location.href = "/login.html"
        }, 200);
        return Promise.reject("请先登录");
    }
    return Promise.reject("服务器异常");
});

// 请求参数序列化
axios.defaults.paramsSerializer = function (params) {
    let p = "";
    Object.keys(params).forEach(k => {
        if (params[k]) {
            p = p + "&" + k + "=" + params[k]
        }
    })
    return p;
}

🎶在 axios 的请求拦截器中配置 token,每次发起请求该请求会首先被拦截
🎶被拦截之后,往该请求的请求头中设置 token【key: authorization;value: token 值】

(3) 短信验证码

Redis 相关常量:

public class RedisConstants {
    public static final String LOGIN_CODE_KEY = "login:code:phone";
    public static final Long LOGIN_CODE_TTL = 2L; // 验证码有效期
    public static final String LOGIN_USER_KEY = "login:token:";
    public static final Long LOGIN_USER_TTL = 36000L;

    public static final Long CACHE_NULL_TTL = 2L;

    public static final Long CACHE_SHOP_TTL = 30L;
    public static final String CACHE_SHOP_KEY = "cache:shop:";

    public static final String LOCK_SHOP_KEY = "lock:shop:";
    public static final Long LOCK_SHOP_TTL = 10L;

    public static final String SECKILL_STOCK_KEY = "seckill:stock:";
    public static final String BLOG_LIKED_KEY = "blog:liked:";
    public static final String FEED_KEY = "feed:";
    public static final String SHOP_GEO_KEY = "shop:geo:";
    public static final String USER_SIGN_KEY = "sign:";
}
   public Result sendCode(String phone, HttpSession session) {
       // 校验手机号格式是否符合手机号的规范
       if (RegexUtils.isPhoneInvalid(phone)) {
           return Result.fail("手机号格式错误");
       }

       // 生成 6 位数字短信验证码
       String code = RandomUtil.randomNumbers(6);

       // 把短信验证码保存到 Redis 中
       stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone,
               code,
               RedisConstants.LOGIN_CODE_TTL,
               TimeUnit.MINUTES);

       // 发送短信验证码给手机号 phone
       log.info("向 {} 手机号发送了验证码:{}", phone, code);

       return Result.ok("发送验证码成功");
   }

(4) 短信验证码登录、注册

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        Result result = requestParamsValidate(loginForm, session);

        if (!result.getSuccess()) { // 手机号或验证码校验失败
            return result;
        }

        // 根据手机号查询用户
        String phone = loginForm.getPhone();
        User user = query().eq("phone", phone).one();

        // 根据手机号在数据中没有查询到用户信息
        if (user == null) {
            // 注册用户
            Result saveResult = saveUserByPhone(phone);
            if (saveResult.getSuccess()) { // 注册用户成功
                user = (User) saveResult.getData();
            } else {
                return saveResult;
            }
        }

        // 保存用户信息到 Redis
        // 生成随机 token 串
        String token = RedisConstants.LOGIN_USER_KEY + UUID.randomUUID().toString(true);

        // 把 User 转换为 UserDTO(过滤敏感数据)
        UserDTO userDTO = user2UserDto(user);

        // 把 UserDTO 转换为 HashMap(便于往 Redis 中存储)
        Map<String, Object> userDtoMap = BeanUtil.beanToMap(userDTO,
                new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));


        // 把用户信息保存到 Redis
        // stringRedisTemplate 要求存储的 key 和 value 都必须是 String 类型
        // 否则会报类型转换错误
        stringRedisTemplate.opsForHash().putAll(token, userDtoMap);
        // 设置登录有效期
        stringRedisTemplate.expire(token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        session.setAttribute("user", user);

        return Result.ok(token);
    }

    private UserDTO user2UserDto(User user) {
        UserDTO userDTO = new UserDTO();
        userDTO.setIcon(user.getIcon());
        userDTO.setId(user.getId());
        userDTO.setNickName(user.getNickName());
        return userDTO;
    }

    private Result saveUserByPhone(String phone) {
        User newUser = new User();
        newUser.setNickName("USER_" + RandomUtil.randomString(9));
        newUser.setPhone(phone);
        if (save(newUser)) {
            return Result.ok(newUser);
        }

        return Result.fail("服务器忙, 用户保存到数据库失败");
    }

    private Result requestParamsValidate(LoginFormDTO loginForm, HttpSession session) {
        // 校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误");
        }

        // 通过手机号获取验证码
        String redisCode = stringRedisTemplate.opsForValue()
                .get(RedisConstants.LOGIN_CODE_KEY + phone);

        // 校验验证码是否存在
        String paramCode = loginForm.getCode();

        if (RegexUtils.isCodeInvalid(paramCode) || !paramCode.equals(redisCode)) {
            return Result.fail("验证码错误");
        }

        return Result.ok();
    }

}

(5) 免登录

🎄 需要实现一个效果:用户只有在使用该应用,哪么该用户的登录有效期就延长七天
🎄 而不是到达七天就退出登录

🎄 在 LoginInterceptor 中,若用户有发请求,就把该用户的登录有些时间的缓存更新为七天

/**
 * 登录校验拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        // 获取请求头中的 token 串
        String token = request.getHeader("authorization");

        if (StrUtil.isBlank(token)) {
            response.setStatus(401);
            return false; // 拦截
        }

        // 通过前端传过来的 token 串从 Redis 中获取用户信息
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(token);

        if (userMap.isEmpty()) {
            response.setStatus(401);
            return false; // 拦截
        }

        // 保存用户信息到 ThreadLocal 中
        // 把 HashMap 类型转换为 UserDto 类型
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserHolder.saveUser(userDTO);

        // 刷新登录有效期(只要用户是活跃的, 登录有效期就延长七天)
        stringRedisTemplate.expire(token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        return true;
    }

    /**
     * 资源释放
     */
    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        UserHolder.removeUser();
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {

    }
}

🎄 LoginInterceptor 拦截器并没有被 IoC 管理,所以不能在 LoginInterceptor 中使用 @Resource@Autowired

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 配置拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                .excludePathPatterns( // 这些请求不可拦截
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

(6) 刷新登录有效期

在这里插入图片描述

🎄 RefreshInterceptor 会拦截每一个请求,然后进行登录有效期刷新
🎄 上一节中的 LoginInterceptor 只有当访问需要登录校验的请求的时候才会刷新

/**
 * 刷新登录拦截器(每个请求都要来到这里)
 */
public class RefreshLoginInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;

    public RefreshLoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        // 获取请求头中的 token 串
        String token = request.getHeader("authorization");

        if (StrUtil.isBlank(token)) {
            return true; // 拦截(进入 LoginInterceptor)
        }

        // 通过前端传过来的 token 串从 Redis 中获取用户信息
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(token);

        if (userMap.isEmpty()) {
            return true; // 拦截(进入 LoginInterceptor)
        }

        // 保存用户信息到 ThreadLocal 中
        // 把 HashMap 类型转换为 UserDto 类型
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserHolder.saveUser(userDTO);

        // 刷新登录有效期(只要用户是活跃的, 登录有效期就延长七天)
        stringRedisTemplate.expire(token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        return true;
    }

    /**
     * 资源释放
     */
    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        UserHolder.removeUser();
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {

    }
}
/**
 * 登录校验拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        if (UserHolder.getUser() == null) {
            response.setStatus(401);
            return false;
        }

        return true;
    }

}
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 配置拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns( // 这些请求不可拦截
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );

        // order 值越小优先级越高
        // order 值默认是 0
        registry.addInterceptor(new RefreshLoginInterceptor(stringRedisTemplate))
                .addPathPatterns("/**").order(-1);
    }
}

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

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

相关文章

23款迈巴赫S480升级原厂10°后轮转向系统,减少转弯半径

就是低速的情况下&#xff0c;有更强的机动性&#xff0c;前后车轮的不同转动方向使得车辆可以凭借更更小转弯半径实现转向&#xff0c;在特定的狭窄路段或者停车时&#xff0c;车辆的操控性大大提升&#xff0c;而内轮差也缩小也增大了转向的安全性。 高速的情况下&#xff0…

C. Road Optimization(dp)

Problem - 1625C - Codeforces 火星政府不仅对优化太空飞行感兴趣&#xff0c;还希望改进该行星的道路系统。 火星上最重要的高速公路之一连接着奥林匹克城和西多尼亚的首都Kstolop。在这个问题中&#xff0c;我们只考虑从Kstolop到奥林匹克城的路线&#xff0c;而不考虑相反的…

技术创举!比亚迪-汉上的实景三维导航...

实景三维技术的发展日新月异&#xff0c;但在应用中却一直深陷内存占用、渲染缓慢、加载卡顿和模型塌陷等问题。对此&#xff0c;大势智慧率先推出海量数据轻量化技术&#xff0c;在业内首次实现实景三维模型在车机系统的直接浏览&#xff0c;展示了轻量化技术赋能实景三维应用…

面试Dubbo ,却问我和Springcloud有什么区别?

Dubbo 、Springcloud? 这两有关系&#xff1f; 前言一、RPC 框架的概念1. 什么是RPC框架2. RPC 和 普通通信 的区别 二、常用 RPC 框架1. Dubbo2. gRPC3. Thrift4. Feign 三、dubbo 与 Springcloud1. Dubbo 的模型2. Springcloud3. dubbo 与 Springcloud 的区别 前言 提到Dub…

若隐若现的芯片

先看效果&#xff1a; 再看代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>若隐若现的芯片</title><script src"https://unpkg.co/gsap3/dist/gsap.min.js">…

HBase(5):导入测试数据集

1 需求 将ORDER_INFO.txt 中的HBase数据集&#xff0c;我们需要将这些指令放到HBase中执行&#xff0c;将数据导入到HBase中。 可以看到这些都是一堆的put语句。那么如何才能将这些语句全部执行呢&#xff1f; 2 执行command文件 2.1 上传command文件 将该数据集文件上传到指…

单点登录原理

单点登录原理 一、什么是单点登录 单点登录英文全称Single Sign On&#xff0c;简称SSO。指在多系统应用群中登录一个系统&#xff0c;便可在其他所有系统中得到授权而无需再次登录&#xff0c;包括单点登录与单点注销两部分。 二、为什么需要单点登录 在一些子系统用户信息…

初识Go语言25-数据结构与算法【堆、Trie树、用go中的list与map实现LRU算法、用go语言中的map和堆实现超时缓存】

文章目录 堆Trie树练习-用go中的list与map实现LRU算法练习-用go语言中的map和堆实现超时缓存 堆 堆是一棵二叉树。大根堆即任意节点的值都大于等于其子节点。反之为小根堆。   用数组来表示堆&#xff0c;下标为 i 的结点的父结点下标为(i-1)/2&#xff0c;其左右子结点分别为…

Python图像锐化及边缘检测(Roberts、Prewitt、Sobel、Lapllacian、Canny、LOG)

目录 图像锐化概述 算法方法介绍 代码实现 效果展示 图像锐化概述 图像锐化(image sharpening)是补偿图像的轮廓&#xff0c;增强图像的边缘及灰度跳变的部分&#xff0c;使图像变得清晰&#xff0c;分为空间域处理和频域处理两类。图像锐化是为了突出图像上地物的边缘、轮…

Docker|kubernetes|本地镜像批量推送到Harbor私有仓库的脚本

前言&#xff1a; 可能有测试环境&#xff0c;而测试环境下有N多的镜像&#xff0c;需要批量导入到自己搭建的Harbor私有仓库内&#xff0c;一般涉及到批量的操作&#xff0c;自然还是使用脚本比较方便。 本文将介绍如何把某个服务器的本地镜像 推送到带有安全证书的私有Harb…

【P61】JMeter JDBC Connection Configuration

文章目录 一、JDBC Connection Configuration 参数说明二、准备工作 一、JDBC Connection Configuration 参数说明 可以给数据源配置不同的连接池&#xff0c;供后续 JDBC 采样器使用&#xff1b;使用前请将对应的数据库驱动复制到 $JMETER_HOME/lib/ 或者 $JMETER_HOME/lible…

使用注解开发

使用注解开发 为了方便查看测试结果以及方便调试&#xff0c;先熟悉和配置日志。 日志 日志工厂 如果一个数据库操作出现了异常、需要进行排错&#xff0c;可以通过查看日志的方式实现。 Mybatis内置的日志工厂能够提供日志功能&#xff0c;具体的日志实现有以下几种&#xff…

基于Java+Vue前后端分离“魅力”繁峙宣传网站设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

国产MCU-CW32F030开发学习-ST7735 LCD模块

国产MCU-CW32F030开发学习-ST7735 LCD模块 硬件平台 CW32_48F大学计划板CW32_IOT_EVA物联网开发评估套件0.96 IIC oled模块 ST7735 LCD模块 硬件接口使用的 2.54mm 间距的排针接口&#xff0c;这使用杜邦线进行连接. ST7735参数供电电压3.3~5.5V驱动ICST7735分辨率128x1…

基于antdv封装的特殊复杂表格,带通行描述信息、可展示通行的单元格信息、可跨页选择数据功能、分页功能、可编辑单元格功能

基于antdv封装的特殊复杂表格&#xff0c;带通行描述信息 主要功能&#xff1a; 可展示通行的单元格信息可跨页选择数据功能表单插槽、合计插槽、操作按钮区插槽分页功能接口内请求api可编辑单元格表格组件暴漏出的方法&#xff1a;查询、获取选中数据、接口返回数据、当前表…

消防应急照明和疏散指示系统手动控制的设计与应用

摘要&#xff1a;针对非集中控制型消防应急照明和疏散指示系统在火灾确认后如何手动控制系统的应急启动存在的实际问题&#xff1a;在哪里手动控制&#xff1f;由谁来手动控制&#xff1f;什么时候能够手动控制&#xff1f;提出 3 种手动控制应急启动系统的方案&#xff1a;① …

Fluttter的ClipRRect控件

ClipRRect简介 ClipRRect&#xff08;Rounded Rectangle Clip&#xff09;是Flutter中的一个控件&#xff0c;用于将其子控件剪裁为圆角矩形形状。 使用场景 ClipRRect通常在需要给子控件添加圆角效果时使用。它可以用于创建圆角图片、圆角容器等各种UI元素。 主要属性 bo…

uniapp 开发小程序之实现不同身份展示不同的 tabbar(底部导航栏),附带相关问题解答

效果展示&#xff1a; 引言 在开发过程中逐渐意识到uniapp原生的tabbar可能不能满足开发要求&#xff0c;通过浏览博客才选择使用uView的Tabbar 底部导航栏来实现&#xff0c;我选择用的是2X版本 安装 我是使用Hbuilder插件的方式引入的组件库&#xff0c;安装配置可以看这篇…

CVE-2023-34541 LangChain 任意命令执行

漏洞简介 LangChain是一个用于开发由语言模型驱动的应用程序的框架。 在LangChain受影响版本中&#xff0c;由于load_prompt函数加载提示文件时未对加载内容进行安全过滤&#xff0c;攻击者可通过构造包含恶意命令的提示文件&#xff0c;诱导用户加载该文件&#xff0c;即可造成…

单片机学习 14-DS18B20温度传感器实验

DS18B20 温度传感器实验 ​ 本次实验我们来学习精度较高的外部 DS18B20 数字温度传感器&#xff0c;由于此传感器是单总线接口&#xff0c;所以需要使用 51 单片机的一个 IO 口模拟单总线时序与 DS18B20 通信&#xff0c;将检测的环境温度读取出来。开发板上集成了 1 个 DS18B…