当前位置: 首页 > news >正文

微信公众号扫码授权登录思路

引言

上学期研究了一下微信登录相关内容,也写了两三篇笔记,但是最后实际登录流程没有写,主要因为感觉功能完成有所欠缺,一直也没有好的思路;这两天我又看了看官方文档,重新构思了一下微信公众号登录相关的内容,可能还是有不足之处,但是基本架子已经搭起来了,在这里我就简单说明一下我的想法;

关于登录方式

登录方式在我看来是必须要设计好的一个关键点,我设计的系统涉及到了手机号验证码登录,每个用户绑定的手机号是唯一的;所以对于初次使用微信登录的用户,必须绑定一个手机号才可以;(如果你只是单纯的微信登录,就会少很多步骤,下面我会细说)

微信中有一个叫openId的参数,这个参数可以说对每个关注公众号的用户都是不一样的,也就是每个用户的唯一性标识;

那么我们可以分析出来,一个用户除了id主键唯一以外,他的手机号是唯一的,微信中对应的openId也是唯一的;

所以数据库中就需要有一个用来存放openId的字段,后期微信登录就要通过该字段来检索用户;

准备工作

想要实现微信公众号扫码登录,首先要知道以下三点的实现:

1,获取微信公众号二维码

2,微信网页授权的实现

3,微信公众号消息接收和回复的实现

这三点我之前已经写过对应文章了,可以结合微信官方文档学习;

  • 微信公众号扫码登录(一)—— 获取微信公众号二维码
  • 微信登录——授权登录获取用户信息
  • 微信公众号被动消息回复实现

下面代码可能会和上面文章有所重复,但也有修改之处,看不懂可以对比来看;

登录流程

我的思路如下:

image-20230213205721283

可以分为以下步骤:

1,判断fromUserName即用户openId在数据库中是否存在
2, 如果存在则通过该openId查询到该用户信息,生成token进行登录操作
3, 如果不存在用户信息则跳转到手机号绑定页面
4, 如果该手机号已经注册有用户,则绑定该openId
5, 如果没有注册,则获取用户微信信息,将openId和手机号绑定


大致思路就是这样,其中当然还有很多小细节,下面用代码来大致演示一下;

代码实现

这个接口其实是实现微信消息接收和推送的接口,上面文章中也有,这里只是提取了service层

/*** 接收微信公众号消息(微信登录也经由该接口)*/
@PostMapping("/callback")
@ResponseBody
public String responseMsg(HttpServletRequest req, HttpServletResponse resp) throws IOException {req.setCharacterEncoding("UTF-8");String respContent = wxService.responseMsg(req);return respContent;
}

service

