component.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  1. # -*- coding: utf-8 -*-
  2. """
  3. wechatpy.component
  4. ~~~~~~~~~~~~~~~
  5. This module provides client library for WeChat Open Platform
  6. :copyright: (c) 2015 by hunter007.
  7. :license: MIT, see LICENSE for more details.
  8. """
  9. from __future__ import absolute_import, unicode_literals
  10. import logging
  11. import time
  12. import warnings
  13. import requests
  14. import six
  15. import xmltodict
  16. from six.moves.urllib.parse import quote
  17. from wechatpy.client import WeChatComponentClient
  18. from wechatpy.constants import WeChatErrorCode
  19. from wechatpy.crypto import WeChatCrypto
  20. from wechatpy.exceptions import APILimitedException, WeChatClientException, WeChatOAuthException, \
  21. WeChatComponentOAuthException
  22. from wechatpy.fields import DateTimeField, StringField
  23. from wechatpy.messages import MessageMetaClass
  24. from wechatpy.session.memorystorage import MemoryStorage
  25. from wechatpy.utils import get_querystring, json, to_binary, to_text, ObjectDict
  26. logger = logging.getLogger(__name__)
  27. COMPONENT_MESSAGE_TYPES = {}
  28. def register_component_message(msg_type):
  29. def register(cls):
  30. COMPONENT_MESSAGE_TYPES[msg_type] = cls
  31. return cls
  32. return register
  33. class BaseComponentMessage(six.with_metaclass(MessageMetaClass)):
  34. """Base class for all component messages and events"""
  35. type = 'unknown'
  36. appid = StringField('AppId')
  37. create_time = DateTimeField('CreateTime')
  38. def __init__(self, message):
  39. self._data = message
  40. def __repr__(self):
  41. _repr = "{klass}({msg})".format(
  42. klass=self.__class__.__name__,
  43. msg=repr(self._data)
  44. )
  45. if six.PY2:
  46. return to_binary(_repr)
  47. else:
  48. return to_text(_repr)
  49. @register_component_message('component_verify_ticket')
  50. class ComponentVerifyTicketMessage(BaseComponentMessage):
  51. """
  52. component_verify_ticket协议
  53. """
  54. type = 'component_verify_ticket'
  55. verify_ticket = StringField('ComponentVerifyTicket')
  56. @register_component_message('unauthorized')
  57. class ComponentUnauthorizedMessage(BaseComponentMessage):
  58. """
  59. 取消授权通知
  60. """
  61. type = 'unauthorized'
  62. authorizer_appid = StringField('AuthorizerAppid')
  63. @register_component_message('authorized')
  64. class ComponentAuthorizedMessage(BaseComponentMessage):
  65. """
  66. 新增授权通知
  67. """
  68. type = 'authorized'
  69. authorizer_appid = StringField('AuthorizerAppid')
  70. authorization_code = StringField('AuthorizationCode')
  71. authorization_code_expired_time = StringField('AuthorizationCodeExpiredTime')
  72. pre_auth_code = StringField('PreAuthCode')
  73. @register_component_message('updateauthorized')
  74. class ComponentUpdateauthorizedMessage(BaseComponentMessage):
  75. """
  76. 更新授权通知
  77. """
  78. type = 'updateauthorized'
  79. authorizer_appid = StringField('AuthorizerAppid')
  80. authorization_code = StringField('AuthorizationCode')
  81. authorization_code_expired_time = StringField('AuthorizationCodeExpiredTime')
  82. pre_auth_code = StringField('PreAuthCode')
  83. class ComponentUnknownMessage(BaseComponentMessage):
  84. """
  85. 未知通知
  86. """
  87. type = 'unknown'
  88. class BaseWeChatComponent(object):
  89. API_BASE_URL = 'https://api.weixin.qq.com/cgi-bin'
  90. def __init__(self,
  91. component_appid,
  92. component_appsecret,
  93. component_token,
  94. encoding_aes_key,
  95. session=None,
  96. auto_retry=True):
  97. """
  98. :param component_appid: 第三方平台appid
  99. :param component_appsecret: 第三方平台appsecret
  100. :param component_token: 公众号消息校验Token
  101. :param encoding_aes_key: 公众号消息加解密Key
  102. """
  103. self._http = requests.Session()
  104. self.component_appid = component_appid
  105. self.component_appsecret = component_appsecret
  106. self.expires_at = None
  107. self.crypto = WeChatCrypto(
  108. component_token, encoding_aes_key, component_appid)
  109. self.session = session or MemoryStorage()
  110. self.auto_retry = auto_retry
  111. if isinstance(session, six.string_types):
  112. from shove import Shove
  113. from wechatpy.session.shovestorage import ShoveStorage
  114. querystring = get_querystring(session)
  115. prefix = querystring.get('prefix', ['wechatpy'])[0]
  116. shove = Shove(session)
  117. storage = ShoveStorage(shove, prefix)
  118. self.session = storage
  119. @property
  120. def component_verify_ticket(self):
  121. return self.session.get('component_verify_ticket')
  122. def _request(self, method, url_or_endpoint, **kwargs):
  123. if not url_or_endpoint.startswith(('http://', 'https://')):
  124. api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL)
  125. url = '{base}{endpoint}'.format(
  126. base=api_base_url,
  127. endpoint=url_or_endpoint
  128. )
  129. else:
  130. url = url_or_endpoint
  131. if 'params' not in kwargs:
  132. kwargs['params'] = {}
  133. if isinstance(kwargs['params'], dict) and \
  134. 'component_access_token' not in kwargs['params']:
  135. kwargs['params'][
  136. 'component_access_token'] = self.access_token
  137. if isinstance(kwargs['data'], dict):
  138. kwargs['data'] = json.dumps(kwargs['data'])
  139. res = self._http.request(
  140. method=method,
  141. url=url,
  142. **kwargs
  143. )
  144. try:
  145. res.raise_for_status()
  146. except requests.RequestException as reqe:
  147. raise WeChatClientException(
  148. errcode=None,
  149. errmsg=None,
  150. client=self,
  151. request=reqe.request,
  152. response=reqe.response
  153. )
  154. return self._handle_result(res, method, url, **kwargs)
  155. def _handle_result(self, res, method=None, url=None, **kwargs):
  156. result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False)
  157. if 'errcode' in result:
  158. result['errcode'] = int(result['errcode'])
  159. if 'errcode' in result and result['errcode'] != 0:
  160. errcode = result['errcode']
  161. errmsg = result.get('errmsg', errcode)
  162. if self.auto_retry and errcode in (
  163. WeChatErrorCode.INVALID_CREDENTIAL.value,
  164. WeChatErrorCode.INVALID_ACCESS_TOKEN.value,
  165. WeChatErrorCode.EXPIRED_ACCESS_TOKEN.value):
  166. logger.info('Component access token expired, fetch a new one and retry request')
  167. self.fetch_access_token()
  168. kwargs['params']['component_access_token'] = self.session.get(
  169. 'component_access_token'
  170. )
  171. return self._request(
  172. method=method,
  173. url_or_endpoint=url,
  174. **kwargs
  175. )
  176. elif errcode == WeChatErrorCode.OUT_OF_API_FREQ_LIMIT.value:
  177. # api freq out of limit
  178. raise APILimitedException(
  179. errcode,
  180. errmsg,
  181. client=self,
  182. request=res.request,
  183. response=res
  184. )
  185. else:
  186. raise WeChatClientException(
  187. errcode,
  188. errmsg,
  189. client=self,
  190. request=res.request,
  191. response=res
  192. )
  193. return result
  194. def fetch_access_token(self):
  195. """
  196. 获取 component_access_token
  197. 详情请参考 https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list\
  198. &t=resource/res_list&verify=1&id=open1419318587&token=&lang=zh_CN
  199. :return: 返回的 JSON 数据包
  200. """
  201. url = '{0}{1}'.format(
  202. self.API_BASE_URL,
  203. '/component/api_component_token'
  204. )
  205. return self._fetch_access_token(
  206. url=url,
  207. data=json.dumps({
  208. 'component_appid': self.component_appid,
  209. 'component_appsecret': self.component_appsecret,
  210. 'component_verify_ticket': self.component_verify_ticket
  211. })
  212. )
  213. def _fetch_access_token(self, url, data):
  214. """ The real fetch access token """
  215. logger.info('Fetching component access token')
  216. res = self._http.post(
  217. url=url,
  218. data=data
  219. )
  220. try:
  221. res.raise_for_status()
  222. except requests.RequestException as reqe:
  223. raise WeChatClientException(
  224. errcode=None,
  225. errmsg=None,
  226. client=self,
  227. request=reqe.request,
  228. response=reqe.response
  229. )
  230. result = res.json()
  231. if 'errcode' in result and result['errcode'] != 0:
  232. raise WeChatClientException(
  233. result['errcode'],
  234. result['errmsg'],
  235. client=self,
  236. request=res.request,
  237. response=res
  238. )
  239. expires_in = 7200
  240. if 'expires_in' in result:
  241. expires_in = result['expires_in']
  242. self.session.set(
  243. 'component_access_token',
  244. result['component_access_token'],
  245. expires_in
  246. )
  247. self.expires_at = int(time.time()) + expires_in
  248. return result
  249. @property
  250. def access_token(self):
  251. """ WeChat component access token """
  252. access_token = self.session.get('component_access_token')
  253. if access_token:
  254. if not self.expires_at:
  255. # user provided access_token, just return it
  256. return access_token
  257. timestamp = time.time()
  258. if self.expires_at - timestamp > 60:
  259. return access_token
  260. self.fetch_access_token()
  261. return self.session.get('component_access_token')
  262. def get(self, url, **kwargs):
  263. return self._request(
  264. method='get',
  265. url_or_endpoint=url,
  266. **kwargs
  267. )
  268. def post(self, url, **kwargs):
  269. return self._request(
  270. method='post',
  271. url_or_endpoint=url,
  272. **kwargs
  273. )
  274. class WeChatComponent(BaseWeChatComponent):
  275. PRE_AUTH_URL = 'https://mp.weixin.qq.com/cgi-bin/componentloginpage'
  276. def get_pre_auth_url(self, redirect_uri):
  277. redirect_uri = quote(redirect_uri, safe=b'')
  278. return "{0}?component_appid={1}&pre_auth_code={2}&redirect_uri={3}".format(
  279. self.PRE_AUTH_URL, self.component_appid, self.create_preauthcode()['pre_auth_code'], redirect_uri
  280. )
  281. def get_pre_auth_url_m(self, redirect_uri):
  282. """
  283. 快速获取pre auth url,可以直接微信中发送该链接,直接授权
  284. """
  285. url = "https://mp.weixin.qq.com/safe/bindcomponent?action=bindcomponent&auth_type=3&no_scan=1&"
  286. redirect_uri = quote(redirect_uri, safe='')
  287. return "{0}component_appid={1}&pre_auth_code={2}&redirect_uri={3}".format(
  288. url, self.component_appid, self.create_preauthcode()['pre_auth_code'], redirect_uri
  289. )
  290. def create_preauthcode(self):
  291. """
  292. 获取预授权码
  293. """
  294. return self.post(
  295. '/component/api_create_preauthcode',
  296. data={
  297. 'component_appid': self.component_appid
  298. }
  299. )
  300. def _query_auth(self, authorization_code):
  301. """
  302. 使用授权码换取公众号的授权信息
  303. :params authorization_code: 授权code,会在授权成功时返回给第三方平台,详见第三方平台授权流程说明
  304. """
  305. return self.post(
  306. '/component/api_query_auth',
  307. data={
  308. 'component_appid': self.component_appid,
  309. 'authorization_code': authorization_code
  310. }
  311. )
  312. def query_auth(self, authorization_code):
  313. """
  314. 使用授权码换取公众号的授权信息,同时储存token信息
  315. :params authorization_code: 授权code,会在授权成功时返回给第三方平台,详见第三方平台授权流程说明
  316. """
  317. result = self._query_auth(authorization_code)
  318. assert result is not None \
  319. and 'authorization_info' in result \
  320. and 'authorizer_appid' in result['authorization_info']
  321. authorizer_appid = result['authorization_info']['authorizer_appid']
  322. if 'authorizer_access_token' in result['authorization_info'] \
  323. and result['authorization_info']['authorizer_access_token']:
  324. access_token = result['authorization_info']['authorizer_access_token']
  325. access_token_key = '{0}_access_token'.format(authorizer_appid)
  326. expires_in = 7200
  327. if 'expires_in' in result['authorization_info']:
  328. expires_in = result['authorization_info']['expires_in']
  329. self.session.set(access_token_key, access_token, expires_in)
  330. if 'authorizer_refresh_token' in result['authorization_info'] \
  331. and result['authorization_info']['authorizer_refresh_token']:
  332. refresh_token = result['authorization_info']['authorizer_refresh_token']
  333. refresh_token_key = '{0}_refresh_token'.format(authorizer_appid)
  334. self.session.set(refresh_token_key, refresh_token) # refresh_token 需要永久储存,不建议使用内存储存,否则每次重启服务需要重新扫码授权
  335. return result
  336. def refresh_authorizer_token(
  337. self, authorizer_appid, authorizer_refresh_token):
  338. """
  339. 获取(刷新)授权公众号的令牌
  340. :params authorizer_appid: 授权方appid
  341. :params authorizer_refresh_token: 授权方的刷新令牌
  342. """
  343. return self.post(
  344. '/component/api_authorizer_token',
  345. data={
  346. 'component_appid': self.component_appid,
  347. 'authorizer_appid': authorizer_appid,
  348. 'authorizer_refresh_token': authorizer_refresh_token
  349. }
  350. )
  351. def get_authorizer_info(self, authorizer_appid):
  352. """
  353. 获取授权方的账户信息
  354. :params authorizer_appid: 授权方appid
  355. """
  356. return self.post(
  357. '/component/api_get_authorizer_info',
  358. data={
  359. 'component_appid': self.component_appid,
  360. 'authorizer_appid': authorizer_appid,
  361. }
  362. )
  363. def get_authorizer_option(self, authorizer_appid, option_name):
  364. """
  365. 获取授权方的选项设置信息
  366. :params authorizer_appid: 授权公众号appid
  367. :params option_name: 选项名称
  368. """
  369. return self.post(
  370. '/component/api_get_authorizer_option',
  371. data={
  372. 'component_appid': self.component_appid,
  373. 'authorizer_appid': authorizer_appid,
  374. 'option_name': option_name
  375. }
  376. )
  377. def set_authorizer_option(
  378. self, authorizer_appid, option_name, option_value):
  379. """
  380. 设置授权方的选项信息
  381. :params authorizer_appid: 授权公众号appid
  382. :params option_name: 选项名称
  383. :params option_value: 设置的选项值
  384. """
  385. return self.post(
  386. '/component/api_set_authorizer_option',
  387. data={
  388. 'component_appid': self.component_appid,
  389. 'authorizer_appid': authorizer_appid,
  390. 'option_name': option_name,
  391. 'option_value': option_value
  392. }
  393. )
  394. def get_client_by_authorization_code(self, authorization_code):
  395. """
  396. 通过授权码直接获取 Client 对象
  397. :params authorization_code: 授权code,会在授权成功时返回给第三方平台,详见第三方平台授权流程说明
  398. """
  399. warnings.warn('`get_client_by_authorization_code` method of `WeChatComponent` is deprecated,'
  400. 'Use `parse_message` parse message and '
  401. 'Use `get_client_by_appid` instead',
  402. DeprecationWarning, stacklevel=2)
  403. result = self.query_auth(authorization_code)
  404. access_token = result['authorization_info']['authorizer_access_token']
  405. refresh_token = result['authorization_info']['authorizer_refresh_token'] # NOQA
  406. authorizer_appid = result['authorization_info']['authorizer_appid'] # noqa
  407. return WeChatComponentClient(
  408. authorizer_appid, self, access_token, refresh_token,
  409. session=self.session
  410. )
  411. def get_client_by_appid(self, authorizer_appid):
  412. """
  413. 通过 authorizer_appid 获取 Client 对象
  414. :params authorizer_appid: 授权公众号appid
  415. """
  416. access_token_key = '{0}_access_token'.format(authorizer_appid)
  417. refresh_token_key = '{0}_refresh_token'.format(authorizer_appid)
  418. access_token = self.session.get(access_token_key)
  419. refresh_token = self.session.get(refresh_token_key)
  420. assert refresh_token
  421. if not access_token:
  422. ret = self.refresh_authorizer_token(
  423. authorizer_appid,
  424. refresh_token
  425. )
  426. access_token = ret['authorizer_access_token']
  427. refresh_token = ret['authorizer_refresh_token']
  428. access_token_key = '{0}_access_token'.format(authorizer_appid)
  429. expires_in = 7200
  430. if 'expires_in' in ret:
  431. expires_in = ret['expires_in']
  432. self.session.set(access_token_key, access_token, expires_in)
  433. return WeChatComponentClient(
  434. authorizer_appid,
  435. self,
  436. session=self.session
  437. )
  438. def parse_message(self, msg, msg_signature, timestamp, nonce):
  439. """
  440. 处理 wechat server 推送消息
  441. :params msg: 加密内容
  442. :params msg_signature: 消息签名
  443. :params timestamp: 时间戳
  444. :params nonce: 随机数
  445. """
  446. content = self.crypto.decrypt_message(msg, msg_signature, timestamp, nonce)
  447. message = xmltodict.parse(to_text(content))['xml']
  448. message_type = message['InfoType'].lower()
  449. message_class = COMPONENT_MESSAGE_TYPES.get(message_type, ComponentUnknownMessage)
  450. msg = message_class(message)
  451. if msg.type == 'component_verify_ticket':
  452. self.session.set(msg.type, msg.verify_ticket)
  453. elif msg.type in ('authorized', 'updateauthorized'):
  454. msg.query_auth_result = self.query_auth(msg.authorization_code)
  455. return msg
  456. def cache_component_verify_ticket(self, msg, signature, timestamp, nonce):
  457. """
  458. 处理 wechat server 推送的 component_verify_ticket消息
  459. :params msg: 加密内容
  460. :params signature: 消息签名
  461. :params timestamp: 时间戳
  462. :params nonce: 随机数
  463. """
  464. warnings.warn('`cache_component_verify_ticket` method of `WeChatComponent` is deprecated,'
  465. 'Use `parse_message` instead',
  466. DeprecationWarning, stacklevel=2)
  467. content = self.crypto.decrypt_message(msg, signature, timestamp, nonce)
  468. message = xmltodict.parse(to_text(content))['xml']
  469. o = ComponentVerifyTicketMessage(message)
  470. self.session.set(o.type, o.verify_ticket)
  471. def get_unauthorized(self, msg, signature, timestamp, nonce):
  472. """
  473. 处理取消授权通知
  474. :params msg: 加密内容
  475. :params signature: 消息签名
  476. :params timestamp: 时间戳
  477. :params nonce: 随机数
  478. """
  479. warnings.warn('`get_unauthorized` method of `WeChatComponent` is deprecated,'
  480. 'Use `parse_message` instead',
  481. DeprecationWarning, stacklevel=2)
  482. content = self.crypto.decrypt_message(msg, signature, timestamp, nonce)
  483. message = xmltodict.parse(to_text(content))['xml']
  484. return ComponentUnauthorizedMessage(message)
  485. def get_component_oauth(self, authorizer_appid):
  486. """
  487. 代公众号 OAuth 网页授权
  488. :params authorizer_appid: 授权公众号appid
  489. """
  490. return ComponentOAuth(authorizer_appid, component=self)
  491. class ComponentOAuth(object):
  492. """ 微信开放平台 代公众号 OAuth 网页授权
  493. 详情请参考
  494. https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419318590
  495. """
  496. API_BASE_URL = 'https://api.weixin.qq.com/'
  497. OAUTH_BASE_URL = 'https://open.weixin.qq.com/connect/'
  498. def __init__(self, app_id, component_appid=None, component_access_token=None,
  499. redirect_uri=None, scope='snsapi_base', state='', component=None):
  500. """
  501. :param app_id: 微信公众号 app_id
  502. :param component: WeChatComponent
  503. """
  504. self._http = requests.Session()
  505. self.app_id = app_id
  506. self.component = component
  507. if self.component is None:
  508. warnings.warn('cannot found `component` param of `ComponentOAuth` `__init__` method,'
  509. 'Use `WeChatComponent.get_component_oauth` instead',
  510. DeprecationWarning, stacklevel=2)
  511. self.component = ObjectDict({'component_appid': component_appid, 'access_token': component_access_token})
  512. if redirect_uri is not None:
  513. warnings.warn('found `redirect_uri` param of `ComponentOAuth` `__init__` method,'
  514. 'Use `ComponentOAuth.get_authorize_url` instead',
  515. DeprecationWarning, stacklevel=2)
  516. self.authorize_url = self.get_authorize_url(redirect_uri, scope, state)
  517. def get_authorize_url(self, redirect_uri, scope='snsapi_base', state=''):
  518. """
  519. :param redirect_uri: 重定向地址,需要urlencode,这里填写的应是服务开发方的回调地址
  520. :param scope: 可选,微信公众号 OAuth2 scope,默认为 ``snsapi_base``
  521. :param state: 可选,重定向后会带上state参数,开发者可以填写任意参数值,最多128字节
  522. """
  523. redirect_uri = quote(redirect_uri, safe=b'')
  524. url_list = [
  525. self.OAUTH_BASE_URL,
  526. 'oauth2/authorize?appid=',
  527. self.app_id,
  528. '&redirect_uri=',
  529. redirect_uri,
  530. '&response_type=code&scope=',
  531. scope,
  532. ]
  533. if state:
  534. url_list.extend(['&state=', state])
  535. url_list.extend([
  536. '&component_appid=',
  537. self.component.component_appid,
  538. ])
  539. url_list.append('#wechat_redirect')
  540. return ''.join(url_list)
  541. def fetch_access_token(self, code):
  542. """获取 access_token
  543. :param code: 授权完成跳转回来后 URL 中的 code 参数
  544. :return: JSON 数据包
  545. """
  546. res = self._get(
  547. 'sns/oauth2/component/access_token',
  548. params={
  549. 'appid': self.app_id,
  550. 'component_appid': self.component.component_appid,
  551. 'component_access_token': self.component.access_token,
  552. 'code': code,
  553. 'grant_type': 'authorization_code',
  554. }
  555. )
  556. self.access_token = res['access_token']
  557. self.open_id = res['openid']
  558. self.refresh_token = res['refresh_token']
  559. self.expires_in = res['expires_in']
  560. self.scope = res['scope']
  561. return res
  562. def refresh_access_token(self, refresh_token):
  563. """刷新 access token
  564. :param refresh_token: OAuth2 refresh token
  565. :return: JSON 数据包
  566. """
  567. res = self._get(
  568. 'sns/oauth2/component/refresh_token',
  569. params={
  570. 'appid': self.app_id,
  571. 'grant_type': 'refresh_token',
  572. 'refresh_token': refresh_token,
  573. 'component_appid': self.component.component_appid,
  574. 'component_access_token': self.component.access_token,
  575. }
  576. )
  577. self.access_token = res['access_token']
  578. self.open_id = res['openid']
  579. self.refresh_token = res['refresh_token']
  580. self.expires_in = res['expires_in']
  581. self.scope = res['scope']
  582. return res
  583. def get_user_info(self, openid=None, access_token=None, lang='zh_CN'):
  584. """ 获取用户基本信息(需授权作用域为snsapi_userinfo)
  585. 如果网页授权作用域为snsapi_userinfo,则此时开发者可以通过access_token和openid拉取用户信息了。
  586. :param openid: 可选,微信 openid,默认获取当前授权用户信息
  587. :param access_token: 可选,access_token,默认使用当前授权用户的 access_token
  588. :param lang: 可选,语言偏好, 默认为 ``zh_CN``
  589. :return: JSON 数据包
  590. """
  591. openid = openid or self.open_id
  592. access_token = access_token or self.access_token
  593. return self._get(
  594. 'sns/userinfo',
  595. params={
  596. 'access_token': access_token,
  597. 'openid': openid,
  598. 'lang': lang
  599. }
  600. )
  601. def _request(self, method, url_or_endpoint, **kwargs):
  602. if not url_or_endpoint.startswith(('http://', 'https://')):
  603. url = '{base}{endpoint}'.format(
  604. base=self.API_BASE_URL,
  605. endpoint=url_or_endpoint
  606. )
  607. else:
  608. url = url_or_endpoint
  609. if isinstance(kwargs.get('data', ''), dict):
  610. body = json.dumps(kwargs['data'], ensure_ascii=False)
  611. body = body.encode('utf-8')
  612. kwargs['data'] = body
  613. res = self._http.request(
  614. method=method,
  615. url=url,
  616. **kwargs
  617. )
  618. try:
  619. res.raise_for_status()
  620. except requests.RequestException as reqe:
  621. raise WeChatOAuthException(
  622. errcode=None,
  623. errmsg=None,
  624. client=self,
  625. request=reqe.request,
  626. response=reqe.response
  627. )
  628. return self._handle_result(res, method=method, url=url, **kwargs)
  629. def _handle_result(self, res, method=None, url=None, **kwargs):
  630. result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False)
  631. if 'errcode' in result:
  632. result['errcode'] = int(result['errcode'])
  633. if 'errcode' in result and result['errcode'] != 0:
  634. errcode = result['errcode']
  635. errmsg = result.get('errmsg', errcode)
  636. if self.component.auto_retry and errcode in (
  637. WeChatErrorCode.INVALID_CREDENTIAL.value,
  638. WeChatErrorCode.INVALID_ACCESS_TOKEN.value,
  639. WeChatErrorCode.EXPIRED_ACCESS_TOKEN.value):
  640. logger.info('Component access token expired, fetch a new one and retry request')
  641. self.component.fetch_access_token()
  642. kwargs['params']['component_access_token'] = self.component.access_token
  643. return self._request(
  644. method=method,
  645. url_or_endpoint=url,
  646. **kwargs
  647. )
  648. elif errcode == WeChatErrorCode.OUT_OF_API_FREQ_LIMIT.value:
  649. # api freq out of limit
  650. raise APILimitedException(
  651. errcode,
  652. errmsg,
  653. client=self,
  654. request=res.request,
  655. response=res
  656. )
  657. else:
  658. raise WeChatComponentOAuthException(
  659. errcode,
  660. errmsg,
  661. client=self,
  662. request=res.request,
  663. response=res
  664. )
  665. return result
  666. def _get(self, url, **kwargs):
  667. return self._request(
  668. method='get',
  669. url_or_endpoint=url,
  670. **kwargs
  671. )