一、需求:
网站需要接入微信扫码登录,但此网站仅能在内网环境下访问,仅网站服务器可以连接微信外网
二、遇到的问题:
1、图片需要联网:
-
参考网页:微信网页扫码登录
按照上述网站上的指南接入,在可访问外网的情况下可以使用,但是由于二维码的图片是需要浏览器从微信的服务器中获取的,在内网情况下无法拿到图片 -
解决方案:可以将二维码图片爬取过来,放入登录页面的标签中
首先访问网站:
https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect通过分析网页源代码可以得到二维码图片是放在一个img标签中,图片链接为一个随机uuid,因此通过正则匹配到地址后再访问该图片链接可以抓取到图片,随后可以通过服务器将图片写回到登陆页面的中:
后台获取图片代码:
@RequestMapping(value = "getQrCode")
public void getQrCode(HttpServletResponse response, HttpServletRequest request) throws IOException
{
try {
byte[] image = userService.getQrCode(request); //将网页上的图片转成btye数组
response.setContentType("image/jpeg");
response.getOutputStream().write(image);
response.addHeader("Content-Disposition","attachment;filename=image.jpg");
}catch (Exception e) {
e.printStackTrace();
}
}
getQrCode:
先访问https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
获取到图片链接之后,再访问图片链接codeUrl进行访问
ResponseEntity<byte[]> imgEntity = restTemplate.getForEntity(codeUrl, byte[].class);
前端img标签的src填我们封装好的转接接口名称即可显示二维码
2、:获取到图片之后,扫码访问无法与前台交互
-
拿到了图片,发现也可以扫,手机微信也能正常识别二维码,但是点了确认登录之后,页面没有跳转到登陆页面,也没有任何反应。原因是原本微信提供了一个页面,该页面上有一些js函数,进行了一些判断(如是否扫码、取消还是成功、执行页面跳转并带上code参数进行下一步验证),由于我们只拿了图片,切断了这些步骤,自然无反应
-
解决方案:分析了微信网页的js,发现有一个定时函数,该函数会定期去请求一个接口,该接口的uuid参数正是之前访问二维码生成的那个uuid
如图所示:/connect/l/qrconnect?uuid=071x6Yhr0dWpH
随后查看该接口的返回内容:
可以发现该接口返回的正是微信扫码登录最关键的code和一个错误码
随后分析微信网页上的js:
<script>
!function() {
function a(a) {
var b = document.location.search || document.location.hash;
if (b) {
if (/\?/.test(b) && (b = b.split("?")[1]),
null == a)
return decodeURIComponent(b);
for (var c = b.split("&"), d = 0; d < c.length; d++)
if (c[d].substring(0, c[d].indexOf("=")) == a)
return decodeURIComponent(c[d].substring(c[d].indexOf("=") + 1))
}
return ""
}
function b(a) {
jQuery.ajax({
type: "GET",
url: p + "/connect/l/qrconnect?uuid=071x6Yhr0dWpHa1T" + (a ? "&last=" + a : ""),
dataType: "script",
cache: !1,
timeout: 6e4,
success: function(a, e, f) {
var g = window.wx_errcode;
switch (g) {
case 405:
var h = "http://172.17.250.142/jkpt/loadByWx";
h = h.replace(/&/g, "&"),
h += (h.indexOf("?") > -1 ? "&" : "?") + "code=" + wx_code + "&state=";
var i = c("self_redirect");
if (d)
if ("true" !== i && "false" !== i)
try {
document.domain = "qq.com";
var j = window.top.location.host.toLowerCase();
j && (window.location = h)
} catch (k) {
window.top.location = h
}
else if ("true" === i)
try {
window.location = h
} catch (k) {
window.top.location = h
}
else
window.top.location = h;
else
window.location = h;
break;
case 404:
jQuery(".js_status").hide(),
jQuery(".js_qr_img").hide(),
jQuery(".js_wx_after_scan").show(),
setTimeout(b, 100, g);
break;
case 403:
jQuery(".js_status").hide(),
jQuery(".js_qr_img").hide(),
jQuery(".js_wx_after_cancel").show(),
setTimeout(b, 2e3, g);
break;
case 402:
case 500:
window.location.reload();
break;
case 408:
setTimeout(b, 2e3)
}
},
error: function(a, c, d) {
var e = window.wx_errcode;
408 == e ? setTimeout(b, 5e3) : setTimeout(b, 5e3, e)
}
})
}
function c(a, b) {
b || (b = window.location.href),
a = a.replace(/[\[\]]/g, "\\$&");
var c = new RegExp("[?&]" + a + "(=([^&#]*)|&|#|$)")
, d = c.exec(b);
return d ? d[2] ? decodeURIComponent(d[2].replace(/\+/g, " ")) : "" : null
}
var d = window.top != window;
if (!d) {
document.getElementsByClassName || (document.getElementsByClassName = function(a) {
for (var b = [], c = new RegExp("(^| )" + a + "( |$)"), d = document.getElementsByTagName("*"), e = 0, f = d.length; f > e; e++)
c.test(d[e].className) && b.push(d[e]);
return b
}
);
for (var e = document.getElementsByClassName("status"), f = 0, g = e.length; g > f; ++f) {
var h = e[f];
h.className = h.className + " normal"
}
}
var i = parseInt(a("styletype"), 10)
, j = parseInt(a("sizetype"), 10)
, k = a("bgcolor")
, l = NaN;
if (1 !== i && 0 !== i && 1 === l && (i = 0),
1 === i)
d ? document.body.className = document.body.className + " redesign-style_iframe" + (1 === j ? " redesign-style_iframe-small" : "") : document.body.className = document.body.className + "redesign-style_page",
k && (document.body.style.backgroundColor = k),
jQuery(".new-template").show();
else {
if (d) {
var m = "";
"white" != m && (document.body.style.color = "#373737")
} else
document.body.style.backgroundColor = "#333333",
document.body.style.padding = "50px";
if (jQuery(".old-template").show(),
0 !== i) {
var n = "";
if (n) {
var o = document.createElement("link");
o.rel = "stylesheet",
o.href = n.replace(new RegExp("javascript:","gi"), ""),
document.getElementsByTagName("head")[0].appendChild(o)
}
}
}
var p = window.usenewdomain ? "https://lp.open.weixin.qq.com" : "https://long.open.weixin.qq.com";
setTimeout(b, 100)
}();
</script>
关键代码段:
可以发现不同状态码有不同的执行逻辑:
405时会将code加入到ridirect_url中然后进行跳转
404表示已经扫描
403表示用户扫描然后按了取消
408则是初始状态,表示无操作
因此解决方案就是将这个接口也经由网站服务器进行一层转封,每次前端轮询这个接口,查询扫码状态,每次生成二维码图片时,也将uuid存入到session当中
代码:
/**
* 请求是否已经扫码
* @param response
* @param request
* @throws IOException
*/
@RequestMapping(value = "getQrCodeResult")
public @ResponseBody String getQrCodeResult(HttpServletResponse response, HttpServletRequest request,String last) throws IOException
{
try {
Object uuid=request.getSession().getAttribute("codeUUID");
if(uuid==null){
return "window.wx_errcode=408;window.wx_code='';";
}
return userService.getQrCodeResult(request,last);//否则给接口发送Get请求,获取最新的状态和code信息
}catch (org.springframework.web.client.ResourceAccessException e1){
} catch(Exception e) {
e.printStackTrace();
}
return "window.wx_errcode=408;window.wx_code='';";
}
这里注意到有个last参数,记得要带上,表示上一次的状态码,猜测微信在这里做了处理 可以减少请求的次数。随后将上述js代码放到登录页面中,一进到页面就启动接口定时任务 进行轮询,如果用户扫码或者取消扫码,通过此接口可以更新相应的状态
3、:/connect/l/qrconnect?uuid=071x6Yhr0dWp接口缓慢,页面状态更新不及时
-
描述:由于我们是做了接口转发,因此状态更新有一些延时,这个需要权衡一下调用频率和轮询时间。此接口怀疑微信做了处理,当用户未进行操作时,此接口返回的状态码为408,此时从请求到结束大概需要10+秒的时间,而当用户扫了码之后,状态码变成了404,此时这个接口请求会变快,大概200ms左右。由于是通过定时器轮询,微信好像做了频率限制,因此当状态码变成404时,由于速度很快,此时我们定时轮询容易造成刷屏,此时状态码会变成666,而原生Js中没有处理此状态码的操作。此外,发现当ctrl+F5强刷网页时,定时函数好像偶尔不执行,导致扫码状态无法更新和跳转登录。
-
解决问题:
针对状态码变成了404,此时这个接口请求会变快造成刷屏的问题,可以加上last参数,加入之后,该请求会挂起直到用户有下一步操作,可以减少刷屏
针对666状态码 不放心可以加入一个处理分支,当遇到666时提示用户刷新二维码,然后启动新一轮计时
针对ctrl+F5强刷网页时,定时函数好像偶尔不执行,导致扫码状态无法更新的问题 目前我自己的解决方案是后台访问该接口时,加入超时时间,当超过一定时间直接返回给前台,因为怀疑就是该接口一开始需要十几秒的访问时间造成的
三、其他:
1如果用了spring security进行管理的话,上述接口都不要屏蔽,否则就无法获取结果了(被拦截了)
2、绕过spring security,后台登录:
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails((HttpServletRequest) request));
SecurityContextHolder.getContext().setAuthentication(authentication);
request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,SecurityContextHolder.getContext());
3、本文仅用于自己记录学习用,如需要转载,请注明出处。如有错漏,欢迎指正。