微信二维码登录有两种方式
1.使用微信开放平台的二维码登录(对接微信)
2.使用微信公众平台的二维码登录(对接公众号)本文章的内容
准备:
首先准备一个公众号测试公众号,这里不做介绍
要有内网穿透的域名,域名填写不填协议 例如: www.baidu.com
要设置推送消息的url接口和token,目的:之前关注过公众号或者扫码后关注公众号,微信会调用我们这里填写的url接口(像回调,但微信文档说的是推送消息,很多事件都会用到这个接口)
代码
1.需要的依赖
糊涂工具类
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.8</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
2.生成二维码的代码
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.XmlUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/wx")
public class WxControllerTest{
@Autowired
private RedisTemplate redisTemplate;
private static final String token_name = "weixin_access_token";
private static final String informUrl = "https://api.weixin.qq.com/cgi-bin/user/info"; //获取用户信息
private static final String TokenUrl = "https://api.weixin.qq.com/cgi-bin/token"; //获取token
private static final String QR_CODE = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=";//获取二维码url和ticket
private static final String GET_QR_CODE = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=";//通过ticket换取二维码
private static final String appID = ""; //todo 你自己的appid
private static final String appsecret = ""; //todo 你自己的appsecret
@GetMapping("/qrCode")
public Map<String, Object> getQRCode() throws UnsupportedEncodingException {
String wxToken = getWXToken();
wxToken = URLEncoder.encode(wxToken, "utf-8");
String url = QR_CODE + wxToken;
Long snowflake = IdUtil.getSnowflake(1, 1).nextId(); //不重复的随机数
/**
* expire_seconds 有效时间 s
* action_name 二维码类型,QR_SCENE为临时的整型参数值,QR_STR_SCENE为临时的字符串参数值,QR_LIMIT_SCENE为永久的整型参数值,QR_LIMIT_STR_SCENE为永久的字符串参数值
* //注意:如果为scene_str那么action_name为QR_STR_SCENE,如果传入的是scene_id那么action_name为QR_SCENE,一定要对应
* action_info 二维码详细信息
* scene_str 场景值ID(字符串形式的ID),字符串类型,长度限制为1到64
* scene_id 场景值ID,临时二维码时为32位非0整型,永久二维码时最大值为100000(目前参数只支持1--100000)
*/
String json = "{\n" +
" \"expire_seconds\": 604800,\n" +
" \"action_name\": \"QR_STR_SCENE\",\n" +
" \"action_info\": {\"scene\": {\"scene_str\": \" " + snowflake + " \"}}\n" +
"} ";
String post = HttpUtil.post(url, json);
/**
* 返回的数据map
* "ticket": 可以调用 https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET 替换TICKET为这个值和微信换取二维码
* "expire_seconds": 过期时间
* "url": 如果不想用ticket换取二维码,可以用这个url使用二维码生成工具生成二维码,和ticket作用是一样的,只不过ticket不需要借助二维码生成工具
*/
Map map = JSONObject.parseObject(post, Map.class);
map.put("eventKey",snowflake)//这个参数是让前端遍历判断是否扫码登录成功的
return map;
}
/**
* 获取公众号验证Token
*
* @return
*/
public String getWXToken() {
//从redis获取token
String token = (String) redisTemplate.boundValueOps(token_name).get();
if (StringUtils.isEmpty(token)) {
Map<String, Object> queries = new HashMap<>();
queries.put("grant_type", "client_credential");
queries.put("appid", appID);
queries.put("secret", appsecret);
String tokenString = HttpUtil.get(TokenUrl, queries, 10 * 1000);
Map<String,Object> map= JSONObject.parseObject(tokenString, Map.class);
redisTemplate.opsForValue().set(token_name, map.get("access_token"), (Integer)map.get("expires_in") - 10, TimeUnit.SECONDS);
return (String)map.get("access_token");
}
return token;
}
}
3.接收推送消息的代码,就是我们配置url和token的内容调用的接口(放入前面的controller中)
/**
* 公众号接口验证以及消息接受回复,包括微信公众号关注取消操作
*
* @param req
* @return
* @throws Exception
*/
@RequestMapping("/reply") //校验url是否可用,和推送url是同一个
@Transactional
public String get(HttpServletRequest req) throws Exception {
String method = req.getMethod();
//get做验证
if (method.equalsIgnoreCase("get")) {
//接口验证
Enumeration parameterNames = req.getParameterNames();
String signature = req.getParameter("signature");/// 微信加密签名
String timestamp = req.getParameter("timestamp");/// 时间戳
String nonce = req.getParameter("nonce"); /// 随机数
String echostr = req.getParameter("echostr"); // 随机字符串
if (this.checkSignature(signature, timestamp, nonce)) {
System.out.println("校验成功!!!!!!!!!!!!!");
return echostr;
}
return null;
} else {
req.setCharacterEncoding("UTF-8");
String message = "success";
try {
//把微信返回的xml信息转义成map
InputStream inputStream = req.getInputStream();
String xml = IoUtil.read(inputStream, StandardCharsets.UTF_8);
// 将XML转换为Map
Map<String, Object> map = XmlUtil.xmlToMap(xml);
String fromUserName = String.valueOf(map.get("FromUserName"));//消息来源用户标识openId
String toUserName = String.valueOf(map.get("ToUserName"));//消息目的用户标识
String msgType = String.valueOf(map.get("MsgType"));//消息类型
String content = String.valueOf(map.get("Content"));//消息内容
String eventKey = String.valueOf(map.get("EventKey")); //事件KEY值,qrscene_为前缀,后面为二维码的参数值
String ticket = String.valueOf(map.get("Ticket")); //二维码的ticket,可用来换取二维码图片
String eventType = String.valueOf(map.get("Event")); //时间类型
//如果为事件类型,扫码关注公众号返回的是subscribe,已经关注公众号了扫码返回SCAN
if ("event".equals(msgType)) {
if ("SCAN".equals(eventType) || "subscribe".equals(eventType)) {
//这里是你的代码逻辑
//能到这里,证明身份校验完成了,将我们登录的信息保存到redis中120s,前端遍历这个eventKey,实现登录
redisTemplate.opsForValue().set(eventKey,fromUserName,120,TimeUnit.SECONDS);
}
}
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
return message;
}
}
}
/**
* 验证签名
*
* @param signature
* @param timestamp
* @param nonce
* @return
*/
public boolean checkSignature(String signature, String timestamp, String nonce) {
String[] arr = new String[]{"weixinCore", timestamp, nonce}; //weixinCore是你定义个token,和网站上保持一致
// 将token、timestamp、nonce三个参数进行字典序排序
Arrays.sort(arr);
StringBuilder content = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
content.append(arr[i]);
}
MessageDigest md = null;
String tmpStr = null;
try {
md = MessageDigest.getInstance("SHA-1");
// 将三个参数字符串拼接成一个字符串进行sha1加密
byte[] digest = md.digest(content.toString().getBytes());
tmpStr = HexUtil.encodeHexStr(digest).toUpperCase(); //将字节数组转换为十六进制字符串,且字母必须是大写(为什么要有这一步呢)
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
// 将sha1加密后的字符串可与signature对比,标识该请求来源于微信
return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
4.前端调用的遍历接口,用来确定是否登录成功
/**
* 遍历查询是否登录成功
* @param eventKey
* @return
*/
@GetMapping("/checkLogin")
public String checkLogin(@RequestParam String eventKey){
Object value = redisTemplate.opsForValue().get(eventKey);
if (value == null){
return "";
}
String eventValue = String.valueOf(value);
//你自己的逻辑
String token = ""; //生成token等登录需要的信息
return token;
}
扩展:
如果你想要通过openId获取unionId等用户信息,可以调用下面的方法(测试号没办法获取unionId)
/**
* 获取公众号的个人信息
*
* @param openid
* @return
*/
public Map<String,Object> getInform(String openid) {
//获取token
String wxToken = getWXToken();
if (StringUtils.isEmpty(wxToken)) {
return null;
}
Map<String, Object> queries = new HashMap<>();
queries.put("access_token", wxToken);
queries.put("openid", openid);
queries.put("lang", "zh_CN");
String s = HttpUtil.get(informUrl, queries, 10 * 1000);
Map<String,Object> map = JSONObject.parseObject(s, Map.class);
return map;
}