为什么要使用微信公众号扫码登录?
-
便捷性: 扫码登录省去了用户输入账号和密码的步骤,使登录过程更加简便和快捷。用户只需通过微信扫一扫功能即可完成登录,无需记忆或输入复杂的登录信息。
-
安全性: 使用微信公众号扫码登录可以提高账户安全性。由于无需输入密码,就减少了密码泄露的风险,避免了因为用户使用弱密码或者遭受钓鱼攻击导致的账户安全问题。
-
统一身份认证: 微信公众号扫码登录可以实现统一身份认证,用户在不同的应用或网站中都可以使用微信账号进行登录。这种方式使得用户只需维护一个账户,提高了用户体验和管理的便捷性。
-
用户信任: 微信作为一个广泛使用的社交平台,用户对其有较高的信任度。通过微信扫码登录,用户可能更愿意信任与微信关联的应用或网站,从而提高用户对服务的信任感。
-
社交分享: 扫码登录后,应用或网站可以获取用户在微信上的一些基本信息,例如头像、昵称等,这些信息可以用于个性化用户体验,同时也可以鼓励用户在应用或网站上进行社交分享,增加用户粘性。
1.关联微信公众号时,会填写配置信息
如上图,weCat.do接口需要放开,另外该接口get请求用于校验配置信息,
/**
* 公众号验证token--验证token
* 在公众号网页中添加/修改 配置服务器信息时,微信平台会验证echostr值是否正确(token校验)
*
* @param signature
* @param timestamp
* @param nonce
* @param echostr
* @return
*/
@GetMapping(value = "/weCat.do", produces = {"application/json;charset=UTF-8"})
public void validationWeCat(@RequestParam String signature,
@RequestParam String timestamp,
@RequestParam String nonce,
@RequestParam String echostr,
HttpServletResponse response) throws IOException {
final boolean b = gzhUtiles.checkQianMing(signature, timestamp, nonce);
if (b) {
response.getOutputStream().println(echostr);
} else {
System.out.println("token验证失败");
}
}
格式校验代码为:
/**
* 公众号验证token--验证签名
*
* @param signature
* @param timestamp
* @param nonce
* @return
*/
public boolean checkQianMing(String signature, String timestamp, String nonce) {
String[] tmpArr = {TOKEN, timestamp, nonce};
Arrays.sort(tmpArr);
StringBuilder tmpStrBuilder = new StringBuilder();
for (String str : tmpArr) {
tmpStrBuilder.append(str);
}
String tmpStr = tmpStrBuilder.toString();
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(tmpStr.getBytes());
StringBuilder hexStrBuilder = new StringBuilder();
for (byte b : digest) {
hexStrBuilder.append(String.format("%02x", b));
}
String calculatedSignature = hexStrBuilder.toString();
return calculatedSignature.equals(signature);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return false;
}
}
其余post请求接受接受微信一系列相关事件推送:
/**
* 接收公众号事件用户事件 订阅,取消订阅
*
* @param requestBody
* @return
*/
@RequestMapping(value = "/weCat.do", method = RequestMethod.POST,produces = {"application/json;charset=UTF-8"})
public void validationWeCat(@RequestBody String requestBody, HttpServletResponse response) throws IOException {
wechatService.validationWeCat(requestBody);
response.getOutputStream().println("success");
}
此处的String requestBody,就是微信想你发送的xml,需要解析
解析示例为:
/**
* 解析公众号用户触发响应的xml
*
* @param requestBody
* @return
*/
public static XmlData jxXml(String requestBody) {
String unescapedXml = StringEscapeUtils.unescapeHtml4(requestBody);
try {
// 为XmlData类创建JAXB上下文
JAXBContext jaxbContext = JAXBContext.newInstance(XmlData.class);
// 创建反序列化器
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
// 从XML反序列化为Java对象
XmlData xmlData = (XmlData) unmarshaller.unmarshal(new StringReader(unescapedXml));
System.out.println(xmlData.toString());
return xmlData;
} catch (JAXBException e) {
e.printStackTrace();
}
return null;
}
原本xml的格式为:关注/取消关注事件 | 微信开放文档
根据解析的内容进行相应逻辑处理
例如:
@Override
public void validationWeCat(String requestBody) {
XmlData xmlData = GzhUtiles.jxXml(requestBody);
Objects.requireNonNull(xmlData, "xml格式校验失败-null");
//TODO:先将用户解析出来
Objects.requireNonNull(xmlData.getEvent(), "xml格式校验失败-eventTypeNull");
Objects.requireNonNull(xmlData.getFromUserName(), "xml格式校验失败-eventTypeNull");
//事件类型
String eventType = xmlData.getEvent();
//用户的openId
String openId = xmlData.getFromUserName();
//用户的事件类型
String eventKey = xmlData.getEventKey();
//todo.1 在公众号点击关注不在进行用户的注册登录,仅仅在用户进行官网扫码时间进行用户的相关操作
//登录注册
if ((EventType.Login.getValue().equals(eventType) && "login".equals(eventKey)) || (EventType.SUBSCRIBE.getValue().equals(eventType) && "qrscene_login".equals(eventKey))) {//用户已关注扫描临时二维码或未关注扫描临时二维码进行关注
String ticket = xmlData.getTicket();
login(ticket, openId);
}
//点击关注事件,不做处理
// else if (EventType.SUBSCRIBE.getValue().equals(eventType)) {
//
// }
//取消关注事件,不做处理或者修改用户账号状态,过期30天不关注注销账号
// else if (EventType.UNSUBSCRIBE.getValue().equals(eventType)) {
//
// }
}
如何实现扫码登录:
1.获取临时二维码或者永久二维码(建议使用临时二维码,量大管饱)
/**
* 获取微信二维码
*
* @return /**
* 获取微信二维码信息
* @return 包含二维码 ticket 和 codeUrl 的 Map
*/
@Override
public Map<String, String> getQRCode() {
try {
// 获取公众号 AccessToken
String gzhAccessToken = gzhUtiles.getToken();
// 构建请求URL
String createQRCodeUrl = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + gzhAccessToken;
// 构建二维码数据
String jsonData = "{\"expire_seconds\": 600, \"action_name\": \"QR_STR_SCENE\", \"action_info\": {\"scene\": {\"scene_str\": \"login\"}}}";
// 发起POST请求获取二维码 ticket
Map<String, Object> ticketMap = httpUtil.doPost(createQRCodeUrl, jsonData, 600);
String ticket = String.valueOf(ticketMap.get("ticket"));
if (ticket != null) {
// 构建二维码显示URL
String codeUrl = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket;
HashMap<String, String> wxMap = new HashMap<>();
wxMap.put("uuid", ticket);
wxMap.put("codeUrl", codeUrl);
return wxMap;
}
} catch (Exception e) {
// 发生异常时抛出自定义异常
throw new WechatQrCodeException();
}
// 默认返回空 Map
return Collections.emptyMap();
}
其中,调用微信相应接口createQRCodeUrl,构建二维码参数,
最后构建的codeUrl交给前端展示,形成网页二维码,在调用该接口时间,会存在相应工具类
@Component
public class GzhUtiles {
@Autowired
private RedisCache redisCache;
private final Environment env;
private static String AppSecret;
private static String appid;
private static String TOKEN;
@Autowired
public GzhUtiles(Environment env) {
this.env = env;
AppSecret = env.getProperty("ruoyi.AppSecret");
appid = env.getProperty("ruoyi.appid");
TOKEN = env.getProperty("ruoyi.gzhTooken");
}
/**
* 公众号验证token--验证签名
*
* @param signature
* @param timestamp
* @param nonce
* @return
*/
public boolean checkQianMing(String signature, String timestamp, String nonce) {
String[] tmpArr = {TOKEN, timestamp, nonce};
Arrays.sort(tmpArr);
StringBuilder tmpStrBuilder = new StringBuilder();
for (String str : tmpArr) {
tmpStrBuilder.append(str);
}
String tmpStr = tmpStrBuilder.toString();
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(tmpStr.getBytes());
StringBuilder hexStrBuilder = new StringBuilder();
for (byte b : digest) {
hexStrBuilder.append(String.format("%02x", b));
}
String calculatedSignature = hexStrBuilder.toString();
return calculatedSignature.equals(signature);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return false;
}
}
/**
* 获取微信公众号的AccessToken
*
* @return
*/
public String getToken() {
// 判断数据是否存在
Boolean gzhAccessToken = redisCache.hasKey("gzh_access_token");
//存入缓存 存在两小时
//三秒
if (!gzhAccessToken) {
// 授予形式
System.out.println("第一次进入");
String grant_type = "client_credential";
// 接口地址拼接参数(appid为微信服务号的appid,secret为服务号的秘钥)
String getTokenApi = "https://api.weixin.qq.com/cgi-bin/token?grant_type=" + grant_type + "&appid=" + appid
+ "&secret=" + AppSecret;
String tokenJsonStr = doGetPost(getTokenApi, "GET", null);
JSONObject tokenJson = JSONObject.parseObject(tokenJsonStr);
String token = tokenJson.get("access_token").toString();
// 存入缓存 120分钟 重新获取
// 存储数据并设置过期时间为 120 分钟
redisCache.setCacheObject("gzh_access_token", token, 2, TimeUnit.HOURS);
return token;
} else {
// 获取数据
return redisCache.getCacheObject("gzh_access_token");
}
}
/**
* 调用接口 post
*
* @param apiPath
*/
public static String doGetPost(String apiPath, String type, Map<String, Object> paramMap) {
OutputStreamWriter out = null;
InputStream is = null;
String result = null;
try {
URL url = new URL(apiPath);// 创建连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setUseCaches(false);
connection.setInstanceFollowRedirects(true);
connection.setRequestMethod(type); // 设置请求方式
connection.setRequestProperty("Accept", "application/json"); // 设置接收数据的格式
connection.setRequestProperty("Content-Type", "application/json"); // 设置发送数据的格式
connection.connect();
if (type.equals("POST")) {
out = new OutputStreamWriter(connection.getOutputStream(), "UTF-8"); // utf-8编码
out.append(JSON.toJSONString(paramMap));
out.flush();
out.close();
}
// 读取响应
is = connection.getInputStream();
int length = (int) connection.getContentLength();// 获取长度
if (length != -1) {
byte[] data = new byte[length];
byte[] temp = new byte[512];
int readLen = 0;
int destPos = 0;
while ((readLen = is.read(temp)) > 0) {
System.arraycopy(temp, 0, data, destPos, readLen);
destPos += readLen;
}
result = new String(data, "UTF-8"); // utf-8编码
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
/**
* 解析公众号用户触发响应的xml
*
* @param requestBody
* @return
*/
public static XmlData jxXml(String requestBody) {
String unescapedXml = StringEscapeUtils.unescapeHtml4(requestBody);
try {
// 为XmlData类创建JAXB上下文
JAXBContext jaxbContext = JAXBContext.newInstance(XmlData.class);
// 创建反序列化器
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
// 从XML反序列化为Java对象
XmlData xmlData = (XmlData) unmarshaller.unmarshal(new StringReader(unescapedXml));
System.out.println(xmlData.toString());
return xmlData;
} catch (JAXBException e) {
e.printStackTrace();
}
return null;
}
}
其中:getToken,类似于访问微信的钥匙,该钥匙获取之后会存在两个小时,调用次数是存在限制的,最好保存到缓存。
当用户扫描二维码后,会存在相应的事件推送到weCat.do接口,解析后处理相应事件,具体逻辑可自行处理