再谈微信公众号网页授权的40163错误
{"errcode":40163,"errmsg":"code been used}
进来看这篇文章的你,肯定在微信公众号开发的时候看到过这行代码吧,没错,通过点击微信公众号菜单或者其他方式做oauth认证(比如微信支付估计也会要用到),都会要通过oauth认证拿到访问者的openid,以及user_info,这个code只能用一次,但是在安卓手机上,点击一次会产生两次访问,导致获取access_token失败!
2017年7月,我遇到了这个问题,当时在SegmentFault https://segmentfault.com/q/1010000010308461 里提了问,后来又跟踪了两天,也没彻底搞懂,反正时好时坏,,后来自己主要精力放在H5+开发上,就不怎么关心公众号这块了;
2018年7月,我又带着python重回公众号,又遇到了这个问题,这次绕不过去了,一年过去了,网上搜索一下,问这个问题的人还是很多,而且也没有真正解决这个问题,
比如这条:https://developers.weixin.qq.com/blogdetail?action=get_post_info&lang=zh_CN&token=&docid=b8f9f09573e92ffb0e23308d54bcdcf7
再比如:https://blog.csdn.net/hejisan/article/details/80374113
其实这个问题也不复杂,总结起来说,就是“点击公众号菜单产生两次请求”,导致code被用了两次,但是因为微信官方的文档没有相关内容,大家也都用自己的方式解决,我的解答应该也只是其中一种吧。
有人说:“后来发现只要加个属性就不会有这个问题了。
https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxx&redirect_uri=xxx&response_type=code&scope=snsapi_userinfo&state=STATE&connect_redirect=1#wechat_redirect
&connect_redirect=1 这个参数”
!很可惜,这条加上去,对我一点也不起作用!
又仔细阅读微信官方的文档 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
这里有
appid
redirect_uri
response_type
scope
state
wechat_redirect
的作用说明,但是并没有上面提到的connect_redirect,
提到了一个state的参数,这个是干嘛用的呢?
/////////////////////////////
state 否 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节
/////////////////////////////
测试的时候发现,在访问https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxx&redirect_uri=xxx&response_type=code&scope=snsapi_userinfo&state=STATE&connect_redirect=1#wechat_redirect的时候,无论state填什么,获取code的时候,这个字段会原样返回过来,好就用它来解决。
既然这个参数会被原样返回过来,那我就利用它作为一个令牌,发送之前先生成一个随机数,然后保存到一个全局变量里,在redirect_uri页面的接收到这个随机数,跟全局变量对比一下,如果一样,就把这个令牌改掉,这样第二次访问的时候,这个令牌就失效了,就pass掉这个请求,避免了被2次访问的情况。
PS.
纯php没有全局变量,用一些框架应该就会有,或者写到session或者缓存都行;
我是在django里开发的,所以就直接搞了个全局变量,简单粗暴。
我是python初学者,代码写得比较拖沓啰嗦,见谅。
有人说是微信内置浏览器的问题,也有人说是nginx的问题,我觉得应该不是nginx,因为这次用python用的是django内置的服务器,也同样情况。这个问题在苹果手机上好像就没有,主要是在安卓版微信上有,因为自己已经不用苹果手机了,所以也没有做测试。
最后,上代码:
我的开发环境:
python 3.5.3
django 2.0.7
view.py代码如下:
import json import random import requests #全局变量 APP_ID = '公众号ID' App_SEC = '公众号SEC' RADOM_STATE = "" def oauth(request): global RADOM_STATE _code = request.GET #1 第一步,拿到code oauth_state = _code['state'] oauth_code = _code['code'] oauth_info_json = { 'oauth_state' : oauth_state, 'oauth_code' : oauth_code } #print("=====>" + json.dumps(oauth_info_json)) print(oauth_state) #本次拿到的随机数令牌 print(RADOM_STATE) #全局变量里保存的跳转之前生成的随机数令牌 #比较本次state和全局随机数令牌是否一致,不一致,说明是第二次访问,丢弃 if RADOM_STATE != oauth_state: pass else: #一致,重新生成随机数令牌,防止第二次访问造成code失效 RADOM_STATE = str(random.randint(1000, 9999)) # print(RADOM_STATE) # 2 第二步, 换取access_token url = 'https://api.weixin.qq.com/sns/oauth2/access_token?appid={}&secret={}&code={}&grant_type=authorization_code'.format(APP_ID, App_SEC, oauth_code) print(url) oauth_access_token = requests.get(url) print("=====>" + oauth_access_token.text) re_json_str = oauth_access_token.text # print(type(re_json_str), re_json_str) re_json = json.loads(re_json_str) # print(type(re_json), re_json) # 3 第三步,拿到用户信息 try: url = 'https://api.weixin.qq.com/sns/userinfo?access_token={}&openid={}&lang=zh_CN'.format(re_json['access_token'], re_json['openid']) print(url) user_info = requests.get(url) #print(user_info.encoding) #print(user_info.text.encode("iso-8859-1").decode('utf8')) user_info_json_str = user_info.text.encode("iso-8859-1").decode('utf8') #微信传过来的中文使用的iso8859编码,为了在模板里能显示出来,要转换成utf8编码 # print(type(user_info_json_str), user_info_json_str) user_info_json = json.loads(user_info_json_str) # print(type(user_info_json), user_info_json) except Exception as e: print(e) content = { 'oauth_info': oauth_info_json, #oauth_info_json 'user_info': user_info_json #user_info_json } return render(request, 'user_info.html', content) #把数据给html模板,其实oauth_info_json没必要传了,只需要user_inf_json就够了,这里只是为了演示! def user_info(request): global RADOM_STATE REDIRECT_URI = "http://www.xxxxxxxxx.cn/weixin/oauth/" #跳转的地址 SCOPE = "snsapi_userinfo" STATE = str(random.randint(1000,9999)) #生成随机数 RADOM_STATE = STATE #随机数给全局变量 redirect_url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid={}&redirect_uri={}&response_type=code&scope={}&state={}&connect_redirect=1#wechat_redirect".format(APP_ID, REDIRECT_URI, SCOPE, STATE) # print(redirect_url) return HttpResponseRedirect(redirect_url) #向微信服务器做跳转
user_info.html模板:
<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>yoooko.cn</title> </head> <body> <h1>Hello</h1> <h3>oauth_state: </h3><p>{{ oauth_info.oauth_state }}</p> <h3>oauth_code: </h3><p>{{ oauth_info.oauth_code }}</p> <p><b>{{ user_info }}</b></p> </body> </html>