wx.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. from __future__ import unicode_literals
  2. import time
  3. import json
  4. import requests
  5. import redis
  6. import string
  7. import random
  8. import hashlib
  9. __all__ = ("wx_login", "WeixinMP", )
  10. try:
  11. unicode = unicode
  12. except NameError:
  13. # python 3
  14. basestring = (str, bytes)
  15. else:
  16. # python 2
  17. bytes = str
  18. class WeixinError(Exception):
  19. def __init__(self, msg):
  20. super(WeixinError, self).__init__(msg)
  21. class Map(dict):
  22. """
  23. 提供字典的dot访问模式
  24. Example:
  25. m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer'])
  26. """
  27. def __init__(self, *args, **kwargs):
  28. super(Map, self).__init__(*args, **kwargs)
  29. for arg in args:
  30. if isinstance(arg, dict):
  31. for k, v in arg.items():
  32. if isinstance(v, dict):
  33. v = Map(v)
  34. self[k] = v
  35. if kwargs:
  36. for k, v in kwargs.items():
  37. if isinstance(v, dict):
  38. v = Map(v)
  39. self[k] = v
  40. def __getattr__(self, attr):
  41. return self[attr]
  42. def __setattr__(self, key, value):
  43. self.__setitem__(key, value)
  44. def __getitem__(self, key):
  45. if key not in self.__dict__:
  46. super(Map, self).__setitem__(key, {})
  47. self.__dict__.update({key: Map()})
  48. return self.__dict__[key]
  49. def __setitem__(self, key, value):
  50. super(Map, self).__setitem__(key, value)
  51. self.__dict__.update({key: value})
  52. def __delattr__(self, item):
  53. self.__delitem__(item)
  54. def __delitem__(self, key):
  55. super(Map, self).__delitem__(key)
  56. del self.__dict__[key]
  57. class WeixinLoginError(WeixinError):
  58. def __init__(self, msg):
  59. super(WeixinLoginError, self).__init__(msg)
  60. class WeixinMP(object):
  61. api_uri = "https://api.weixin.qq.com/cgi-bin"
  62. def __init__(self, app_id=None, app_secret=None, redis_url=None):
  63. self.app_id = app_id
  64. self.app_secret = app_secret
  65. self.session = requests.Session()
  66. if redis_url is not None:
  67. self.redis_store = redis.StrictRedis.from_url(
  68. redis_url, decode_responses=True)
  69. else:
  70. self.redis_store = None
  71. def init_app(self, app):
  72. """
  73. flask config
  74. """
  75. app.config.setdefault("WXMP_REDIS_URL", None)
  76. app.config.setdefault("WXMP_APP_ID", None)
  77. app.config.setdefault("WXMP_APP_SECRET", None)
  78. self.app_id = app.config.get("WXMP_APP_ID")
  79. self.app_secret = app.config.get("WXMP_APP_SECRET")
  80. redis_url = app.config.get("WXMP_REDIS_URL")
  81. assert self.app_id is not None, "APP_ID IS NULL"
  82. assert self.app_secret is not None, "APP_SECRET IS NULL"
  83. # assert redis_url is not None, "WXMP_REDIS_URL IS NULL"
  84. if redis_url:
  85. self.redis_store = redis.StrictRedis.from_url(
  86. redis_url, decode_responses=True)
  87. def fetch(self, method, url, params=None, data=None, headers=None):
  88. req = requests.Request(method, url, params=params,
  89. data=data, headers=headers)
  90. prepped = req.prepare()
  91. resp = self.session.send(prepped, timeout=20)
  92. data = Map(resp.json())
  93. # if data.errcode:
  94. # msg = "%(errcode)d %(errmsg)s" % data
  95. # raise WeixinError(msg)
  96. return data
  97. def get(self, path, params=None, token=True):
  98. url = "{0}{1}".format(self.api_uri, path)
  99. params = {} if not params else params
  100. token and params.setdefault("access_token", self.access_token)
  101. return self.fetch("GET", url, params)
  102. def gen_token(self):
  103. params = dict()
  104. params.setdefault("grant_type", "client_credential")
  105. params.setdefault("appid", self.app_id)
  106. params.setdefault("secret", self.app_secret)
  107. data = self.get("/token", params, False)
  108. print("-" * 20, data, params)
  109. return data.access_token
  110. def get_user_info(self, access_token, user_id="hehe", lang="zh_CN"):
  111. """
  112. 获取用户基本信息。
  113. :param user_id: 用户 ID 。 就是你收到的 `Message` 的 source
  114. :param lang: 返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语
  115. :return: 返回的 JSON 数据包
  116. """
  117. params = dict()
  118. params.setdefault("openid", user_id)
  119. params.setdefault("access_token", access_token)
  120. params.setdefault("lang", lang)
  121. data = self.get("/user/info", params, False)
  122. print(data, "get_user_info")
  123. return data
  124. def check_token(self, access_token):
  125. data = self.get_user_info(access_token)
  126. if data.errcode in (40001, 42001):
  127. return False
  128. return True
  129. @property
  130. def access_token(self):
  131. """
  132. 获取服务端凭证
  133. """
  134. if self.redis_store:
  135. ac_key = "access_token:%s" % self.app_id
  136. access_token = self.redis_store.get(ac_key)
  137. if not access_token:
  138. access_token = self.gen_token()
  139. self.redis_store.setex(ac_key, 2 * 60 * 60, access_token)
  140. return access_token
  141. else:
  142. if self.check_token(access_token):
  143. return access_token
  144. return self.gen_token()
  145. def gen_ticket(self):
  146. params = dict()
  147. params.setdefault("type", "jsapi")
  148. data = self.get("/ticket/getticket", params, True)
  149. return data.ticket
  150. @property
  151. def jsapi_ticket(self):
  152. """
  153. 获取jsapi ticket
  154. """
  155. if self.redis_store:
  156. ticket_key = "jsapi_ticket:%s" % self.app_id
  157. ticket = self.redis_store.get(ticket_key)
  158. if not ticket:
  159. ticket = self.gen_ticket()
  160. self.redis_store.setex(ticket_key, 2 * 60 * 60, ticket)
  161. return ticket
  162. else:
  163. return self.gen_ticket()
  164. @property
  165. def nonce_str(self):
  166. char = string.ascii_letters + string.digits
  167. return "".join(random.choice(char) for _ in range(32))
  168. def jsapi_sign(self, **kwargs):
  169. """
  170. 生成签名给js使用
  171. """
  172. timestamp = str(int(time.time()))
  173. nonce_str = self.nonce_str
  174. kwargs.setdefault("jsapi_ticket", self.jsapi_ticket)
  175. kwargs.setdefault("timestamp", timestamp)
  176. kwargs.setdefault("noncestr", nonce_str)
  177. raw = [(str(k), str(kwargs[k])) for k in sorted(kwargs.keys())]
  178. s = "&".join("=".join(kv) for kv in raw if kv[1])
  179. print(s)
  180. sign = hashlib.sha1(s.encode("utf-8")).hexdigest().lower()
  181. return Map(sign=sign, timestamp=timestamp, noncestr=nonce_str)
  182. class WeixinLogin(object):
  183. def __init__(self, app=None, prefix=""):
  184. if app:
  185. self.app_id = app.config.get(prefix + "WXMP_APP_ID")
  186. # print(self.app_id)
  187. self.app_secret = app.config.get(prefix + "WXMP_APP_SECRET")
  188. else:
  189. self.app_id = ""
  190. self.app_secret = ""
  191. def init_app(self, app, prefix=""):
  192. self.app_id = app.config.get(prefix + "WXMP_APP_ID")
  193. self.app_secret = app.config.get(prefix + "WXMP_APP_SECRET")
  194. def _get(self, url, params):
  195. resp = requests.get(url, params=params)
  196. data = Map(json.loads(resp.content.decode("utf-8")))
  197. # if data.errcode:
  198. # msg = "%(errcode)d %(errmsg)s" % data
  199. # raise WeixinLoginError(msg)
  200. return data
  201. def authorize(self, redirect_uri, scope="snsapi_base", state=None):
  202. """
  203. 生成微信认证地址并且跳转
  204. :param redirect_uri: 跳转地址
  205. :param scope: 微信认证方式,有`snsapi_base`跟`snsapi_userinfo`两种
  206. :param state: 认证成功后会原样带上此字段
  207. """
  208. assert scope in ["snsapi_base", "snsapi_userinfo", "snsapi_login"]
  209. if scope == "snsapi_login":
  210. url = "https://open.weixin.qq.com/connect/qrconnect"
  211. else:
  212. url = "https://open.weixin.qq.com/connect/oauth2/authorize"
  213. data = dict()
  214. data.setdefault("appid", self.app_id)
  215. data.setdefault("redirect_uri", redirect_uri)
  216. data.setdefault("response_type", "code")
  217. data.setdefault("scope", scope)
  218. if state:
  219. data.setdefault("state", state)
  220. print(data, "data")
  221. data = [(k, data[k]) for k in sorted(data.keys()) if data[k]]
  222. s = "&".join("=".join(kv) for kv in data if kv[1])
  223. print(s)
  224. return "{0}?{1}#wechat_redirect".format(url, s)
  225. def access_token(self, code):
  226. """
  227. 获取令牌
  228. """
  229. url = "https://api.weixin.qq.com/sns/oauth2/access_token"
  230. args = dict()
  231. args.setdefault("appid", self.app_id)
  232. args.setdefault("secret", self.app_secret)
  233. args.setdefault("code", code)
  234. args.setdefault("grant_type", "authorization_code")
  235. return self._get(url, args)
  236. def auth(self, access_token, openid):
  237. """
  238. 检验授权凭证
  239. :param access_token: 授权凭证
  240. :param openid: 唯一id
  241. """
  242. url = "https://api.weixin.qq.com/sns/auth"
  243. args = dict()
  244. args.setdefault("access_token", access_token)
  245. args.setdefault("openid", openid)
  246. return self._get(url, args)
  247. def refresh_token(self, refresh_token):
  248. """
  249. 重新获取access_token
  250. :param refresh_token: 刷新令牌
  251. """
  252. url = "https://api.weixin.qq.com/sns/oauth2/refresh_token"
  253. args = dict()
  254. args.setdefault("appid", self.app_id)
  255. args.setdefault("grant_type", "refresh_token")
  256. args.setdefault("refresh_token", refresh_token)
  257. return self._get(url, args)
  258. def userinfo(self, access_token, openid):
  259. """
  260. 获取用户信息
  261. :param access_token: 令牌
  262. :param openid: 用户id,每个应用内唯一
  263. """
  264. url = "https://api.weixin.qq.com/sns/userinfo"
  265. args = dict()
  266. args.setdefault("access_token", access_token)
  267. args.setdefault("openid", openid)
  268. args.setdefault("lang", "zh_CN")
  269. return self._get(url, args)
  270. def user_info(self, access_token, openid):
  271. """
  272. 获取用户信息
  273. 兼容老版本0.3.0,与WeixinMP的user_info冲突
  274. """
  275. return self.userinfo(access_token, openid)
  276. def get_sp_unionid(self, code):
  277. url = "https://api.weixin.qq.com/sns/jscode2session"
  278. args = dict(appid=self.app_id, secret=self.app_secret,
  279. js_code=code, grant_type="authorization_code")
  280. return self._get(url, args)
  281. wx_login = WeixinLogin()