ymao %!s(int64=4) %!d(string=hai) anos
achega
36df4dfb77
Modificáronse 9 ficheiros con 416 adicións e 0 borrados
  1. 87 0
      .gitignore
  2. 3 0
      README.md
  3. 0 0
      __init__.py
  4. 6 0
      authen/__init__.py
  5. 24 0
      authen/wxlogin/__init__.py
  6. 47 0
      authen/wxlogin/login.py
  7. 37 0
      authen/wxlogin/wraps.py
  8. 182 0
      authen/wxlogin/wx.py
  9. 30 0
      setup.py

+ 87 - 0
.gitignore

@@ -0,0 +1,87 @@
+# ---> macOS
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# ---> Python
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# wxlogin
+
+微信pc和移动端登录相关工具包

+ 0 - 0
__init__.py


+ 6 - 0
authen/__init__.py

@@ -0,0 +1,6 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+    __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+    from pkgutil import extend_path
+    __path__ = extend_path(__path__, __name__)

+ 24 - 0
authen/wxlogin/__init__.py

@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+
+from .login import bp
+from .wx import WeixinLogin
+
+
+class WxLogin(WeixinLogin):
+
+    def __init__(self, app=None, config_prefix=""):
+        self.prefix = config_prefix
+        if app is not None:
+            self.init_app(app)
+
+    def init_app(self, app):
+        app.config.setdefault(self.prefix + "WXMP_APP_ID", None)
+        app.config.setdefault(self.prefix + "WXMP_APP_SECRET", None)
+        app.config.setdefault(self.prefix + "WXMP_USER_COOKIES_NAME", None)
+        app.config.setdefault(self.prefix + "WXMP_VERSION_CODE", "v1")
+        super(WxLogin, self).init_app(app, self.prefix)
+        # wx_login.init_app(app)
+
+        # 注册rbac视图函数
+        app.register_blueprint(
+            bp, url_prefix="/%s/wxlogin" % app.config["WXMP_VERSION_CODE"])

+ 47 - 0
authen/wxlogin/login.py

@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+
+import json
+import urllib
+import datetime
+
+from flask import Blueprint, url_for, redirect, \
+    request, current_app
+
+from .wx import wx_login
+
+
+bp = Blueprint("wxmp", __name__, template_folder="templates")
+
+
+@bp.route("/login")
+def wxmp_login():
+    oauth_type = request.args.get("oauth_type", "snsapi_userinfo")
+    if oauth_type not in ("snsapi_userinfo", "snsapi_base", "snsapi_login"):
+        oauth_type = "snsapi_userinfo"
+    next = request.args.get("next") or request.referrer or "/"
+    callback = url_for(
+        ".wxmp_authorized",
+        oauth_type=oauth_type, next=next, _external=True)
+    url = wx_login.authorize(urllib.quote(callback), oauth_type)
+    return redirect(url)
+
+
+@bp.route("/authorized")
+def wxmp_authorized():
+    code = request.args.get("code")
+    if not code:
+        return "ERR_INVALID_CODE", 400
+    next = request.args.get("next", "/")
+    oauth_type = request.args.get("oauth_type", "snsapi_userinfo")
+    data = wx_login.access_token(code)
+    # openid = data.openid
+    # unionid = data.unionid
+    resp = redirect(next)
+    expires = datetime.datetime.now() + datetime.timedelta(hours=2)
+    if oauth_type == "snsapi_userinfo":
+        cookies_type = "WXMP_USER_COOKIES_NAME"
+    else:
+        cookies_type = "WXMP_BASE_COOKIES_NAME"
+    cookies_name = current_app.config.get(cookies_type)
+    resp.set_cookie(cookies_name, json.dumps(data), expires=expires)
+    return resp

+ 37 - 0
authen/wxlogin/wraps.py

@@ -0,0 +1,37 @@
+import json
+import urllib
+
+from flask import redirect, url_for, request, current_app
+from functools import wraps
+
+
+def wxmp_login_required(func):
+    @wraps(func)
+    def decorated_view(*args, **kwargs):
+        cookies_name = current_app.config.get("WXMP_USER_COOKIES_NAME")
+        token_data = request.cookies.get(cookies_name)
+        if not token_data:
+            return redirect(url_for("wxmp.wxmp_login", next=request.path))
+        kwargs.update({"token_data": json.loads(token_data)})
+        return func(*args, **kwargs)
+    return decorated_view
+
+
+def wxmp_base_login_required(func):
+    @wraps(func)
+    def decorated_view(*args, **kwargs):
+        cookies_name = current_app.config.get("WXMP_BASE_COOKIES_NAME")
+        token_data = request.cookies.get(cookies_name)
+        if not token_data:
+            return redirect(
+                url_for(
+                    "wxmp.wxmp_login",
+                    next="?".join([
+                        request.path,
+                        urllib.urlencode(request.args)
+                    ]),
+                    oauth_type="snsapi_base")
+            )
+        kwargs.update({"token_data": json.loads(token_data)})
+        return func(*args, **kwargs)
+    return decorated_view

+ 182 - 0
authen/wxlogin/wx.py