// 这个方法就是微信的登录方法
@Override
public String responseMsg(HttpServletRequest req) {if (req == null) {throw new BusinessException(StatusCode.SYSTEM_ERROR);}String message = "success";try {// 把微信返回的xml信息转义成mapMap<String, String> xmlMessage = WxMessageUtil.xmlToMap(req); // 解析微信发来的请求信息String fromUserName = xmlMessage.get("FromUserName"); // 这个就该事件的用户openIdString toUserName = xmlMessage.get("ToUserName"); // 这个开发者微信号String msgType = xmlMessage.get("MsgType"); // 消息类型(event或者text)String createTime = xmlMessage.get("CreateTime"); // 消息创建时间 (整型)log.info("发送方帐号(用户的openId)=>" + fromUserName);log.info("开发者微信号=>" + toUserName);log.info("消息类型为=>" + msgType);log.info("消息创建时间 (整型)=>" + createTime);if ("event".equals(msgType)) { // 如果是事件推送String eventType = xmlMessage.get("Event"); // 事件类型String eventKey = xmlMessage.get("EventKey"); // 获取事件KEY值if ("subscribe".equals(eventType)) { // 如果是扫描二维码后订阅消息String subscribeContent = "感谢关注";// 如果是扫码登录二维码后订阅公众号,则获取该用户信息进行登录操作if (!StringUtils.isAnyBlank(eventKey)&& WxConstant.LOGIN_QR_ID.toString().equals(eventKey.split("_")[1])) {subscribeContent = dealWithWxLoginUser(fromUserName, subscribeContent);}String subscribeReturnXml = WxMessageUtil.getWxReturnMsg(xmlMessage, subscribeContent);return subscribeReturnXml;}if ("SCAN".equals(eventType)) { // 如果是扫码消息String scanContent = "扫码成功";// 如果是扫描登录二维码,则获取该用户信息进行登录操作if (!StringUtils.isAnyBlank(eventKey)&& WxConstant.LOGIN_QR_ID.toString().equals(eventKey)) {scanContent = dealWithWxLoginUser(fromUserName, scanContent);}String scanReturnXml = WxMessageUtil.getWxReturnMsg(xmlMessage, scanContent);return scanReturnXml;}}if ("text".equals(msgType)) { // 如果是文本消息推送String content = xmlMessage.get("Content"); // 接收到的消息内容String textReturnXml = WxMessageUtil.getWxReturnMsg(xmlMessage, content);return textReturnXml; // 将接收到的文本消息变成xml格式再返回}} catch (IOException | DocumentException e) {throw new RuntimeException(e);}return message;
}/*** 处理微信登录的用户* @param openId 扫码登录用户的openId* @param content 处理结果* @return 处理信息*/
private String dealWithWxLoginUser(String openId, String content) {if (StringUtils.isAnyBlank(openId, content)) {throw new BusinessException(StatusCode.PARAMS_ERROR, "dealWithWxLoginUser方法参数为空");}// 1,判断fromUserName即用户openId在数据库中是否存在User loginUser = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getWechatNum, openId));if (loginUser == null) {// 如果不存在用户信息则跳转到手机号绑定页面// (此时微信登录就和这里的方法没有关系了,登录工作由下面跳转的绑定页面完成,这里链接目的之一是引导用户授权信息)// 能访问到该绑定页面只有两种情况,不符合这两种情况不能访问该页面:// 1,该openId对应用户未绑定手机号 2,该openId对应用户为空content = "[POLAR]该账号未绑定手机号,请绑定手机号后登录\n" +"<a href =\"http://内网穿透域名/api/user/wx/redirect\">[绑定手机号]</a>";} else {// 如果存在则通过该openId查询到该用户信息,生成token(存入redis)进行登录操作// 封装用户信息UserVo userVo = userUtil.setUserVo(loginUser);// 生成tokenString token = JwtUtil.createJWT(loginUser.getId().toString());// 将用户信息存入redisredisTemplate.opsForValue().set(RedisKey.LOGIN_USER + loginUser.getId(), userVo, 14, TimeUnit.DAYS);content = "用户" + userVo.getNickname() + "登录成功\n\n" +"登录日期:" + new Date();}return content;
}

这里实现的就是流程图中的第一步判断openId是否存在,主要判断方法就是dealWithWxLoginUser这个方法,该方法触发的条件就是用户扫码订阅公众号事件或者扫码登录事件(SCAN和subscribe)。

可以看到dealWithWxLoginUser方法中当用户不存在时,则需要引导用户进入手机号绑定界面,这里就是通过公众号向用户发送了手机号绑定超链接;实际效果如图:

image-20230213210702476

然后用户点击下面超链接进行手机号绑定;

如果已经绑定openId,则执行登录操作,效果如图:

image-20230213210649602


接下来就是绑定手机号步骤,绑定手机号链接到的是微信的网页授权接口,这一块在微信授权文章里说过,通过内网穿透映射到对应链接;

代码如下:

// 调用微信授权接口重定向
@GetMapping("/redirect")
@ResponseBody
public String toRedirectUrl(HttpServletResponse response) {String redirectUrl = "https://open.weixin.qq.com/connect/oauth2/authorize" +"?appid=" + WxConfigurationConstant.APP_ID +"&redirect_uri=" + WxConfigurationConstant.REDIRECT_URL +"&response_type=code" + "&scope=snsapi_userinfo" + // 只有关注公众号才能获取用户全部信息"&state=STATE" + "&connect_redirect=1#wechat_redirect";try {response.sendRedirect(redirectUrl); // 重定向url} catch (IOException e) {log.error("获取微信code失败: " + e.getMessage());}return "重定向成功";
}// 授权接口重定向回调方法
@GetMapping("/redirect/info")
public String redirectInfo(@RequestParam(value = "code") String code,@RequestParam(value = "state", required = false) String state,HttpServletResponse response,Model model) {// 获取登录用户信息WxUserInfo loginUserInfo = wxService.getWxLoginUserInfo(code);// 判断该用户是否已经绑定手机号(只有用户绑定手机号后恶意访问绑定链接才会触发),已绑定则跳转错误界面User user = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getWechatNum, loginUserInfo.getOpenid()));if (user != null) {return "BindPhoneErrorPage";}model.addAttribute("loginUserInfo", loginUserInfo);// 不响应json数据,返回一个手机号绑定界面return "BindPhonePage";
}

第一个/redirect接口就是上面超链接的url,通过该接口进行用户信息授权获取的:

image-20230213211237179

然后该接口重定向到下面的/redirect/info接口,这个接口的作用才是实际获取用户信息的接口,通过getWxLoginUserInfo方法获取扫码用户的微信信息;这里可能会有疑惑,既然/redirect/info接口是获取用户信息的接口,那么要/redirect接口干嘛,这里上面微信授权文章也讲过,目的就是一个:获取code参数,只有重定向方式才能活到到code,有了code参数才能获取用户信息;微信官方文档有写;

/redirect/info接口返回的是一个html页面,即手机号绑定页面,并把授权获取的用户信息loginUserInfo也传到了该页面,这里用的是springboot的thymleaf模板,因为我前端实在是烂,就简单写了个条条框框实现这个功能,后期再完善页面:

BindPhonePage:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>绑定手机号</title></head><body><div><br/>
<!--      <span id="hideWxUserInfo" th:text="${loginUserInfo}" hidden></span>--><input id="phoneNum" placeholder="请输入手机号" /> <br/><br/><input id="code" placeholder="请输入验证码" /><button id="sendCode">发送验证码</button><br/><br/><button id="bindClick">绑定</button></div></body><script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script><script type="text/javascript" th:inline="javascript">$("#sendCode").click(() => {// 发送ajax请求获取验证码var phone = $("#phoneNum").val();$.ajax({url:'http://内网穿透域名/api/user/sms/aliyun/code/' + phone,// data:{// },type:'post',// dataType:'json',success:function (data) {if (data.code == 20000) {alert('验证码发送成功')} else {// 提示信息alert(data.message);}}});})$("#bindClick").click(() => {// 发送手机号绑定请求var wxUserInfo = [[${loginUserInfo}]];var phone = $("#phoneNum").val();var code = $("#code").val();var allInfo = {"phone": phone,"code": code,"wxUserInfo": wxUserInfo}$.ajax({url:'http://内网穿透域名/api/user/bind/phone',data: JSON.stringify(allInfo),contentType: "application/json;charset=UTF-8",type:'post',dataType:'json',success:function (data) {if (data.code == 20000) {alert('绑定成功');// TODO 跳转失败不知道为什么window.location.href = 'BindPhoneErrorPage.html';} else {// 提示信息alert(data.description);}}});})</script>
</html>

可以看到绑定按钮点击后会发送一个绑定请求:http://内网穿透域名/api/user/bind/phone,这个接口主要完成流程图的注册或添加openId功能,代码如下:

@PostMapping("/bind/phone")
public BaseResponse<String> bindPhoneAndOpenId(@RequestBody BindPhoneAndOpenIdRequest bindRequest) {if (bindRequest == null|| StringUtils.isAnyBlank(bindRequest.getPhone(), bindRequest.getCode())|| bindRequest.getWxUserInfo() == null) {throw new BusinessException(StatusCode.PARAMS_ERROR);}String phone = bindRequest.getPhone();String code = bindRequest.getCode();WxUserInfo wxUserInfo = bindRequest.getWxUserInfo();userService.bindPhoneAndOpenId(phone, code, wxUserInfo);return ResultUtils.success("绑定成功");
}

service

@Transactional
@Override
public void bindPhoneAndOpenId(String phone, String code, WxUserInfo wxUserInfo) {// 参数校验if (StringUtils.isAnyBlank(phone, code) || wxUserInfo == null) {throw new BusinessException(StatusCode.PARAMS_ERROR, "参数为空");}RegExpUtil.regExpVerify(RegExpUtil.phoneRegExp, phone, "手机号格式错误");// 从redis中获取验证码进行校验String phoneCode = (String) redisTemplate.opsForValue().get(RedisKey.SMS_LOGIN_CODE + phone);if (StringUtils.isAnyBlank(phoneCode)) {throw new BusinessException(StatusCode.OPERATION_ERROR, "验证码不存在或已超时");}phoneCode = phoneCode.split("_")[0]; // 获取真正的验证码if (!code.equals(phoneCode)) {throw new BusinessException(StatusCode.OPERATION_ERROR, "验证码错误");}User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));if (user != null) { // 如果该手机号已经注册有用户,则绑定该openIduser.setWechatNum(wxUserInfo.getOpenid());user.setUpdateTime(null);userMapper.updateById(user);// 再次查询处理好的user数据user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));} else { // 如果没有注册,则获取用户微信信息,将openId和手机号绑定(用户注册流程)user = new User();user.setPhone(phone);user.setNickname(wxUserInfo.getNickname());user.setAvatar(wxUserInfo.getHeadimgurl());user.setGender(wxUserInfo.getSex());user.setWechatNum(wxUserInfo.getOpenid());user.setProfile("简单介绍一下自己吧!");userMapper.insert(user); // 新增用户// 重新获取新增用户信息user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));// 设置用户为普通用户Role normalRole = roleService.getOne(new LambdaQueryWrapper<Role>().eq(Role::getRoleName, "普通用户"));UserRoleRelation userRoleRelation = new UserRoleRelation();userRoleRelation.setUserId(user.getId());userRoleRelation.setRoleId(normalRole.getId());userRoleRelationService.save(userRoleRelation);}// 封装用户信息UserVo userVo = userUtil.setUserVo(user);// 生成tokenString token = JwtUtil.createJWT(user.getId().toString());// 将用户信息存入redisredisTemplate.opsForValue().set(RedisKey.LOGIN_USER + user.getId(), userVo, 14, TimeUnit.DAYS);// 登录成功后将验证码清除redisTemplate.delete(RedisKey.SMS_LOGIN_CODE + phone);
}

这里就是对应流程图的如下步骤:

image-20230213212511626

前面一大堆就是参数校验和验证码校验,后面if~else才是核心;

其中跳转BindPhoneErrorPage.html失败,没有找到原因,后期完善了补充;

BindPhoneErrorPage页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>ERROR</title>
</head><body><h1>绑定手机号成功</h1></body>
</html>

效果如下:

用户点击绑定手机号会有如下页面进行操作:

image-20230213214548462

获取验证码后点击绑定即可:

image-20230213220811635

image-20230213220842914

绑定成功后查看数据库可以看到完整用户数据,即phone和openId字段都有(我这里openId字段是wechat_num):

image-20230213212839267

第一条openId为空的数据就是没有使用过微信登录只通过手机号验证码登录的用户;第二条就是绑定好的数据;

查看redis:

image-20230213213114940

至此微信登录大致就完成了;

总结

其实我这里实现的仅仅是后端部分,前端如何判断用户是否扫码,如何获取用户登录的token都是待解决的问题,这里我可以提供两个思路,前端可以通过和后端长连接websocket通信进行判断用户是否扫码从而进一行下一步操作;或者是很多网站都是用的前端通过轮询的方式定时向后端发送请求查看后端是否登录完成;

以上思路和代码仅仅是我个人想法,稳定性和效率上没有经过测试,希望能給你提供一个思路,如果有问题请指点一二;

相关文章:

微信公众号扫码授权登录思路

引言 上学期研究了一下微信登录相关内容&#xff0c;也写了两三篇笔记&#xff0c;但是最后实际登录流程没有写&#xff0c;主要因为感觉功能完成有所欠缺&#xff0c;一直也没有好的思路&#xff1b;这两天我又看了看官方文档&#xff0c;重新构思了一下微信公众号登录相关的…...

数据结构与算法基础-学习-10-线性表之顺序栈的清理、销毁、压栈、弹栈

一、函数实现顺序栈的其他函数实现&#xff0c;请看之前的博客链接《数据结构与算法基础-学习-09-线性表之栈的理解、初始化顺序栈、判断顺序栈空、获取顺序栈长度的实现》。1、ClearSqStack&#xff08;1&#xff09;用途清理栈的空间。只需要栈顶指针和栈底指针相等&#xff…...

Hazel游戏引擎(005)

本人菜鸟&#xff0c;文中若有代码、术语等错误&#xff0c;欢迎指正 我写的项目地址&#xff1a;https://github.com/liujianjie/GameEngineLightWeight&#xff08;中文的注释适合中国人的你&#xff09; 文章目录前言关键操作代码文件关键代码代码流程代码文件关键代码exter…...

牛客网Python篇数据分析习题(四)

1.现有一个Nowcoder.csv文件&#xff0c;它记录了牛客网的部分用户数据&#xff0c;包含如下字段&#xff08;字段与字段之间以逗号间隔&#xff09;&#xff1a; Nowcoder_ID&#xff1a;用户ID Level&#xff1a;等级 Achievement_value&#xff1a;成就值 Num_of_exercise&a…...

盲盒如何创业?

所谓的“盲盒”&#xff0c;受众群体大部分是那些爱碰运气的人&#xff0c;顾客买的是那种在打开盲盒时一刹那的惊喜感和神秘感&#xff0c;在打开盲盒之前&#xff0c;谁也不知道自己会得到什么&#xff0c;这也是为什么消费者更愿意购买的原因。网上的盲盒&#xff0c;主要是…...

第1集丨Java中面向对象相关概念汇总

目录一、基本概念1.1 类1.2 属性1.3 方法1.4 静态1.5 包1.6 import二、高级概念2.1 构造方法2.2 继承2.3 super & this2.4 多态2.5 方法重载2.6 方法重写2.7 访问权限2.8 内部类2.9 final2.10 抽象2.11 接口2.12 匿名类面向对象的编程思想力图使计算机语言中对事物的描述与…...

高性能(二)

三、读写分离和分库分表 1.读写分离 1.1 概述 将数据库的读写操作分散到不同的数据库节点上 通常一主多从一台主数据库负责写&#xff0c;多台从数据库负责读。 主库和从库之间会进行数据同步&#xff0c;以保证从库中数据的准确性。 1.2 问题及解决 1.2.1 问题 主从同…...

Allegro如何实现同一个屏幕界面分屏显示操作指导

Allegro如何实现同一个屏幕界面分屏显示操作指导 在做PCB设计的时候,会需要分屏显示,比如一边是放大的视图,另外一边是缩小的视图,Allegro支持同一个屏幕界面下进行分屏显示,如下图 而且会实时同步起来 如何分屏,具体操作如下 点击View...

前后端一些下载与配置(第二篇 第10天过后)nuxt banner redis 短信服务

NUXT 应该是不用怎么装&#xff1f; 有现成的 axios 还需要在npm吗 好像已经有现成的了 banner banner 笔记汇总P396 Redis Linux安装redis tar -xzvf redis-6.2.6.tar.gz cd redis-6.2.6 照着他做 然后 cd /usr/local/redis/bin ./redis-server /usr/local/redis…...

OSG三维渲染引擎编程学习之四十八:“第五章:OSG场景渲染” 之 “5.6 多重纹理映射”

目录 第五章 OSG场景渲染 5.6 多重纹理映射 5.6.1 多重纹理映射介绍 5.6.2 多重纹理映射示例...

对Node.js 的理解?优缺点?应用场景?

一、是什么 Node.js 是一个开源与跨平台的 JavaScript 运行时环境 在浏览器外运行 V8 JavaScript 引擎&#xff08;Google Chrome 的内核&#xff09;&#xff0c;利用事件驱动、非阻塞和异步输入输出模型等技术提高性能 可以理解为 Node.js 就是一个服务器端的、非阻塞式I/…...

Bean的生命周期

所谓的生命周期指的是一个对象从诞生到销毁的整个生命过程&#xff0c;我们把这个过程就叫做一个对象的生命周期~~ Bean的生命周期分为以下五大部分&#xff1a; 实例化&#xff08;为 Bean 分配内存空间&#xff09; 设置属性&#xff08;Bean对象注入/装配&#xff09; 初…...

Python学习-----函数2.0(函数对象,名称空间,作用域-->全局变量与局部变量)

目录 前言&#xff1a; 1.函数对象 &#xff08;1&#xff09;函数对象的引用 &#xff08;2&#xff09;函数可以放到序列里面 &#xff08;3&#xff09;函数可以作为参数 &#xff0c; 传递给另一个函数 2.名称空间 3.作用域 &#xff08;1&#xff09;作用域的理解 …...

Java中Json字符串和Java对象的互转

JSON&#xff08;JavaScript Object Notation&#xff09;是一种轻量级的数据交换格式。诞生于 2002 年。易于人阅读和编写。同时也易于机器解析和生成。JSON 是目前主流的前后端数据传输方式。 JSON 采用完全独立于语言的文本格式&#xff0c;但是也使用了类似于 C 语言家族的…...

代码随想录NO42 | 动态规划_Leetcode70. 爬楼梯 (进阶) 322. 零钱兑换 279.完全平方数

动态规划_Leetcode70. 爬楼梯 &#xff08;进阶&#xff09; 322. 零钱兑换 279.完全平方数70. 爬楼梯 &#xff08;进阶&#xff09; 在原题基础上&#xff0c;改为&#xff1a;一步一个台阶&#xff0c;两个台阶&#xff0c;三个台阶&#xff0c;…&#xff0c;直到 m个台阶…...

【C++从入门到放弃】初识C++(基础知识入门详解)

&#x1f9d1;‍&#x1f4bb;作者&#xff1a; 情话0.0 &#x1f4dd;专栏&#xff1a;《C从入门到放弃》 &#x1f466;个人简介&#xff1a;一名双非编程菜鸟&#xff0c;在这里分享自己的编程学习笔记&#xff0c;欢迎大家的指正与点赞&#xff0c;谢谢&#xff01; C基础…...

企业工程项目管理系统源码+spring cloud 系统管理+java 系统设置+二次开发

工程项目各模块及其功能点清单 一、系统管理 1、数据字典&#xff1a;实现对数据字典标签的增删改查操作 2、编码管理&#xff1a;实现对系统编码的增删改查操作 3、用户管理&#xff1a;管理和查看用户角色 4、菜单管理&#xff1a;实现对系统菜单的增删改查操…...

【GPLT 三阶题目集】L3-016 二叉搜索树的结构

二叉搜索树或者是一棵空树&#xff0c;或者是具有下列性质的二叉树&#xff1a; 若它的左子树不空&#xff0c;则左子树上所有结点的值均小于它的根结点的值&#xff1b;若它的右子树不空&#xff0c;则右子树上所有结点的值均大于它的根结点的值&#xff1b;它的左、右子树也分…...

核心交换机安全多业务高性能万兆交换机

RG-S5750-24SFP/12GT交换机是锐捷网络推出的融合了高性能、高安全、多业务的新一代三层交换机。RG-S5750-24SFP/12GT 交换机能够提供灵活的介质接口&#xff0c;满足网络建设中不同介质的连接需要。全千兆的端口形态&#xff0c;加上可扩展的高密度万兆端口&#xff0c;提供1&a…...

Android APK 签名打包原理分析(三)【静默安装的实现方案】

背景 小编目前从事的系统定制类工作,有客户提出了,需要后台“静默安装”他们的app,也就是悄无声息的安装,而且特别强调,不可以跳出任何安装引导页面,他们的app下载完成之后,后台调用公开的android install代码,系统就后台完成安装,安装完成之后,重新打开应用就可以。…...

Vue记事本应用实现教程

文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展&#xff1a;显示创建时间8. 功能扩展&#xff1a;记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...

Docker 运行 Kafka 带 SASL 认证教程

Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明&#xff1a;server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...

STM32F4基本定时器使用和原理详解

STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...

将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?

Otsu 是一种自动阈值化方法&#xff0c;用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理&#xff0c;能够自动确定一个阈值&#xff0c;将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...

智能分布式爬虫的数据处理流水线优化:基于深度强化学习的数据质量控制

在数字化浪潮席卷全球的今天&#xff0c;数据已成为企业和研究机构的核心资产。智能分布式爬虫作为高效的数据采集工具&#xff0c;在大规模数据获取中发挥着关键作用。然而&#xff0c;传统的数据处理流水线在面对复杂多变的网络环境和海量异构数据时&#xff0c;常出现数据质…...

#Uniapp篇:chrome调试unapp适配

chrome调试设备----使用Android模拟机开发调试移动端页面 Chrome://inspect/#devices MuMu模拟器Edge浏览器&#xff1a;Android原生APP嵌入的H5页面元素定位 chrome://inspect/#devices uniapp单位适配 根路径下 postcss.config.js 需要装这些插件 “postcss”: “^8.5.…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...

【笔记】WSL 中 Rust 安装与测试完整记录

#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统&#xff1a;Ubuntu 24.04 LTS (WSL2)架构&#xff1a;x86_64 (GNU/Linux)Rust 版本&#xff1a;rustc 1.87.0 (2025-05-09)Cargo 版本&#xff1a;cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...

掌握 HTTP 请求:理解 cURL GET 语法

cURL 是一个强大的命令行工具&#xff0c;用于发送 HTTP 请求和与 Web 服务器交互。在 Web 开发和测试中&#xff0c;cURL 经常用于发送 GET 请求来获取服务器资源。本文将详细介绍 cURL GET 请求的语法和使用方法。 一、cURL 基本概念 cURL 是 "Client URL" 的缩写…...

Python竞赛环境搭建全攻略

Python环境搭建竞赛技术文章大纲 竞赛背景与意义 竞赛的目的与价值Python在竞赛中的应用场景环境搭建对竞赛效率的影响 竞赛环境需求分析 常见竞赛类型&#xff08;算法、数据分析、机器学习等&#xff09;不同竞赛对Python版本及库的要求硬件与操作系统的兼容性问题 Pyth…...