title: 从零玩转第三方登录之WeChat公众号扫码关注登陆
date: 2022-09-27 22:46:53.362
updated: 2023-03-30 13:28:41.359
url: https://www.yby6.com/archives/wechatgzh
categories:
- 从零玩转系列
tags:
- 第三方登录
- 从零玩转系列
前言
由于看见了面试鸭的登陆方式,我也想来整一个.注意: 只能微信认证的公众号才能有二维码扫码的权限,那么我们将使用 微信的测试账户来玩转扫码(沙箱)
1. 大致流程思路:
一、用户打开网页进行登陆/注册 扫码(微信的)
二、用户扫码成功后 微信会根据我们配置的回调地址访问我们的回调并且传递某些参数
三、用户扫码成功并且进行了关注我们的公众号 微信也会访问回调 传递参数
四、++域名使用内网穿透(我这里使用花生壳)++
思路地址: 接收事件推送
在微信用户和公众号产生交互的过程中,用户的某些操作会使得微信服务器通过事件推送的形式通知到开发者在开发者中心处设置的服务器地址,从而开发者可以获取到该信息。其中,某些事件推送在发生后,是允许开发者回复用户的,某些则不允许,详细内容如下:
1、关注/取消关注事件
2、 扫描带参数二维码事件
3、上报地理位置事件
4、自定义菜单事件
5、点击菜单拉取消息时的事件推送
6、点击菜单跳转链接时的事件推送
根据上述六点我们PC端只需要 1、2点即可只是来扫码公众号并且关注后登录
2. 进入测试号页面
测试号接口配置
接口信息配置: 将会get方法来进行验签你服务器的请求 和 post来回调推送信息到服务器
参考: 接口信息配置
JS接口安全配置:我们在日常当中经常可以看见js接口安全域名。那么,js接口安全域名是什么?js接口安全域名主要用于微信公众号,如果大家要进行微信的开发,创建公众号是需要填写js接口安全域名的。当我们运用程序的时候,网络是会自动验证安全域名的,它可以解决服务器终端的语言问题,能够让访问正常的运行,只有使用好js接口安全域名,网上的用户才能够访问到网页。
参考:JS接口安全配置
3. 介绍
获取 AccessToken
用于请求微信API 需要用到的认证信息
参考: 获取AccessToken
临时二维码
- 用户扫描带场景值二维码时,可能推送以下两种事件:
如果用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者。- 如果用户已经关注公众号,在用户扫描后会自动进入会话,微信也会将带场景值扫描事件推送给开发者。
- 获取带参数的二维码的过程包括两步,首先创建二维码ticket,然后凭借 ticket 到指定 URL 换取二维码。
正确的 Json 返回结果:
{"ticket":"gQH47joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL2taZ2Z3TVRtNzJXV1Brb3ZhYmJJAAIEZ23sUwMEmm
3sUw==","expire_seconds":60,"url":"http://weixin.qq.com/q/kZgfwMTm72WWPkovabbI"}- 参考: 临时二维码
4. 代码操作
编写接口配置以便能修改接口
/***
* 微信服务器触发get请求用于检测签名-
* 如果需要绝对的安全就按照微信来进行验签
*/
@GetMapping("/weChatScanCodeCallback")
@ResponseBody
public String weChatScan(HttpServletRequest request) {
log.info("验签章:{}", request.getParameterMap());
return request.getParameter("echostr");
}
解析微信返回参数
使用DOM4J将微信返回XML格式转换一下
/**
* @Author yang shuai
* @Date 2022/9/3
*/
public class XmlUtil {
/**
* 读取xml标签内容存放map当中
*/
public static Map<String,Object> parseXML(InputStream in){
Map<String,Object> map=new HashMap<>();
try {
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(in);
Element root = document.getRootElement();
Iterator iterator = root.elementIterator();
while (iterator.hasNext()){
Element element = (Element) iterator.next();
map.put(element.getName(),element.getStringValue());
}
} catch (DocumentException e) {
e.printStackTrace();
}
return map;
}
}
接收微信回调
/**
* 接收微信推送事件
*/
@PostMapping("/weChatScanCodeCallback")
@ResponseBody
public String weChatCallback(HttpServletRequest request) {
try {
InputStream inputStream = request.getInputStream();
Map<String, Object> map = XmlUtil.parseXML(inputStream);
log.info("接收参数:{}", map);
} catch (IOException e) {
e.printStackTrace();
}
return "success";
Last
注入restTemplate请求
/**
* @Author yang shuai
* @Date 2022/9/3
* 注入restTemplate用于http请求
*/
@Configuration
public class RestTemplateConfig {
@Resource
private RestTemplateBuilder templateBuilder;
@Bean
public RestTemplate restTemplate(){
return templateBuilder.build();
}
}
生成微信二维码
/**
* @Author yang shuai
* @Date 2022/9/3
*/
public interface WeChatService {
/**
* 获取token
*
* @return
*/
String getAccessToken();
/**
* 获取生成二维码参数
*
* @return
*/
Map<String, Object> getQrCode();
}
实现
/**
* @Author yang shuai
* @Date 2022/9/3
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatServiceImpl implements WeChatService {
@Value("${weChat.gzh.appid:''}")
private String appid;
@Value("${weChat.gzh.secret:''}")
private String secret;
private final RestTemplate restTemplate;
private final RedisCache redisCacheManager;
/**
* 获取token用于操作微信接口
*/
@Override
public String getAccessToken() {
String key = "wx_access_token";
if (redisCacheManager.hashKey(key)) {
return redisCacheManager.getCacheObject(key);
}
// 获取微信扫码 token
String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appid, secret);
ResponseEntity<String> result = restTemplate.getForEntity(url, String.class);
if (result.getStatusCode() == HttpStatus.OK) {
JSONObject jsonObject = JSON.parseObject(result.getBody());
String access_token = jsonObject.getString("access_token");
Long expires_in = jsonObject.getLong("expires_in");
redisCacheManager.setCacheObject(key, access_token, expires_in, TimeUnit.SECONDS);
return access_token;
}
return null;
}
/**
* 获取微信公众号二维码
*/
@Override
public Map<String, Object> getQrCode() {
// 获取临时二维码
String url = String.format("https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s", getAccessToken());
ResponseEntity<String> result = restTemplate.postForEntity(url, "{\"expire_seconds\": 604800, \"action_name\": \"QR_STR_SCENE\", \"action_info\": {\"scene\": {\"scene_str\": \"test\"}}}", String.class);
log.info("二维码:{}", result.getBody());
JSONObject jsonObject = JSON.parseObject(result.getBody());
Map<String, Object> map = new HashMap<>();
map.put("ticket", jsonObject.getString("ticket"));
map.put("url", jsonObject.getString("url"));
return map;
}
}
5. 改造Controller
新增获取二维码
/**
* 获取二维码参数
*
* @return
*/
@GetMapping("/getQrCode")
@ResponseBody
public Object getQrCode() {
return weChatService.getQrCode();
}
6.编写前段Demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆</title>
</head>
<body>
<div style="width: 200px;margin: 50px auto">
<div id="qrcode"></div>
<div id="msg" style="display: none">
扫码成功!
</div>
</div>
<script type='text/javascript' src='http://cdn.staticfile.org/jquery/2.1.1/jquery.min.js'></script>
<script src="https://cdn.bootcss.com/jquery.qrcode/1.0/jquery.qrcode.min.js"></script>
<script>
$(function () {
let count = 0;
//获取二维码参数
$.get('https://34i33045l8.oicp.vip/weChat/getQrCode', function (res) {
//生成二维码
$('#qrcode').qrcode(res.url);
})
})
</script>
</body>
</html>
7.启动后端查看效果
1、使用idea打开html挂载一个node
2、打开前面要求设置的内网穿透用于接收微信的回调
3、进行扫码-查看后台打印参数数据
4、扫码后查看控制台
推送 XML 数据包示例:
- 用户未关注时,进行关注后的事件推送
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
<EventKey><![CDATA[qrscene_123123]]></EventKey>
<Ticket><![CDATA[TICKET]]></Ticket>
</xml>
示例:
在这里大家应该大致的知道下面的该如何实现了!
- 微信回调会一直存在
Ticket
字段 用于表示每次二维码的唯一标识
我们将它进行存储redis当中并且可以看到Event
我们利用它来区分当前是否为扫码还是关注的推送- 则前段进行段轮训来请求校验当前为什么状态?
参数说明:
参数 | 描述 |
---|---|
ToUserName | 开发者微信号 |
FromUserName | 发送方帐号(一个OpenID) |
CreateTime | 消息创建时间 (整型) |
MsgType | 消息类型,event |
Event | 事件类型,subscribe(扫码关注) or SCAN (扫码) |
EventKey | 事件 KEY 值,qrscene_为前缀,后面为二维码的参数值 |
Ticket | 二维码的ticket,可用来换取二维码图片 |
8. 改造Controller
新增短轮询检查扫码状态
/**
* 用于检测扫码和关注状态
*
* @return
*/
@PostMapping("/checkLogin")
@ResponseBody
public Object checkLogin(String ticket) {
// 存在该信息并且为关注了公众号
if (redisCache.hashKey(ticket)) {
if (!redisCache.getCacheObject(ticket).equals("subscribe")) {
return AjaxResult.error(201, "扫码成功");
}
//扫码通过则删除
redisCache.deleteObject(ticket);
return AjaxResult.success();
}
return AjaxResult.error("无动作");
}
修改微信回调完善业务
/**
* 接收微信推送事件
*
* @param request
* @return
*/
@PostMapping("/weChatScanCodeCallback")
@ResponseBody
public String weChatCallback(HttpServletRequest request) {
try {
InputStream inputStream = request.getInputStream();
Map<String, Object> map = XmlUtil.parseXML(inputStream);
log.info("接收参数:{}", map);
String userOpenId = (String) map.get("FromUserName");
String event = (String) map.get("Event");
// 自己生成的二维码不管是关注还是扫码都能取到ticket凭证,这里我使用Ticket作为每次二维码的唯一标识
String ticket = (String) map.get("Ticket");
if ("subscribe".equals(event)) {
// 根据openid判断用户是否存在,不存在则获取新增用户
// 或者根据前段传递手机号或者用户名称来进行openId绑定 看你自己的业务.
redisCache.setCacheObject(ticket, "subscribe", (long) (10 * 60), TimeUnit.SECONDS);
log.info("用户关注:{}", userOpenId);
} else if ("SCAN".equals(event)) {
redisCache.setCacheObject(ticket, "scan", (long) (10 * 60), TimeUnit.SECONDS);
log.info("用户扫码:{}", userOpenId);
}
} catch (IOException e) {
log.error("回调异常:",e);
}
return "success";
}
新增前段短轮询
替换你自己的内网穿透
$(function () {
let count = 0;
//获取二维码参数
$.get('https://34i33045l8.oicp.vip/weChat/getQrCode', function (res) {
//生成二维码
$('#qrcode').qrcode(res.url);
// 轮训获取用户扫码登陆状态
let task = setInterval(function () {
$.post('https://34i33045l8.oicp.vip/weChat/checkLogin', {ticket: res.ticket}, function ({code,msg}) {
console.log(code);
if (code === 200) { // 扫码并且关注成功
clearInterval(task)
location.href = 'http://yby6.com'
} else if (code === 201) { // 扫码成功
$("#msg").text(msg);
document.querySelector("#msg").style.display = "block"
} else {
}
count ++;
})
}, 2000)
})
})
最后操作流程
注: 前端记得整扫码超时!