@@ -0,0 +1,182 @@
+from __future__ import unicode_literals
+
+import json
+import requests
+
+__all__ = ("wx_login")
+
+try:
+    unicode = unicode
+except NameError:
+    # python 3
+    basestring = (str, bytes)
+else:
+    # python 2
+    bytes = str
+
+
+class WeixinError(Exception):
+
+    def __init__(self, msg):
+        super(WeixinError, self).__init__(msg)
+
+
+class Map(dict):
+    """
+    提供字典的dot访问模式
+    Example:
+    m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer'])
+    """
+    def __init__(self, *args, **kwargs):
+        super(Map, self).__init__(*args, **kwargs)
+        for arg in args:
+            if isinstance(arg, dict):
+                for k, v in arg.items():
+                    if isinstance(v, dict):
+                        v = Map(v)
+                    self[k] = v
+
+        if kwargs:
+            for k, v in kwargs.items():
+                if isinstance(v, dict):
+                    v = Map(v)
+                self[k] = v
+
+    def __getattr__(self, attr):
+        return self[attr]
+
+    def __setattr__(self, key, value):
+        self.__setitem__(key, value)
+
+    def __getitem__(self, key):
+        if key not in self.__dict__:
+            super(Map, self).__setitem__(key, {})
+            self.__dict__.update({key: Map()})
+        return self.__dict__[key]
+
+    def __setitem__(self, key, value):
+        super(Map, self).__setitem__(key, value)
+        self.__dict__.update({key: value})
+
+    def __delattr__(self, item):
+        self.__delitem__(item)
+
+    def __delitem__(self, key):
+        super(Map, self).__delitem__(key)
+        del self.__dict__[key]
+
+
+class WeixinLoginError(WeixinError):
+
+    def __init__(self, msg):
+        super(WeixinLoginError, self).__init__(msg)
+
+
+class WeixinLogin(object):
+
+    app_id = ""
+    app_secret = ""
+
+    def init_app(self, app, prefix=""):
+        self.app_id = app.config.get(prefix + "WXMP_APP_ID")
+        self.app_secret = app.config.get(prefix + "WXMP_APP_SECRET")
+
+    def _get(self, url, params):
+        resp = requests.get(url, params=params)
+        data = Map(json.loads(resp.content.decode("utf-8")))
+        if data.errcode:
+            msg = "%(errcode)d %(errmsg)s" % data
+            raise WeixinLoginError(msg)
+        return data
+
+    def authorize(self, redirect_uri, scope="snsapi_base", state=None):
+        """
+        生成微信认证地址并且跳转
+
+        :param redirect_uri: 跳转地址
+        :param scope: 微信认证方式,有`snsapi_base`跟`snsapi_userinfo`两种
+        :param state: 认证成功后会原样带上此字段
+        """
+        assert scope in ["snsapi_base", "snsapi_userinfo", "snsapi_login"]
+        if scope == "snsapi_login":
+            url = "https://open.weixin.qq.com/connect/qrconnect"
+        else:
+            url = "https://open.weixin.qq.com/connect/oauth2/authorize"
+        data = dict()
+        data.setdefault("appid", self.app_id)
+        data.setdefault("redirect_uri", redirect_uri)
+        data.setdefault("response_type", "code")
+        data.setdefault("scope", scope)
+        if state:
+            data.setdefault("state", state)
+        data = [(k, data[k]) for k in sorted(data.keys()) if data[k]]
+        s = "&".join("=".join(kv) for kv in data if kv[1])
+        print(s)
+        return "{0}?{1}#wechat_redirect".format(url, s)
+
+    def access_token(self, code):
+        """
+        获取令牌
+        """
+        url = "https://api.weixin.qq.com/sns/oauth2/access_token"
+        args = dict()
+        args.setdefault("appid", self.app_id)
+        args.setdefault("secret", self.app_secret)
+        args.setdefault("code", code)
+        args.setdefault("grant_type", "authorization_code")
+
+        return self._get(url, args)
+
+    def auth(self, access_token, openid):
+        """
+        检验授权凭证
+
+        :param access_token: 授权凭证
+        :param openid: 唯一id
+        """
+        url = "https://api.weixin.qq.com/sns/auth"
+        args = dict()
+        args.setdefault("access_token", access_token)
+        args.setdefault("openid", openid)
+
+        return self._get(url, args)
+
+    def refresh_token(self, refresh_token):
+        """
+        重新获取access_token
+
+        :param refresh_token: 刷新令牌
+        """
+        url = "https://api.weixin.qq.com/sns/oauth2/refresh_token"
+        args = dict()
+        args.setdefault("appid", self.app_id)
+        args.setdefault("grant_type", "refresh_token")
+        args.setdefault("refresh_token", refresh_token)
+
+        return self._get(url, args)
+
+    def userinfo(self, access_token, openid):
+        """
+        获取用户信息
+
+        :param access_token: 令牌
+        :param openid: 用户id,每个应用内唯一
+        """
+        url = "https://api.weixin.qq.com/sns/userinfo"
+        args = dict()
+        args.setdefault("access_token", access_token)
+        args.setdefault("openid", openid)
+        args.setdefault("lang", "zh_CN")
+
+        return self._get(url, args)
+
+    def user_info(self, access_token, openid):
+        """
+        获取用户信息
+
+        兼容老版本0.3.0,与WeixinMP的user_info冲突
+        """
+        return self.userinfo(access_token, openid)
+
+
+wx_login = WeixinLogin()

+ 30 - 0
setup.py

@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+
+from setuptools import setup, find_packages
+
+setup(
+    name='authen.wxlogin',
+    version=0.33,
+    url='http://git.aoyanming.com/authen29/wxlogin/',
+    license='GPL',
+    author='authen',
+    author_email='295002887@qq.com',
+    description='wxlogin func',
+    long_description=__doc__,
+    packages=find_packages(exclude=['ez_setup']),
+    namespace_packages=['authen'],
+    include_package_data=True,
+    install_requires=[
+        'setuptools'
+    ],
+    classifiers=[
+        "Framework :: Plone",
+        "Framework :: Zope2",
+        "Framework :: Zope3",
+        "Programming Language :: Python",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+    ],
+    entry_points="""
+    # -*- Entry points: -*-
+    """
+)