├── .github └── workflows │ └── build.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── demo ├── config.py ├── libs │ ├── __init__.py │ └── base.py ├── main.py ├── plugins │ ├── __init__.py │ └── ssoclient │ │ └── __init__.py ├── utils │ ├── __init__.py │ ├── aes_cbc.py │ ├── jwt.py │ ├── log.py │ ├── tool.py │ └── web.py └── views │ ├── FrontView.py │ └── __init__.py ├── misc ├── passport.sql ├── sso.png └── supervisord.conf ├── requirements.txt └── src ├── cli.py ├── config.py ├── hlm ├── __init__.py ├── _userapp.py ├── _usermsg.py ├── _userprofile.py └── _usersso.py ├── libs ├── __init__.py ├── auth.py └── base.py ├── main.py ├── online_gunicorn.sh ├── plugins ├── AccessCount │ └── __init__.py ├── __init__.py ├── oauth2_coding │ ├── __init__.py │ └── templates │ │ └── connect_coding.html ├── oauth2_gitee │ ├── __init__.py │ └── templates │ │ └── connect_gitee.html ├── oauth2_github │ ├── __init__.py │ └── templates │ │ └── connect_github.html ├── oauth2_qq │ ├── __init__.py │ └── templates │ │ └── connect_qq.html ├── oauth2_weibo │ ├── __init__.py │ └── templates │ │ └── connect_weibo.html └── ssoserver │ └── __init__.py ├── static ├── css │ ├── ImgCropping.css │ ├── Vidage.min.css │ ├── auth.css │ ├── cropper.min.css │ ├── docs.css │ └── global.css ├── images │ ├── avatar │ │ └── default.png │ ├── bg.jpg │ ├── coding.png │ ├── favicon.png │ ├── gitee.png │ ├── github.png │ ├── login.mp4 │ ├── logo-1.png │ ├── logo.png │ ├── pattern.svg │ ├── qq.png │ ├── vaptcha-loading.gif │ ├── vip.png │ ├── wechat.png │ ├── weibo.png │ └── wrz.png ├── js │ ├── Vidage.min.js │ ├── cropper.min.js │ └── jquery.min.js ├── layui │ ├── css │ │ ├── layui.css │ │ └── modules │ │ │ ├── code.css │ │ │ ├── laydate │ │ │ └── default │ │ │ │ └── laydate.css │ │ │ └── layer │ │ │ └── default │ │ │ ├── icon-ext.png │ │ │ ├── icon.png │ │ │ ├── layer.css │ │ │ ├── loading-0.gif │ │ │ ├── loading-1.gif │ │ │ └── loading-2.gif │ ├── font │ │ ├── iconfont.eot │ │ ├── iconfont.svg │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ └── iconfont.woff2 │ └── layui.js ├── mymod │ ├── app.js │ ├── base.js │ ├── forgot.js │ ├── message.js │ ├── oauthguide.js │ ├── passport.js │ ├── popup.js │ ├── security.js │ ├── setting.js │ ├── signin.js │ └── signup.js ├── spop-0.1.3 │ ├── spop.min.css │ └── spop.min.js └── videos │ ├── bg.mp4 │ └── bg.webm ├── templates ├── auth │ ├── OAuthGuide.html │ ├── base.html │ ├── forgot.html │ ├── signIn.html │ └── signUp.html ├── public │ ├── base.html │ ├── feedback.html │ ├── footer.html │ └── terms.html └── user │ ├── apps.edit.html │ ├── apps.html │ ├── base.html │ ├── message.html │ ├── micro.html │ ├── security.html │ ├── setting.html │ ├── sysmanager.html │ ├── user.bind.html │ └── user.html ├── test └── __init__.py ├── utils ├── Signature.py ├── __init__.py ├── aes_cbc.py ├── ip2region.db ├── ip2region.py ├── jwt.py ├── log.py ├── send_email_msg.py ├── send_phone_msg.py ├── tool.py └── web.py ├── version.py └── views ├── ApiView.py ├── FrontView.py └── __init__.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | docker: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v1 20 | 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v1 23 | 24 | - name: Login to DockerHub 25 | uses: docker/login-action@v1 26 | with: 27 | username: ${{ secrets.DOCKER_USERNAME }} 28 | password: ${{ secrets.DOCKER_PASSWORD }} 29 | 30 | - name: Extract branch name 31 | id: branch 32 | shell: bash 33 | run: echo "##[set-output name=tag;]$(echo ${GITHUB_REF##*/} | sed 's/master/latest/')" 34 | 35 | - name: Build and push 36 | uses: docker/build-push-action@v2 37 | with: 38 | context: . 39 | platforms: linux/amd64 40 | push: true 41 | tags: | 42 | staugur/passport:${{ steps.branch.outputs.tag }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | logs/ 92 | src/logs/ 93 | src/logs/sys.log 94 | src/static/upload/ 95 | .DS_Store 96 | online_preboot.sh 97 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7-slim 2 | ARG PIPMIRROR=https://pypi.org/simple 3 | COPY requirements.txt . 4 | RUN apt update &&\ 5 | apt install -y --no-install-recommends default-libmysqlclient-dev python-dev build-essential &&\ 6 | sed '/st_mysql_options options;/a unsigned int reconnect;' /usr/include/mysql/mysql.h -i.bkp &&\ 7 | pip install --timeout 30 --index $PIPMIRROR --no-cache-dir -r requirements.txt &&\ 8 | rm -rf /var/lib/apt/lists/* requirements.txt 9 | COPY src /passport 10 | WORKDIR /passport 11 | EXPOSE 10030 12 | ENTRYPOINT ["bash", "online_gunicorn.sh", "entrypoint"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Mr.tao 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Passport 2 | Unified authentication and authorization management SSO system for SaintIC Org. 3 | 4 | 5 | # Docuement 6 | [Passport Docs](https://docs.saintic.com/passport/) 7 | 8 | 9 | ## Environment 10 | > 1. Python Version: 2.7 11 | > 2. Web Framework: Flask 12 | > 3. Required Modules for Python 13 | > 4. MySQL, Redis 14 | 15 | 16 | ## Usage 17 | 18 | ``` 19 | 1. 依赖: 20 | 1.0 yum install -y gcc gcc-c++ python-devel libffi-devel openssl-devel mysql-devel 21 | 1.1 git clone https://github.com/staugur/passport && cd passport 22 | 1.2 pip install -r requirements.txt 23 | 1.3 MySQL需要导入 `misc/passport.sql` 数据库文件 24 | 25 | 2. 修改 `src/config.py` 中配置项, getenv函数后是环境变量及其默认值(优先环境变量,其次默认值)。 26 | 2.1 修改GLOBAL全局配置项(主要是端口、日志级别) 27 | 2.2 修改MODULES核心配置项账号认证模块的MYSQL信息 28 | 2.3 修改PLUGINS插件配置项(主要是第三方登录) 29 | 30 | 3. 运行: 31 | 3.1 python main.py #开发模式 32 | 3.2 sh online_gunicorn.sh #生产模式 33 | 34 | 4. 创建管理员: 35 | 4.1 python cli.py --createSuperuser #根据提示输入管理员邮箱密码完成创建 36 | ``` 37 | 38 | 39 | ## Cli 40 | 41 | ``` 42 | cd src 43 | python cli.py #下面是帮助信息 44 | usage: cli.py [-h] [--refresh_loginlog] [--refresh_clicklog] 45 | [--createSuperuser] 46 | 47 | optional arguments: 48 | -h, --help show this help message and exit 49 | --refresh_loginlog 刷入登录日志 50 | --refresh_clicklog 刷入访问日志 51 | --createSuperuser 创建管理员用户 52 | ``` 53 | 54 | 55 | ## TODO 56 | 57 | - redis sid存登录时设备信息 58 | 59 | - ~~绑定邮箱手机、手机登录~~ 60 | 61 | - 用户行为记录 62 | 63 | - 系统管理 64 | 65 | - 安全 66 | 67 | 68 | ## Design 69 | ![Design][1] 70 | 71 | [1]: ./misc/sso.png 72 | -------------------------------------------------------------------------------- /demo/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | demo.config 4 | ~~~~~~~~~~~~~~ 5 | 6 | configure file 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | from os import getenv 13 | 14 | GLOBAL = { 15 | 16 | "ProcessName": "demo", 17 | #Custom process, you can see it with "ps aux|grep ProcessName". 18 | 19 | "Host": getenv("demo_host", "0.0.0.0"), 20 | #Application run network address, you can set it `0.0.0.0`, `127.0.0.1`, ``. 21 | 22 | "Port": getenv("demo_port", 5001), 23 | #Application run port, default port. 24 | 25 | "LogLevel": getenv("demo_loglevel", "DEBUG"), 26 | #Application to write the log level, currently has DEBUG, INFO, WARNING, ERROR, CRITICAL. 27 | } 28 | 29 | 30 | SSO = { 31 | 32 | "app_name": getenv("demo_sso_app_name", GLOBAL["ProcessName"]), 33 | # SSO中注册的应用名 34 | 35 | "app_id": getenv("demo_sso_app_id", "app_id"), 36 | # SSO中注册返回的`app_id` 37 | 38 | "app_secret": getenv("demo_sso_app_secret", "app_secret"), 39 | # SSO中注册返回的`app_secret` 40 | 41 | "sso_server": getenv("demo_sso_server", "https://passport.saintic.com"), 42 | # SSO完全合格域名根地址 43 | } 44 | 45 | 46 | MYSQL = getenv("demo_mysql_url") 47 | #MYSQL数据库连接信息 48 | #mysql://host:port:user:password:database?charset=&timezone= 49 | 50 | 51 | REDIS = getenv("demo_redis_url") 52 | #Redis数据库连接信息,格式: 53 | #redis://[:password]@host:port/db 54 | #host,port必填项,如有密码,记得密码前加冒号,比如redis://localhost:6379/0 55 | 56 | 57 | # 系统配置 58 | SYSTEM = { 59 | 60 | "HMAC_SHA256_KEY": getenv("demo_hmac_sha256_key", "273d32c8d797fa715190c7408ad73811"), 61 | # hmac sha256 key 62 | 63 | "AES_CBC_KEY": getenv("demo_aes_cbc_key", "YRRGBRYQqrV1gv5A"), 64 | # utils.aes_cbc.CBC类中所用加密key 65 | 66 | "JWT_SECRET_KEY": getenv("demo_jwt_secret_key", "WBlE7_#qDf2vRb@vM!Zw#lqrg@rdd3A6"), 67 | # utils.jwt.JWTUtil类中所用加密key 68 | } 69 | 70 | -------------------------------------------------------------------------------- /demo/libs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | # modules open interface -------------------------------------------------------------------------------- /demo/libs/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | demo.libs.base 4 | ~~~~~~~~~~~~~~ 5 | 6 | Base class: dependent services, connection information, and public information. 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | from utils.tool import plugin_logger 13 | 14 | 15 | class ServiceBase(object): 16 | """ 所有服务的基类 """ 17 | 18 | def __init__(self): 19 | #设置全局超时时间(如连接超时) 20 | self.timeout= 2 21 | 22 | 23 | class PluginBase(ServiceBase): 24 | """ 插件基类: 提供插件所需要的公共接口与扩展点 """ 25 | 26 | def __init__(self): 27 | super(PluginBase, self).__init__() 28 | self.logger = plugin_logger 29 | -------------------------------------------------------------------------------- /demo/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | demo.main 4 | ~~~~~~~~~~~~~~ 5 | 6 | Entrance 7 | 8 | Docstring conventions: 9 | http://flask.pocoo.org/docs/0.10/styleguide/#docstrings 10 | 11 | Comments: 12 | http://flask.pocoo.org/docs/0.10/styleguide/#comments 13 | 14 | :copyright: (c) 2017 by staugur. 15 | :license: MIT, see LICENSE for more details. 16 | """ 17 | 18 | import os 19 | import jinja2 20 | from config import GLOBAL 21 | from utils.tool import err_logger, access_logger 22 | from utils.web import verify_sessionId, analysis_sessionId, get_redirect_url 23 | from views import FrontBlueprint 24 | from flask import Flask, request, g, jsonify 25 | from flask_pluginkit import PluginManager 26 | 27 | 28 | __author__ = 'staugur' 29 | __email__ = 'staugur@saintic.com' 30 | __doc__ = 'SSO Cient Demo' 31 | __date__ = '2018-03-14' 32 | __version__ = '0.1.0' 33 | 34 | 35 | # 初始化定义application 36 | app = Flask(__name__) 37 | app.config.update( 38 | SECRET_KEY=os.urandom(24) 39 | ) 40 | 41 | # 初始化插件管理器(自动扫描并加载运行) 42 | plugin = PluginManager(app) 43 | 44 | # 注册视图包中蓝图 45 | app.register_blueprint(FrontBlueprint) 46 | 47 | # 添加模板上下文变量 48 | @app.context_processor 49 | def GlobalTemplateVariables(): 50 | data = {"Version": __version__, "Author": __author__, "Email": __email__, "Doc": __doc__} 51 | return data 52 | 53 | 54 | @app.before_request 55 | def before_request(): 56 | g.signin = verify_sessionId(request.cookies.get("sessionId")) 57 | g.sid, g.uid = analysis_sessionId(request.cookies.get("sessionId"), "tuple") if g.signin else (None, None) 58 | g.ip = request.headers.get('X-Real-Ip', request.remote_addr) 59 | # 仅是重定向页面快捷定义 60 | g.redirect_uri = get_redirect_url() 61 | 62 | 63 | if __name__ == '__main__': 64 | app.run(host=GLOBAL["Host"], port=int(GLOBAL["Port"]), debug=True) 65 | -------------------------------------------------------------------------------- /demo/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | # Plugins Package -------------------------------------------------------------------------------- /demo/plugins/ssoclient/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | demo.plugins.ssoclient 4 | ~~~~~~~~~~~~~~ 5 | 6 | SSO Client 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | #: Importing these two modules is the first and must be done. 13 | #: 首先导入这两个必须模块 14 | from __future__ import absolute_import 15 | from libs.base import PluginBase 16 | #: Import the other modules here, and if it's your own module, use the relative Import. eg: from .lib import Lib 17 | #: 在这里导入其他模块, 如果有自定义包目录, 使用相对导入, 如: from .lib import Lib 18 | import requests, json 19 | from config import SSO 20 | from utils.web import login_required, anonymous_required, set_ssoparam, set_sessionId, get_redirect_url, get_referrer_url 21 | from utils.tool import url_check, logger, hmac_sha256 22 | from flask import Blueprint, request, jsonify, g, redirect, url_for, make_response 23 | 24 | #:Your plugin name 25 | #:你的插件名称 26 | __name__ = "ssoclient" 27 | #: Plugin describes information 28 | #: 插件描述信息 29 | __description__ = "SSO Client" 30 | #: Plugin Author 31 | #: 插件作者 32 | __author__ = "Mr.tao " 33 | #: Plugin Version 34 | #: 插件版本 35 | __version__ = "0.1" 36 | #: Plugin Url 37 | #: 插件主页 38 | __url__ = "https://www.saintic.com" 39 | #: Plugin License 40 | #: 插件许可证 41 | __license__ = "MIT" 42 | #: Plugin License File 43 | #: 插件许可证文件 44 | __license_file__= "LICENSE" 45 | #: Plugin Readme File 46 | #: 插件自述文件 47 | __readme_file__ = "README" 48 | #: Plugin state, enabled or disabled, default: enabled 49 | #: 插件状态, enabled、disabled, 默认enabled 50 | __state__ = "enabled" 51 | 52 | # 定义sso server地址并删除SSO多余参数 53 | sso_server = SSO.get("sso_server").strip("/") 54 | if not url_check(sso_server): 55 | raise 56 | 57 | # 定义请求函数 58 | def sso_request(url, params=None, data=None, timeout=5, num_retries=1): 59 | """ 60 | @params dict: 请求查询参数 61 | @data dict: 提交表单数据 62 | @timeout int: 超时时间,单位秒 63 | @num_retries int: 超时重试次数 64 | """ 65 | headers = {"User-Agent": "Mozilla/5.0 (X11; CentOS; Linux i686; rv:7.0.1406) Gecko/20100101 PassportClient/{}".format(__version__)} 66 | try: 67 | resp = requests.post(url, params=params, headers=headers, timeout=timeout, data=data).json() 68 | except requests.exceptions.Timeout,e: 69 | logger.error(e, exc_info=True) 70 | if num_retries > 0: 71 | return sso_request(url, params=params, data=data, timeout=timeout, num_retries=num_retries-1) 72 | else: 73 | return resp 74 | 75 | # 定义蓝图 76 | sso_blueprint = Blueprint("sso", "sso") 77 | @sso_blueprint.route("/Login") 78 | @anonymous_required 79 | def Login(): 80 | """ Client登录地址,需要跳转到SSO Server上 """ 81 | ReturnUrl = request.args.get("ReturnUrl") or get_referrer_url() or url_for("front.index", _external=True) 82 | if url_check(sso_server): 83 | NextUrl = "{}/sso/?sso={}".format(sso_server, set_ssoparam(ReturnUrl)) 84 | return redirect(NextUrl) 85 | else: 86 | return "Invalid Configuration" 87 | 88 | @sso_blueprint.route("/Logout") 89 | @login_required 90 | def Logout(): 91 | """ Client注销地址,需要跳转到SSO Server上 """ 92 | ReturnUrl = request.args.get("ReturnUrl") or get_referrer_url() or url_for("front.index", _external=True) 93 | NextUrl = "{}/signOut?ReturnUrl={}".format(sso_server, ReturnUrl) 94 | return redirect(NextUrl) 95 | 96 | @sso_blueprint.route("/authorized", methods=["GET", "POST"]) 97 | def authorized(): 98 | """ Client SSO 单点登录、注销入口, 根据`Action`参数判断是`ssoLogin`还是`ssoLogout` """ 99 | Action = request.args.get("Action") 100 | if Action == "ssoLogin": 101 | # 单点登录 102 | ticket = request.args.get("ticket") 103 | if request.method == "GET" and ticket and g.signin == False: 104 | resp = sso_request("{}/sso/validate".format(sso_server), dict(Action="validate_ticket"), dict(ticket=ticket, app_name=SSO["app_name"], get_userinfo=False, get_userbind=False)) 105 | logger.debug("SSO check ticket resp: {}".format(resp)) 106 | if resp and isinstance(resp, dict) and "success" in resp and "uid" in resp: 107 | if resp["success"] is True: 108 | uid = resp["uid"] 109 | sid = resp["sid"] 110 | expire = int(resp["expire"]) 111 | #userinfo = resp["userinfo"] 112 | #logger.debug(userinfo) 113 | # 授权令牌验证通过,设置局部会话,允许登录 114 | sessionId = set_sessionId(uid=uid, seconds=expire, sid=sid) 115 | response = make_response(redirect(get_redirect_url("front.index"))) 116 | response.set_cookie(key="sessionId", value=sessionId, max_age=expire, httponly=True, secure=False if request.url_root.split("://")[0] == "http" else True) 117 | return response 118 | elif Action == "ssoLogout": 119 | # 单点注销 120 | ReturnUrl = request.args.get("ReturnUrl") or get_referrer_url() or url_for("front.index", _external=True) 121 | NextUrl = "{}/signOut?ReturnUrl={}".format(sso_server, ReturnUrl) 122 | app_name = request.args.get("app_name") 123 | if request.method == "GET" and NextUrl and app_name and g.signin == True and app_name == SSO["app_name"]: 124 | response = make_response(redirect(NextUrl)) 125 | response.set_cookie(key="sessionId", value="", expires=0) 126 | return response 127 | elif Action == "ssoConSync": 128 | # 数据同步:参数中必须包含大写的hmac_sha256(app_name:app_id:app_secret)的signature值 129 | signature = request.args.get("signature") 130 | if request.method == "POST" and signature and signature == hmac_sha256("{}:{}:{}".format(SSO["app_name"], SSO["app_id"], SSO["app_secret"])).upper(): 131 | try: 132 | data = json.loads(request.form.get("data")) 133 | ct = data["CallbackType"] 134 | cd = data["CallbackData"] 135 | uid = data["uid"] 136 | token = data["token"] 137 | except Exception,e: 138 | logger.warning(e) 139 | else: 140 | logger.info("ssoConSync with uid: {} -> {}: {}".format(uid, ct, cd)) 141 | resp = sso_request("{}/sso/validate".format(sso_server), dict(Action="validate_sync"), dict(token=token, uid=uid)) 142 | if resp and isinstance(resp, dict) and resp.get("success") is True: 143 | # 之后根据不同类型的ct处理cd 144 | logger.debug("ssoConSync is ok") 145 | return jsonify(msg="Synchronization completed", success=True, app_name=SSO["app_name"]) 146 | return "Invalid Authorized" 147 | 148 | #: 返回插件主类 149 | def getPluginClass(): 150 | return SSOClientMain 151 | 152 | #: 插件主类, 不强制要求名称与插件名一致, 保证getPluginClass准确返回此类 153 | class SSOClientMain(PluginBase): 154 | 155 | def register_bep(self): 156 | """注册蓝图入口, 返回蓝图路由前缀及蓝图名称""" 157 | bep = {"prefix": "/sso", "blueprint": sso_blueprint} 158 | return bep 159 | -------------------------------------------------------------------------------- /demo/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | # public utils -------------------------------------------------------------------------------- /demo/utils/aes_cbc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | demo.utils.aes_cbc 4 | ~~~~~~~~~~~~~~ 5 | 6 | AES加密的实现模式CBC。 7 | CBC使用密码和salt(起扰乱作用)按固定算法(md5)产生key和iv。然后用key和iv(初始向量,加密第一块明文)加密(明文)和解密(密文)。 8 | 9 | :copyright: (c) 2017 by staugur. 10 | :license: MIT, see LICENSE for more details. 11 | """ 12 | 13 | from Crypto.Cipher import AES 14 | from binascii import b2a_hex, a2b_hex 15 | from config import SYSTEM 16 | 17 | 18 | class CBC(): 19 | """密钥生成器""" 20 | 21 | def __init__(self): 22 | # key长度要求16的倍数 23 | self.key = SYSTEM["AES_CBC_KEY"] 24 | self.mode = AES.MODE_CBC 25 | 26 | def encrypt(self, text): 27 | # 加密函数,如果text不是16的倍数【加密文本text必须为16的倍数!】,那就补足为16的倍数 28 | cryptor = AES.new(self.key, self.mode, self.key) 29 | # 这里密钥key 长度必须为16(AES-128)、24(AES-192)、或32(AES-256)Bytes 长度.目前AES-128足够用 30 | length = 16 31 | count = len(text) 32 | add = length - (count % length) 33 | text = text + ('\0' * add) 34 | self.ciphertext = cryptor.encrypt(text) 35 | # 因为AES加密时候得到的字符串不一定是ascii字符集的,输出到终端或者保存时候可能存在问题 36 | # 所以这里统一把加密后的字符串转化为16进制字符串 37 | return b2a_hex(self.ciphertext) 38 | 39 | def decrypt(self, text): 40 | # 解密后,去掉补足的空格用strip() 去掉 41 | cryptor = AES.new(self.key, self.mode, self.key) 42 | plain_text = cryptor.decrypt(a2b_hex(text)) 43 | return plain_text.rstrip('\0') 44 | -------------------------------------------------------------------------------- /demo/utils/jwt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | demo.utils.jwt 4 | ~~~~~~~~~~~~~~ 5 | 6 | Json Web Token 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | import hashlib 13 | import hmac 14 | import time 15 | import datetime 16 | import random 17 | import base64 18 | import json 19 | from config import SYSTEM 20 | from .tool import logger 21 | 22 | 23 | class JWTException(Exception): 24 | pass 25 | 26 | 27 | class SignatureBadError(JWTException): 28 | pass 29 | 30 | 31 | class TokenExpiredError(JWTException): 32 | pass 33 | 34 | 35 | class InvalidTokenError(JWTException): 36 | pass 37 | 38 | 39 | class JWTUtil(object): 40 | """ Json Web Token Utils """ 41 | 42 | def __init__(self): 43 | """ 定义公共变量 44 | @param secretkey str: 签名加密串,建议足够复杂,切勿丢失; 45 | @param audience str: 标准载荷声明中的aud,即接收方; 46 | 标准载荷声明: 47 | iss: 签发者 48 | sub: 主题 49 | aud: 接收方 50 | exp: 过期时间,UNIX时间戳,这个过期时间必须要大于签发时间 51 | nbf: 指定一个UNIX时间戳之前,此token是不可用的 52 | iat: 签发时间,UNIX时间戳 53 | jti: 唯一身份标识 54 | """ 55 | self.secretkey = SYSTEM["JWT_SECRET_KEY"] 56 | self._header = { 57 | "typ": "JWT", 58 | "alg": "HS256" 59 | } 60 | self._payload = { 61 | "iss": "JWTPlugin staugur@saintic.com", 62 | "sub": "Json Web Token", 63 | "aud": "SaintIC Inc.", 64 | "jti": self.md5(self.secretkey), 65 | } 66 | # 标准载荷 67 | self._payloadkey = ("iss", "sub", "aud", "exp", "nbf", "iat", "jti") 68 | 69 | def md5(self, pwd): 70 | """ MD5加密 """ 71 | return hashlib.md5(pwd).hexdigest() 72 | 73 | def get_current_timestamp(self): 74 | """ 获取本地当前时间戳: Unix timestamp:是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒 """ 75 | return int(time.mktime(datetime.datetime.now().timetuple())) 76 | 77 | def timestamp_after_timestamp(self, timestamp=None, seconds=0, minutes=0, hours=0, days=0): 78 | """ 给定时间戳,计算该时间戳之后多少秒、分钟、小时、天的时间戳(本地时间) """ 79 | # 1. 默认时间戳为当前时间 80 | timestamp = self.get_current_timestamp() if timestamp is None else timestamp 81 | # 2. 先转换为datetime 82 | d1 = datetime.datetime.fromtimestamp(timestamp) 83 | # 3. 根据相关时间得到datetime对象并相加给定时间戳的时间 84 | d2 = d1 + datetime.timedelta(seconds=int(seconds), minutes=int(minutes), hours=int(hours), days=int(days)) 85 | # 4. 返回某时间后的时间戳 86 | return int(time.mktime(d2.timetuple())) 87 | 88 | def signatureJWT(self, message): 89 | """ Python generate HMAC-SHA-256 from string """ 90 | return hmac.new( 91 | key=self.secretkey, 92 | msg=message, 93 | digestmod=hashlib.sha256 94 | ).hexdigest() 95 | 96 | def createJWT(self, payload={}, expiredSeconds=3600): 97 | """ 生成token: https://tools.ietf.org/html/rfc7519 98 | @param payload dict: 自定义公有或私有载荷, 存放有效信息的地方; 99 | @param expiredSeconds int: Token过期时间,单位秒,签发时间是本地当前时间戳,此参数指定签发时间之后多少秒过期; 100 | """ 101 | # 1. check params 102 | if isinstance(payload, dict): 103 | for i in self._payloadkey: 104 | if i in payload: 105 | raise KeyError("Standard key exists in payload") 106 | else: 107 | raise TypeError("payload is not a dict") 108 | 109 | # 2. predefined data 110 | payload.update(self._payload) 111 | payload.update( 112 | # exp: 根据秒数生成过期时间戳 113 | exp=self.timestamp_after_timestamp(self.get_current_timestamp(), seconds=expiredSeconds), 114 | # iat: 当前时间戳 115 | iat=self.get_current_timestamp() 116 | ) 117 | 118 | # 3. base64 urlsafe encode 119 | # 头部编码 120 | first_part = base64.urlsafe_b64encode(json.dumps(self._header, sort_keys=True, separators=(',', ':'))) 121 | # 载荷消息体编码 122 | second_part = base64.urlsafe_b64encode(json.dumps(payload, sort_keys=True, separators=(',', ':'))) 123 | # 签名以上两部分: 把header、playload的base64url编码加密后再次base64编码 124 | third_part = base64.urlsafe_b64encode(self.signatureJWT("{0}.{1}".format(first_part, second_part))) 125 | 126 | # 4. returns the available token 127 | token = first_part + '.' + second_part + '.' + third_part 128 | return token 129 | 130 | def analysisJWT(self, token): 131 | """ 解析token, 返回解码后的header、payload、signature等 """ 132 | _header, _payload, _signature = token.split(".") 133 | data = { 134 | "header": json.loads(base64.urlsafe_b64decode(str(_header))), 135 | "payload": json.loads(base64.urlsafe_b64decode(str(_payload))), 136 | "signature": base64.urlsafe_b64decode(str(_signature)) 137 | } 138 | return data 139 | 140 | def verifyJWT(self, token): 141 | """ 验证token 142 | @param token str unicode: 请求生成的token串 143 | >> 1. 验证并拆分token为header、payload、signature, 分别解码验证; 144 | >> 2. 验证header 145 | >> 3. payload一致性验证后, 验证过期时间; 146 | >> 4. 根据header、payload用密钥签名对比请求的signature; 147 | """ 148 | # 1. 拆分解析 149 | if isinstance(token, (str, unicode)): 150 | if token.count(".") == 2: 151 | token = self.analysisJWT(token) 152 | else: 153 | raise InvalidTokenError("invalid token") 154 | else: 155 | raise InvalidTokenError("token is in string or Unicode format") 156 | 157 | # 2. 验证header 158 | if self._header == token["header"]: 159 | payload = token["payload"] 160 | else: 161 | raise InvalidTokenError("header missmatch") 162 | 163 | # 3. 验证payload 164 | for i in self._payloadkey: 165 | if i in ("exp", "iat"): 166 | continue 167 | if payload.get(i) != self._payload.get(i): 168 | raise InvalidTokenError("payload contains standard declaration keys") 169 | if self.get_current_timestamp() > payload["exp"]: 170 | raise TokenExpiredError("token expired") 171 | 172 | # 4. 验证签名 173 | # 头部编码 174 | first_part = base64.urlsafe_b64encode(json.dumps(token["header"], sort_keys=True, separators=(',', ':'))) 175 | # 载荷消息体编码 176 | second_part = base64.urlsafe_b64encode(json.dumps(token["payload"], sort_keys=True, separators=(',', ':'))) 177 | # 签名以上两部分: 把header、playload的base64url编码加密后再次base64编码. 178 | third_part = self.signatureJWT("{0}.{1}".format(first_part, second_part)) 179 | # 校验签名 180 | if token["signature"] == third_part: 181 | return True 182 | else: 183 | raise SignatureBadError("invalid signature") 184 | -------------------------------------------------------------------------------- /demo/utils/log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | demo.utils.log 4 | ~~~~~~~~~~~~~~ 5 | 6 | Define logging base class. 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | import os 13 | import logging, logging.handlers 14 | from config import GLOBAL 15 | 16 | class Logger: 17 | 18 | def __init__(self, logName, backupCount=10): 19 | self.logName = logName 20 | self.log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs') 21 | self.logFile = os.path.join(self.log_dir, '{0}.log'.format(self.logName)) 22 | self._levels = { 23 | "DEBUG" : logging.DEBUG, 24 | "INFO" : logging.INFO, 25 | "WARNING" : logging.WARNING, 26 | "ERROR" : logging.ERROR, 27 | "CRITICAL" : logging.CRITICAL 28 | } 29 | self._logfmt = '%Y-%m-%d %H:%M:%S' 30 | self._logger = logging.getLogger(self.logName) 31 | if not os.path.exists(self.log_dir): os.mkdir(self.log_dir) 32 | 33 | handler = logging.handlers.TimedRotatingFileHandler(filename=self.logFile, 34 | backupCount=backupCount, 35 | when="midnight") 36 | handler.suffix = "%Y%m%d" 37 | formatter = logging.Formatter('[ %(levelname)s ] %(asctime)s %(filename)s:%(threadName)s:%(lineno)d %(message)s', datefmt=self._logfmt) 38 | handler.setFormatter(formatter) 39 | self._logger.addHandler(handler) 40 | self._logger.setLevel(self._levels.get(GLOBAL.get('LogLevel', "INFO"))) 41 | 42 | @property 43 | def getLogger(self): 44 | return self._logger 45 | 46 | if __name__ == "__main__": 47 | syslog = Logger("sys").getLogger 48 | reqlog = Logger("req").getLogger 49 | 50 | syslog.info("sys hello info") 51 | syslog.debug("sys hello debug") 52 | syslog.error("sys hello error") 53 | syslog.warning("sys hello warning") 54 | 55 | reqlog.info("req hello info") 56 | reqlog.debug("req hello debug") 57 | reqlog.error("req hello error") 58 | reqlog.warning("req hello warning") 59 | -------------------------------------------------------------------------------- /demo/utils/tool.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | demo.utils.tool 4 | ~~~~~~~~~~~~~~ 5 | 6 | Common function. 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | import re, hashlib, datetime, time, random, hmac 13 | from uuid import uuid4 14 | from log import Logger 15 | from base64 import b32encode 16 | from config import SYSTEM 17 | from functools import wraps 18 | from flask import g, request, redirect, url_for 19 | 20 | ip_pat = re.compile(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$") 21 | mail_pat = re.compile(r"([0-9a-zA-Z\_*\.*\-*]+)@([a-zA-Z0-9\-*\_*\.*]+)\.([a-zA-Z]+$)") 22 | chinese_pat = re.compile(u"[\u4e00-\u9fa5]+") 23 | Universal_pat = re.compile(r"[a-zA-Z\_][0-9a-zA-Z\_]*") 24 | comma_pat = re.compile(r"\s*,\s*") 25 | logger = Logger("sys").getLogger 26 | err_logger = Logger("error").getLogger 27 | plugin_logger = Logger("plugin").getLogger 28 | access_logger = Logger("access").getLogger 29 | md5 = lambda pwd:hashlib.md5(pwd).hexdigest() 30 | hmac_sha256 = lambda message: hmac.new(key=SYSTEM["HMAC_SHA256_KEY"], msg=message, digestmod=hashlib.sha256).hexdigest() 31 | gen_token = lambda n=32:b32encode(uuid4().hex)[:n] 32 | gen_requestId = lambda :str(uuid4()) 33 | gen_fingerprint = lambda n=16,s=2: ":".join([ "".join(random.sample("0123456789abcdef",s)) for i in range(0, n) ]) 34 | 35 | 36 | def ip_check(ip): 37 | if isinstance(ip, (str, unicode)): 38 | return ip_pat.match(ip) 39 | 40 | def url_check(addr): 41 | """检测UrlAddr是否为有效格式,例如 42 | http://ip:port 43 | https://abc.com 44 | """ 45 | regex = re.compile( 46 | r'^(?:http)s?://' # http:// or https:// 47 | r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... 48 | r'localhost|' #localhost... 49 | r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip 50 | r'(?::\d+)?' # optional port 51 | r'(?:/?|[/?]\S+)$', re.IGNORECASE) 52 | if addr and isinstance(addr, (str, unicode)): 53 | if regex.match(addr): 54 | return True 55 | return False 56 | 57 | def ParseMySQL(mysql, callback="dict"): 58 | protocol, dburl = mysql.split("://") 59 | if "?" in mysql: 60 | dbinfo, dbargs = dburl.split("?") 61 | else: 62 | dbinfo, dbargs = dburl, "charset=utf8&timezone=+8:00" 63 | host,port,user,password,database = dbinfo.split(":") 64 | charset, timezone = dbargs.split("&")[0].split("charset=")[-1] or "utf8", dbargs.split("&")[-1].split("timezone=")[-1] or "+8:00" 65 | if callback in ("list", "tuple"): 66 | return protocol,host,port,user,password,database,charset, timezone 67 | else: 68 | return {"Protocol": protocol, "Host": host, "Port": port, "Database": database, "User": user, "Password": password, "Charset": charset, "Timezone": timezone} 69 | 70 | def get_current_timestamp(): 71 | """ 获取本地当前时间戳(10位): Unix timestamp:是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒 """ 72 | return int(time.time()) 73 | 74 | def timestamp_after_timestamp(timestamp=None, seconds=0, minutes=0, hours=0, days=0): 75 | """ 给定时间戳(10位),计算该时间戳之后多少秒、分钟、小时、天的时间戳(本地时间) """ 76 | # 1. 默认时间戳为当前时间 77 | timestamp = get_current_timestamp() if timestamp is None else timestamp 78 | # 2. 先转换为datetime 79 | d1 = datetime.datetime.fromtimestamp(timestamp) 80 | # 3. 根据相关时间得到datetime对象并相加给定时间戳的时间 81 | d2 = d1 + datetime.timedelta(seconds=int(seconds), minutes=int(minutes), hours=int(hours), days=int(days)) 82 | # 4. 返回某时间后的时间戳 83 | return int(time.mktime(d2.timetuple())) 84 | 85 | def timestamp_to_timestring(timestamp, format='%Y-%m-%d %H:%M:%S'): 86 | """ 将时间戳(10位)转换为可读性的时间 """ 87 | # timestamp为传入的值为时间戳(10位整数),如:1332888820 88 | timestamp = time.localtime(timestamp) 89 | # 经过localtime转换后变成 90 | ## time.struct_time(tm_year=2012, tm_mon=3, tm_mday=28, tm_hour=6, tm_min=53, tm_sec=40, tm_wday=2, tm_yday=88, tm_isdst=0) 91 | # 最后再经过strftime函数转换为正常日期格式。 92 | return time.strftime(format, timestamp) 93 | 94 | def timestring_to_timestamp(timestring, format="%Y-%m-%d %H:%M:%S"): 95 | """ 将普通时间格式转换为时间戳(10位), 形如 '2016-05-05 20:28:54',由format指定 """ 96 | try: 97 | # 转换成时间数组 98 | timeArray = time.strptime(timestring, format) 99 | except Exception: 100 | raise 101 | else: 102 | # 转换成10位时间戳 103 | return int(time.mktime(timeArray)) 104 | 105 | class DO(dict): 106 | """A dict that allows for object-like property access syntax.""" 107 | 108 | def __getattr__(self, name): 109 | try: 110 | return self[name] 111 | except KeyError: 112 | raise AttributeError(name) -------------------------------------------------------------------------------- /demo/utils/web.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | demo.utils.web 4 | ~~~~~~~~~~~~~~ 5 | 6 | Common function for web. 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | from .tool import logger 13 | from .jwt import JWTUtil, JWTException 14 | from .aes_cbc import CBC 15 | from config import SSO 16 | from libs.base import ServiceBase 17 | from functools import wraps 18 | from flask import g, request, redirect, url_for, abort 19 | 20 | jwt = JWTUtil() 21 | cbc = CBC() 22 | 23 | 24 | def get_referrer_url(): 25 | """获取上一页地址""" 26 | if request.referrer and request.referrer.startswith(request.host_url) and request.endpoint and not "api." in request.endpoint: 27 | url = request.referrer 28 | else: 29 | url = None 30 | return url 31 | 32 | 33 | def get_redirect_url(endpoint="front.index"): 34 | """获取重定向地址 35 | NextUrl: 引导重定向下一步地址 36 | ReturnUrl: 最终重定向地址 37 | 以上两个不存在时,如果定义了非默认endpoint,则首先返回;否则返回referrer地址,不存在时返回endpoint默认主页 38 | """ 39 | url = request.args.get('NextUrl') or request.args.get('ReturnUrl') 40 | if not url: 41 | if endpoint != "front.index": 42 | url = url_for(endpoint) 43 | else: 44 | url = get_referrer_url() or url_for(endpoint) 45 | return url 46 | 47 | 48 | def set_ssoparam(ReturnUrl="/"): 49 | """生成sso请求参数,5min过期""" 50 | app_name = SSO.get("app_name") 51 | app_id = SSO.get("app_id") 52 | app_secret = SSO.get("app_secret") 53 | return cbc.encrypt(jwt.createJWT(payload=dict(app_name=app_name, app_id=app_id, app_secret=app_secret, ReturnUrl=ReturnUrl), expiredSeconds=300)) 54 | 55 | 56 | def set_sessionId(uid, seconds=43200, sid=None): 57 | """设置cookie""" 58 | payload = dict(uid=uid, sid=sid) if sid else dict(uid=uid) 59 | sessionId = jwt.createJWT(payload=payload, expiredSeconds=seconds) 60 | return cbc.encrypt(sessionId) 61 | 62 | 63 | def verify_sessionId(cookie): 64 | """验证cookie""" 65 | if cookie: 66 | try: 67 | sessionId = cbc.decrypt(cookie) 68 | except Exception, e: 69 | logger.debug(e) 70 | else: 71 | try: 72 | success = jwt.verifyJWT(sessionId) 73 | except JWTException, e: 74 | logger.debug(e) 75 | else: 76 | # 验证token无误即设置登录态,所以确保解密、验证两处key切不可丢失,否则随意伪造! 77 | return success 78 | return False 79 | 80 | 81 | def analysis_sessionId(cookie, ReturnType="dict"): 82 | """分析获取cookie中payload数据""" 83 | data = dict() 84 | if cookie: 85 | try: 86 | sessionId = cbc.decrypt(cookie) 87 | except Exception, e: 88 | logger.debug(e) 89 | else: 90 | try: 91 | success = jwt.verifyJWT(sessionId) 92 | except JWTException, e: 93 | logger.debug(e) 94 | else: 95 | if success: 96 | # 验证token无误即设置登录态,所以确保解密、验证两处key切不可丢失,否则随意伪造! 97 | data = jwt.analysisJWT(sessionId)["payload"] 98 | if ReturnType == "dict": 99 | return data 100 | else: 101 | return data.get("sid"), data.get("uid") 102 | 103 | 104 | def login_required(f): 105 | @wraps(f) 106 | def decorated_function(*args, **kwargs): 107 | if not g.signin: 108 | return redirect(url_for('sso.Login')) 109 | return f(*args, **kwargs) 110 | return decorated_function 111 | 112 | 113 | def anonymous_required(f): 114 | @wraps(f) 115 | def decorated_function(*args, **kwargs): 116 | if g.signin: 117 | return redirect(g.redirect_uri) 118 | return f(*args, **kwargs) 119 | return decorated_function 120 | -------------------------------------------------------------------------------- /demo/views/FrontView.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | demo.views.FrontView 4 | ~~~~~~~~~~~~~~ 5 | 6 | The blueprint for front view. 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | from flask import Blueprint, g 13 | 14 | 15 | #初始化前台蓝图 16 | FrontBlueprint = Blueprint("front", __name__) 17 | 18 | @FrontBlueprint.route('/') 19 | def index(): 20 | #首页 21 | return "登录状态: {}".format(g.signin) 22 | -------------------------------------------------------------------------------- /demo/views/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | # views blueprint 3 | 4 | from .FrontView import FrontBlueprint 5 | 6 | __all__ = ["FrontBlueprint", ] 7 | -------------------------------------------------------------------------------- /misc/passport.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat MySQL Data Transfer 3 | 4 | Date: 2018-08-08 17:39:09 5 | */ 6 | 7 | SET FOREIGN_KEY_CHECKS=0; 8 | 9 | -- ---------------------------- 10 | -- Table structure for sso_apps 11 | -- ---------------------------- 12 | DROP TABLE IF EXISTS `sso_apps`; 13 | CREATE TABLE `sso_apps` ( 14 | `id` int(11) NOT NULL AUTO_INCREMENT, 15 | `name` varchar(18) NOT NULL COMMENT '应用名称', 16 | `description` varchar(50) NOT NULL COMMENT '应用描述', 17 | `app_id` char(32) NOT NULL COMMENT '应用id', 18 | `app_secret` char(36) NOT NULL COMMENT '应用密钥', 19 | `app_redirect_url` varchar(255) NOT NULL COMMENT '应用回调根地址,即授权、注销前缀', 20 | `ctime` int(10) NOT NULL COMMENT '创建时间', 21 | `mtime` int(10) DEFAULT NULL COMMENT '更新时间', 22 | PRIMARY KEY (`id`), 23 | UNIQUE KEY `name` (`name`) 24 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 25 | 26 | -- ---------------------------- 27 | -- Table structure for user_auth 28 | -- ---------------------------- 29 | DROP TABLE IF EXISTS `user_auth`; 30 | CREATE TABLE `user_auth` ( 31 | `id` bigint(20) NOT NULL AUTO_INCREMENT, 32 | `uid` char(22) NOT NULL COMMENT '用户id', 33 | `identity_type` tinyint(2) unsigned NOT NULL COMMENT '0保留 1手机号 2邮箱 3GitHub 4qq 5微信 6谷歌 7新浪微博 8Coding 9码云', 34 | `identifier` varchar(50) NOT NULL DEFAULT '' COMMENT '手机号、邮箱或第三方应用的唯一标识(openid、union_id)', 35 | `certificate` varchar(106) NOT NULL DEFAULT '' COMMENT '密码凭证(站内的保存密码,站外的不保存或保存token)', 36 | `verified` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否已验证 0-未验证 1-已验证', 37 | `status` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '用户状态 0-禁用 1-启用', 38 | `ctime` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '绑定时间', 39 | `mtime` int(11) unsigned DEFAULT '0' COMMENT '更新绑定时间', 40 | `etime` int(11) unsigned DEFAULT '0' COMMENT '到期时间,特指OAuth2登录', 41 | `refresh_token` varchar(255) DEFAULT '' COMMENT '第三方登录刷新token', 42 | PRIMARY KEY (`id`), 43 | UNIQUE KEY `identifier` (`identifier`) USING BTREE, 44 | KEY `status` (`status`) USING BTREE, 45 | KEY `idx_uid` (`uid`) USING BTREE, 46 | KEY `identity_type` (`identity_type`) USING BTREE 47 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户授权表'; 48 | 49 | -- ---------------------------- 50 | -- Table structure for user_loginlog 51 | -- ---------------------------- 52 | DROP TABLE IF EXISTS `user_loginlog`; 53 | CREATE TABLE `user_loginlog` ( 54 | `id` bigint(20) NOT NULL AUTO_INCREMENT, 55 | `uid` char(22) NOT NULL COMMENT '用户id', 56 | `login_type` tinyint(2) unsigned NOT NULL DEFAULT '1' COMMENT '登录类型 同identity_type', 57 | `login_ip` varchar(15) NOT NULL COMMENT '登录IP', 58 | `login_area` varchar(200) DEFAULT NULL COMMENT '登录地点', 59 | `login_time` int(10) NOT NULL COMMENT '登录时间', 60 | `user_agent` varchar(255) NOT NULL COMMENT '用户代理', 61 | `browser_type` varchar(10) CHARACTER SET utf8 DEFAULT NULL COMMENT '浏览器终端类型,入pc mobile bot tablet', 62 | `browser_device` varchar(30) CHARACTER SET utf8 DEFAULT NULL COMMENT '浏览器设备,如pc,xiaomi,iphone', 63 | `browser_os` varchar(30) CHARACTER SET utf8 DEFAULT NULL COMMENT '浏览器所在操作系统,如windows10,iPhone', 64 | `browser_family` varchar(30) CHARACTER SET utf8 DEFAULT NULL COMMENT '浏览器种类及版本,如chrome 60.0.3122', 65 | PRIMARY KEY (`id`), 66 | KEY `idx_uid_type_time` (`uid`,`login_type`) USING BTREE 67 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='登录日志表'; 68 | 69 | -- ---------------------------- 70 | -- Table structure for user_profile 71 | -- ---------------------------- 72 | DROP TABLE IF EXISTS `user_profile`; 73 | CREATE TABLE `user_profile` ( 74 | `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', 75 | `uid` char(22) NOT NULL COMMENT '用户id', 76 | `register_source` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '注册来源 同identity_type 不可更改', 77 | `register_ip` varchar(15) NOT NULL DEFAULT '' COMMENT '注册IP地址,不可更改', 78 | `nick_name` varchar(49) DEFAULT '' COMMENT '用户昵称', 79 | `domain_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '个性域名', 80 | `gender` tinyint(1) unsigned DEFAULT '2' COMMENT '用户性别 0-女 1-男 2-保密', 81 | `birthday` int(10) unsigned DEFAULT '0' COMMENT '用户生日时间戳', 82 | `signature` varchar(140) DEFAULT '' COMMENT '用户个人签名', 83 | `avatar` varchar(255) DEFAULT 'https://static.saintic.com/cdn/images/defaultAvatar.png' COMMENT '头像', 84 | `location` varchar(50) DEFAULT '' COMMENT '地址', 85 | `ctime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '账号创建时间', 86 | `mtime` int(10) unsigned DEFAULT '0' COMMENT '资料修改时间', 87 | `is_realname` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否实名认证 0-未实名 1-已实名', 88 | `is_admin` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否管理员 0-否 1-是', 89 | `lock_nick_name` int(10) DEFAULT '1' COMMENT '昵称锁,10位有效时间戳内表示加锁,1表示无锁', 90 | `lock_domain_name` tinyint(1) DEFAULT '1' COMMENT '域名锁,0表示加锁 1表示无锁', 91 | PRIMARY KEY (`id`), 92 | UNIQUE KEY `uid` (`uid`), 93 | UNIQUE KEY `dn` (`domain_name`) 94 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户个人资料表'; 95 | 96 | 97 | -- ---------------------------- 98 | -- Table structure for sys_clicklog 99 | -- ---------------------------- 100 | DROP TABLE IF EXISTS `sys_clicklog`; 101 | CREATE TABLE `sys_clicklog` ( 102 | `id` int(6) NOT NULL AUTO_INCREMENT, 103 | `url` varchar(255) COLLATE utf8_unicode_ci NOT NULL, 104 | `agent` varchar(500) COLLATE utf8_unicode_ci NOT NULL, 105 | `method` varchar(8) COLLATE utf8_unicode_ci NOT NULL, 106 | `ip` varchar(15) COLLATE utf8_unicode_ci NOT NULL, 107 | `status_code` char(3) COLLATE utf8_unicode_ci NOT NULL, 108 | `referer` varchar(500) COLLATE utf8_unicode_ci DEFAULT NULL, 109 | `isp` varchar(200) CHARACTER SET utf8 DEFAULT NULL, 110 | `browserType` varchar(10) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '浏览器终端类型,入pc mobile bot tablet', 111 | `browserDevice` varchar(30) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '浏览器设备,如pc,xiaomi,iphone', 112 | `browserOs` varchar(30) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '浏览器所在操作系统,如windows10,iPhone', 113 | `browserFamily` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '浏览器种类及版本,如chrome 60.0.3122', 114 | `clickTime` int(10) DEFAULT '0', 115 | `TimeInterval` varchar(8) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '请求处理的时间秒数', 116 | PRIMARY KEY (`id`), 117 | UNIQUE KEY `id` (`id`) 118 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 119 | -------------------------------------------------------------------------------- /misc/sso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/misc/sso.png -------------------------------------------------------------------------------- /misc/supervisord.conf: -------------------------------------------------------------------------------- 1 | [unix_http_server] 2 | file=/tmp/supervisor.sock 3 | chmod=0777 4 | 5 | [supervisorctl] 6 | serverurl=unix:///tmp/supervisor.sock 7 | 8 | [supervisord] 9 | logfile=/var/log/supervisord.log 10 | logfile_maxbytes=10MB 11 | logfile_backups=1 12 | loglevel=info 13 | pidfile=/tmp/supervisord.pid 14 | nodaemon=true 15 | user=root 16 | 17 | [program:passport] 18 | command=bash online_gunicorn.sh run 19 | directory=/passport 20 | user=root 21 | numprocs=1 22 | startsecs=0 23 | autorestart=true -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=1.0.0,<2.0.0 2 | Werkzeug<1.0.0 3 | Flask-PluginKit>=3.4.0 4 | requests 5 | setproctitle 6 | redis<=2.10.6 7 | torndb==0.3 8 | MySQL-python==1.2.5 9 | gevent==22.10.2 10 | gunicorn==19.10.0 11 | greenlet==2.0.2 12 | shortuuid==0.5.0 13 | user_agents 14 | upyun==2.5.3 15 | pycrypto==2.6.1 16 | bleach==3.3.0 17 | aliyun-python-sdk-dysmsapi==1.0.0 18 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | passport.config 4 | ~~~~~~~~~~~~~~ 5 | 6 | configure file 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | from os import getenv 13 | 14 | GLOBAL = { 15 | "ProcessName": "passport", 16 | # Custom process, you can see it with "ps aux|grep ProcessName". 17 | "Host": getenv("passport_host", "0.0.0.0"), 18 | # Application run network address, you can set it 0.0.0.0, 127.0.0.1. 19 | "Port": getenv("passport_port", 10030), 20 | # Application run port, default port. 21 | "LogLevel": getenv("passport_loglevel", "DEBUG"), 22 | # Application log level, DEBUG, INFO, WARNING, ERROR, CRITICAL. 23 | } 24 | 25 | 26 | MYSQL = getenv("passport_mysql_url") 27 | # 必填-MYSQL数据库连接信息 28 | # mysql://host:port:user:password:database?charset=&timezone= 29 | 30 | 31 | REDIS = getenv("passport_redis_url") 32 | # 必填-Redis数据库连接信息,格式: 33 | # redis://[:password]@host:port/db 34 | # host,port必填项,如有密码,记得密码前加冒号,默认localhost:6379/0 35 | 36 | SECRET_KEY = getenv("passport_secret_key", "gVO9uEqAI5O8c7nHS03rQ0j4") 37 | # 应用密钥 38 | 39 | 40 | # picbed/sapic图床 41 | PICBED = { 42 | "enable": getenv("passport_picbed_enable", getenv("passport_sapic_enable", False)), 43 | "api": getenv("passport_picbed_api", getenv("passport_sapic_api")), 44 | "LinkToken": getenv( 45 | "passport_picbed_linktoken", getenv("passport_sapic_linktoken") 46 | ), 47 | } 48 | 49 | 50 | # 又拍云存储配置 51 | UPYUN = { 52 | "enable": getenv("passport_upyun_enable", False), 53 | "bucket": getenv("passport_upyun_bucket", ""), 54 | "username": getenv("passport_upyun_username", ""), 55 | "password": getenv("passport_upyun_password", ""), 56 | "dn": getenv("passport_upyun_dn", ""), 57 | "basedir": getenv("passport_upyun_basedir", "/passport"), 58 | } 59 | 60 | 61 | # 邮箱配置段:建议 62 | EMAIL = { 63 | "useraddr": getenv("passport_email_useraddr"), 64 | # 邮箱用户:发件人 65 | "userpass": getenv("passport_email_userpass"), 66 | # 用户邮箱密码 67 | "smtpServer": getenv("passport_email_smtpserver"), 68 | # 邮箱服务器地址 69 | "smtpPort": getenv("passport_email_smtpport", 25), 70 | # 邮箱服务器端口 71 | "smtpSSL": getenv("passport_email_smtpssl", False), 72 | # 是否使用SSL加密 73 | } 74 | 75 | # 发送短信配置段:建议 76 | PHONE = { 77 | "ACCESS_KEY_ID": getenv("passport_phone_keyid"), 78 | # 阿里云api密钥key 79 | "ACCESS_KEY_SECRET": getenv("passport_phone_keysecret"), 80 | # 阿里云api密钥secret加密串 81 | "sign_name": getenv("passport_phone_sign_name"), 82 | # 阿里云短信签名名称 83 | "template_code": getenv("passport_phone_template_code"), 84 | # 阿里云短信模版CODE 85 | } 86 | 87 | # 系统配置 88 | SYSTEM = { 89 | "HMAC_SHA256_KEY": getenv( 90 | "passport_hmac_sha256_key", "273d32c8d797fa715190c7408ad73811" 91 | ), 92 | # hmac sha256 key 93 | "AES_CBC_KEY": getenv("passport_aes_cbc_key", "YRRGBRYQqrV1gv5A"), 94 | # utils.aes_cbc.CBC类中所用加密key 95 | "JWT_SECRET_KEY": getenv( 96 | "passport_jwt_secret_key", "WBlE7_#qDf2vRb@vM!Zw#lqrg@rdd3A6" 97 | ), 98 | # utils.jwt.JWTUtil类中所用加密key 99 | "Sign": { 100 | "version": getenv("passport_sign_version", "v1"), 101 | "accesskey_id": getenv("passport_sign_accesskeyid", "accesskey_id"), 102 | "accesskey_secret": getenv("passport_sign_accesskeysecret", "accesskey_secret"), 103 | }, 104 | # utils.Signature.Signature类中所有签名配置 105 | "CACHE_ENABLE": { 106 | "UserAdmin": getenv("passport_cache_useradmin", True), 107 | # 开启管理员用户缓存 108 | "UserProfile": getenv("passport_cache_userprofile", True), 109 | # 开启用户资料缓存 110 | "UserApps": getenv("passport_cache_userapps", True), 111 | # 开启sso应用缓存 112 | }, 113 | # 缓存启用项 114 | "PersonalizedDomainNamePrefix": getenv("passport_personalizeddomainnameprefix", ""), 115 | # 个性域名前缀:业务系统中用户对公个人主页前缀地址,此配置项在github.com/staugur/EauDouce程序中可以体现 116 | "SESSION_EXPIRE": int(getenv("passport_session_expire", 604800)), 117 | # session过期时间,单位秒,默认7d 118 | "EMAIL": getenv("passport_system_email", ""), 119 | # 意见反馈或管理员收件人 120 | "STATUS": getenv("passport_status_url", ""), 121 | # 服务状态地址 122 | "CASENUMBER": getenv("passport_casenumber"), 123 | # ICP备案号 124 | "AUTH_BG_VIDEO_WEBM": getenv("passport_authbgvideo_webm", "/static/videos/bg.webm"), 125 | "AUTH_BG_VIDEO_MP4": getenv("passport_authbgvideo_mp4", "/static/videos/bg.mp4"), 126 | # 登录页背景视频 127 | } 128 | 129 | 130 | # 插件配置段 131 | PLUGINS = { 132 | # 下面几个是第三方登录插件 133 | "weibo": { 134 | "ENABLE": getenv("passport_weibo_enable", False), 135 | "APP_ID": getenv("passport_weibo_appid"), 136 | "APP_KEY": getenv("passport_weibo_appkey"), 137 | "REDIRECT_URI": getenv( 138 | "passport_weibo_redirecturi", 139 | "https://passport.saintic.com/oauth2/weibo/authorized", 140 | ), 141 | }, 142 | "qq": { 143 | "ENABLE": getenv("passport_qq_enable", False), 144 | "APP_ID": getenv("passport_qq_appid"), 145 | "APP_KEY": getenv("passport_qq_appkey"), 146 | "REDIRECT_URI": getenv( 147 | "passport_qq_redirecturi", 148 | "https://passport.saintic.com/oauth2/qq/authorized", 149 | ), 150 | }, 151 | "github": { 152 | "ENABLE": getenv("passport_github_enable", False), 153 | "APP_ID": getenv("passport_github_appid"), 154 | "APP_KEY": getenv("passport_github_appkey"), 155 | "REDIRECT_URI": getenv( 156 | "passport_github_redirecturi", 157 | "https://passport.saintic.com/oauth2/github/authorized", 158 | ), 159 | }, 160 | "coding": { 161 | "ENABLE": getenv("passport_coding_enable", False), 162 | "APP_ID": getenv("passport_coding_appid"), 163 | "APP_KEY": getenv("passport_coding_appkey"), 164 | "REDIRECT_URI": getenv( 165 | "passport_coding_redirecturi", 166 | "https://passport.saintic.com/oauth2/coding/authorized", 167 | ), 168 | }, 169 | "gitee": { 170 | "ENABLE": getenv("passport_gitee_enable", False), 171 | "APP_ID": getenv("passport_gitee_appid"), 172 | "APP_KEY": getenv("passport_gitee_appkey"), 173 | "REDIRECT_URI": getenv( 174 | "passport_gitee_redirecturi", 175 | "https://passport.saintic.com/oauth2/gitee/authorized", 176 | ), 177 | }, 178 | "AccessCount": getenv("passport_accesscount"), 179 | # 访问统计插件 180 | } 181 | -------------------------------------------------------------------------------- /src/hlm/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | # 高层功能封装类 3 | 4 | from ._userapp import UserAppManager 5 | from ._usersso import UserSSOManager 6 | from ._usermsg import UserMsgManager 7 | from ._userprofile import UserProfileManager 8 | 9 | __all__ = ["UserAppManager", "UserSSOManager", "UserMsgManager", "UserProfileManager"] 10 | -------------------------------------------------------------------------------- /src/hlm/_userapp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | passport.hlm._userapp 4 | ~~~~~~~~~~~~~~ 5 | 6 | SSO Client 应用管理 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | import json 13 | from libs.base import ServiceBase 14 | from utils.tool import logger, md5, gen_token, Universal_pat, url_pat, get_current_timestamp 15 | from torndb import IntegrityError 16 | from config import SYSTEM 17 | 18 | 19 | class UserAppManager(ServiceBase): 20 | 21 | def __init__(self): 22 | super(UserAppManager, self).__init__() 23 | self.cache_enable = True if SYSTEM["CACHE_ENABLE"]["UserApps"] in ("true", "True", True) else False 24 | 25 | def getUserApp(self, name): 26 | """ 通过app_name获取应用信息 """ 27 | if name: 28 | res = self.listUserApp() 29 | if res["code"] == 0: 30 | try: 31 | data = ( i for i in res['data'] if i['name'] == name ).next() 32 | except StopIteration: 33 | pass 34 | else: 35 | return data 36 | 37 | def listUserApp(self): 38 | """ 查询userapp应用列表 """ 39 | res = dict(msg=None, code=1) 40 | key = "passport:user:apps" 41 | try: 42 | if self.cache_enable is False: 43 | raise 44 | data = json.loads(self.redis.get(key)) 45 | if data: 46 | logger.info("Hit listUserApps Cache") 47 | else: 48 | raise 49 | except: 50 | sql = "SELECT id,name,description,app_id,app_secret,app_redirect_url,ctime,mtime FROM sso_apps" 51 | try: 52 | data = self.mysql.query(sql) 53 | except Exception, e: 54 | logger.error(e, exc_info=True) 55 | res.update(msg="System is abnormal") 56 | else: 57 | res.update(data=data, code=0) 58 | pipe = self.redis.pipeline() 59 | pipe.set(key, json.dumps(data)) 60 | pipe.expire(key, 600) 61 | pipe.execute() 62 | else: 63 | res.update(data=data, code=0) 64 | return res 65 | 66 | def refreshUserApp(self): 67 | """ 刷新userapp应用列表缓存 """ 68 | key = "passport:user:apps" 69 | return True if self.cache_enable and self.redis.delete(key) == 1 else False 70 | 71 | def createUserApp(self, name, description, app_redirect_url): 72 | """新建userapp应用 73 | @param name str: 应用名 74 | @param description str: 应用描述 75 | @param app_redirect_url str: 回调url 76 | """ 77 | res = dict(msg=None, code=1) 78 | if name and description and app_redirect_url and Universal_pat.match(name) and url_pat.match(app_redirect_url): 79 | app_id = md5(name) 80 | app_secret = gen_token(36) 81 | ctime = get_current_timestamp() 82 | sql = "INSERT INTO sso_apps (name, description, app_id, app_secret, app_redirect_url, ctime) VALUES (%s, %s, %s, %s, %s, %s)" 83 | try: 84 | self.mysql.insert(sql, name, description, app_id, app_secret, app_redirect_url, ctime) 85 | except IntegrityError: 86 | res.update(msg="Name already exists", code=2) 87 | except Exception, e: 88 | logger.error(e, exc_info=True) 89 | res.update(msg="System is abnormal", code=3) 90 | else: 91 | res.update(code=0, refreshCache=self.refreshUserApp()) 92 | else: 93 | res.update(msg="There are invalid parameters", code=4) 94 | return res 95 | 96 | def updateUserApp(self, name, description, app_redirect_url): 97 | """更新userapp应用 98 | @param name str: 应用名 99 | @param description str: 应用描述 100 | @param app_redirect_url str: 回调url 101 | """ 102 | res = dict(msg=None, code=1) 103 | if name and description and app_redirect_url and Universal_pat.match(name) and url_pat.match(app_redirect_url): 104 | mtime = get_current_timestamp() 105 | sql = "UPDATE sso_apps SET description=%s, app_redirect_url=%s, mtime=%s WHERE name=%s" 106 | try: 107 | self.mysql.update(sql, description, app_redirect_url, mtime, name) 108 | except IntegrityError: 109 | res.update(msg="Name already exists", code=2) 110 | except Exception, e: 111 | logger.error(e, exc_info=True) 112 | res.update(msg="System is abnormal", code=3) 113 | else: 114 | res.update(code=0, refreshCache=self.refreshUserApp()) 115 | else: 116 | res.update(msg="There are invalid parameters", code=4) 117 | return res 118 | 119 | def deleteUserApp(self, name): 120 | """删除userapp应用 121 | @param name str: 应用名 122 | """ 123 | res = dict(msg=None, code=1) 124 | if name: 125 | sql = "DELETE FROM sso_apps WHERE name=%s" 126 | try: 127 | self.mysql.execute(sql, name) 128 | except Exception, e: 129 | logger.error(e, exc_info=True) 130 | res.update(msg="System is abnormal", code=3) 131 | else: 132 | res.update(code=0, refreshCache=self.refreshUserApp()) 133 | else: 134 | res.update(msg="There are invalid parameters", code=4) 135 | return res 136 | -------------------------------------------------------------------------------- /src/libs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | # modules open interface -------------------------------------------------------------------------------- /src/libs/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | passport.libs.base 4 | ~~~~~~~~~~~~~~ 5 | 6 | Base class: dependent services, connection information, and public information. 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | import json 13 | from config import SYSTEM 14 | from utils.tool import logger, plugin_logger, create_redis_engine, create_mysql_engine 15 | 16 | 17 | class ServiceBase(object): 18 | """ 所有服务的基类 """ 19 | 20 | def __init__(self): 21 | # 设置全局超时时间(如连接超时) 22 | self.timeout = 2 23 | self.redis = create_redis_engine() 24 | self.mysql = create_mysql_engine() 25 | self.cache_admin = True if SYSTEM["CACHE_ENABLE"]["UserAdmin"] in ("true", "True", True) else False 26 | 27 | @property 28 | def listAdminUsers(self): 29 | """ 管理员用户列表缓存 """ 30 | key = "passport:user:admins" 31 | try: 32 | if self.cache_admin is False: 33 | raise 34 | data = json.loads(self.redis.get(key)) 35 | if data: 36 | logger.info("Hit listAdminUsers Cache") 37 | else: 38 | raise 39 | except: 40 | sql = "SELECT uid FROM user_profile WHERE is_admin = 1" 41 | data = [item['uid'] for item in self.mysql.query(sql)] 42 | try: 43 | pipe = self.redis.pipeline() 44 | pipe.set(key, json.dumps(data)) 45 | pipe.expire(key, 600) 46 | pipe.execute() 47 | except Exception, e: 48 | logger.error(e, exc_info=True) 49 | return data 50 | 51 | @property 52 | def refreshAdminUsers(self): 53 | """ 刷新管理员列表缓存 """ 54 | key = "passport:user:admins" 55 | return True if self.cache_admin and self.redis.delete(key) == 1 else False 56 | 57 | def isAdmin(self, uid): 58 | """ 判断是否为管理员 """ 59 | return uid in self.listAdminUsers 60 | 61 | 62 | class PluginBase(ServiceBase): 63 | """ 插件基类: 提供插件所需要的公共接口与扩展点 """ 64 | 65 | def __init__(self): 66 | super(PluginBase, self).__init__() 67 | self.logger = plugin_logger 68 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | passport.main 4 | ~~~~~~~~~~~~~~ 5 | 6 | Entrance 7 | 8 | Docstring conventions: 9 | http://flask.pocoo.org/docs/0.10/styleguide/#docstrings 10 | 11 | Comments: 12 | http://flask.pocoo.org/docs/0.10/styleguide/#comments 13 | 14 | :copyright: (c) 2017 by staugur. 15 | :license: MIT, see LICENSE for more details. 16 | """ 17 | 18 | import jinja2 19 | import os 20 | import sys 21 | import config 22 | import time 23 | from version import __version__ 24 | from utils.tool import ( 25 | logger, 26 | err_logger, 27 | access_logger, 28 | create_redis_engine, 29 | create_mysql_engine, 30 | DO, 31 | ) 32 | from utils.web import ( 33 | verify_sessionId, 34 | analysis_sessionId, 35 | tpl_adminlogin_required, 36 | get_redirect_url, 37 | get_ip, 38 | ) 39 | from hlm import UserAppManager, UserSSOManager, UserMsgManager, UserProfileManager 40 | from views import FrontBlueprint, ApiBlueprint 41 | from flask import request, g, jsonify 42 | from flask_pluginkit import Flask, PluginManager 43 | 44 | reload(sys) 45 | sys.setdefaultencoding("utf-8") 46 | 47 | __author__ = "staugur" 48 | __email__ = "staugur@saintic.com" 49 | __doc__ = "统一认证与单点登录系统" 50 | __date__ = "2018-01-09" 51 | 52 | 53 | # 初始化定义application 54 | app = Flask(__name__) 55 | app.config.update(SECRET_KEY=config.SECRET_KEY, MAX_CONTENT_LENGTH=4 * 1024 * 1024) 56 | 57 | # 初始化接口管理器 58 | api = DO( 59 | { 60 | "userapp": UserAppManager(), 61 | "usersso": UserSSOManager(), 62 | "usermsg": UserMsgManager(), 63 | "userprofile": UserProfileManager(), 64 | } 65 | ) 66 | 67 | # 初始化插件管理器(自动扫描并加载运行) 68 | plugin = PluginManager(app) 69 | 70 | # 注册视图包中蓝图 71 | app.register_blueprint(FrontBlueprint) 72 | app.register_blueprint(ApiBlueprint, url_prefix="/api") 73 | 74 | 75 | # 添加模板上下文变量 76 | @app.context_processor 77 | def GlobalTemplateVariables(): 78 | data = { 79 | "Version": __version__, 80 | "Author": __author__, 81 | "Email": __email__, 82 | "Doc": __doc__, 83 | "CONFIG": config, 84 | "tpl_adminlogin_required": tpl_adminlogin_required, 85 | } 86 | return data 87 | 88 | 89 | @app.before_request 90 | def before_request(): 91 | sessionId = request.cookies.get("sessionId", request.headers.get("sessionId")) 92 | g.startTime = time.time() 93 | g.redis = create_redis_engine() 94 | g.mysql = create_mysql_engine() 95 | g.signin = verify_sessionId(sessionId) 96 | g.sid, g.uid = analysis_sessionId(sessionId, "tuple") if g.signin else (None, None) 97 | logger.debug("uid: {}, sid: {}".format(g.uid, g.sid)) 98 | g.api = api 99 | g.ip = get_ip() 100 | g.agent = request.headers.get("User-Agent") 101 | # 仅是重定向页面快捷定义 102 | g.redirect_uri = get_redirect_url() 103 | 104 | 105 | @app.after_request 106 | def after_request(response): 107 | response.headers["Access-Control-Allow-Origin"] = "*" 108 | response.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS" 109 | response.headers["Access-Control-Allow-Headers"] = ( 110 | "sessionId,Authorization,X-Requested-With" 111 | ) 112 | return response 113 | 114 | 115 | @app.teardown_request 116 | def teardown_request(exception): 117 | if exception: 118 | err_logger.error(exception, exc_info=True) 119 | if hasattr(g, "redis"): 120 | g.redis.connection_pool.disconnect() 121 | if hasattr(g, "mysql"): 122 | g.mysql.close() 123 | 124 | 125 | @app.errorhandler(500) 126 | def server_error(error=None): 127 | if error: 128 | err_logger.error("500: {}".format(error), exc_info=True) 129 | message = {"msg": "Server Error", "code": 500} 130 | return jsonify(message), 500 131 | 132 | 133 | @app.errorhandler(404) 134 | def not_found(error=None): 135 | message = { 136 | "code": 404, 137 | "msg": "Not Found: " + request.url, 138 | } 139 | resp = jsonify(message) 140 | resp.status_code = 404 141 | return resp 142 | 143 | 144 | @app.errorhandler(403) 145 | def Permission_denied(error=None): 146 | message = {"msg": "Authentication failed, permission denied.", "code": 403} 147 | return jsonify(message), 403 148 | 149 | 150 | if __name__ == "__main__": 151 | from werkzeug.contrib.fixers import ProxyFix 152 | 153 | app.wsgi_app = ProxyFix(app.wsgi_app) 154 | app.run(host=config.GLOBAL["Host"], port=int(config.GLOBAL["Port"]), debug=True) 155 | -------------------------------------------------------------------------------- /src/online_gunicorn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | #使用gunicorn启动, 要求系统安装了gunicorn, gevent 4 | # 5 | 6 | dir=$(cd $(dirname $0); pwd) 7 | cd $dir 8 | 9 | #定义变量(自定义更改) 10 | 11 | #准备环境 12 | if [ -r online_preboot.sh ]; then 13 | . online_preboot.sh 14 | fi 15 | 16 | #定义常量(请勿更改) 17 | host=$(python -c "from config import GLOBAL;print GLOBAL['Host']") 18 | port=$(python -c "from config import GLOBAL;print GLOBAL['Port']") 19 | procname=$(python -c "from config import GLOBAL;print GLOBAL['ProcessName']") 20 | cpu_count=$(cat /proc/cpuinfo | grep "processor" | wc -l) 21 | [ -d ${dir}/logs ] || mkdir -p ${dir}/logs 22 | logfile=${dir}/logs/gunicorn.log 23 | pidfile=${dir}/logs/${procname}.pid 24 | 25 | function Monthly2Number() { 26 | case "$1" in 27 | Jan) echo 1;; 28 | Feb) echo 2;; 29 | Mar) echo 3;; 30 | Apr) echo 4;; 31 | May) echo 5;; 32 | Jun) echo 6;; 33 | Jul) echo 7;; 34 | Aug) echo 8;; 35 | Sep) echo 9;; 36 | Oct) echo 10;; 37 | Nov) echo 11;; 38 | Dec) echo 12;; 39 | *) exit;; 40 | esac 41 | } 42 | 43 | case $1 in 44 | start) 45 | if [ -f $pidfile ]; then 46 | echo "Has pid($(cat $pidfile)) in $pidfile, please check, exit." ; exit 1 47 | else 48 | gunicorn -w $cpu_count --threads 16 -b ${host}:${port} main:app -k gevent --daemon --pid $pidfile --log-file $logfile --max-requests 250 --name $procname 49 | sleep 1 50 | pid=$(cat $pidfile) 51 | [ "$?" != "0" ] && exit 1 52 | echo "$procname start over with pid ${pid}" 53 | fi 54 | ;; 55 | 56 | run) 57 | #前台运行 58 | gunicorn -w $cpu_count --threads 16 -b ${host}:${port} main:app -k gevent --max-requests 250 --name $procname 59 | ;; 60 | 61 | entrypoint) 62 | #以docker-entrypoint方式启动 63 | exec gunicorn -w $cpu_count --threads 16 -b ${host}:${port} main:app -k gevent --max-requests 250 --name $procname 64 | ;; 65 | 66 | stop) 67 | if [ ! -f $pidfile ]; then 68 | echo "$pidfile does not exist, process is not running" 69 | else 70 | echo "Stopping ${procname}..." 71 | pid=$(cat $pidfile) 72 | kill $pid 73 | while [ -x /proc/${pid} ] 74 | do 75 | echo "Waiting for ${procname} to shutdown ..." 76 | kill $pid ; sleep 1 77 | done 78 | echo "${procname} stopped" 79 | rm -f $pidfile 80 | fi 81 | ;; 82 | 83 | status) 84 | if [ ! -f $pidfile ]; then 85 | echo -e "\033[39;31m${procname} has stopped.\033[0m" 86 | exit 87 | fi 88 | pid=$(cat $pidfile) 89 | procnum=$(ps aux | grep -v grep | grep $pid | grep $procname | wc -l) 90 | m=$(ps -eO lstart | grep $pid | grep $procname | grep -vE "worker|grep|Team.Api\." | awk '{print $3}') 91 | t=$(Monthly2Number $m) 92 | if [[ "$procnum" != "1" ]]; then 93 | echo -e "\033[39;31m异常,pid文件与系统pid数量不相等。\033[0m" 94 | echo -e "\033[39;34m pid数量:${procnum}\033[0m" 95 | echo -e "\033[39;34m pid文件:${pid}($pidfile)\033[0m" 96 | else 97 | echo -e "\033[39;33m${procname}\033[0m": 98 | echo " pid: $pid" 99 | echo -e " state:" "\033[39;32mrunning\033[0m" 100 | echo -e " process start time:" "\033[39;32m$(ps -eO lstart | grep $pid | grep $procname | grep -vE "worker|grep|Team.Api\." | awk '{print $6"-"$3"-"$4,$5}' | sed "s/${m}/${t}/")\033[0m" 101 | echo -e " process running time:" "\033[39;32m$(ps -eO etime| grep $pid | grep $procname | grep -vE "worker|grep|Team.Api\." | awk '{print $2}')\033[0m" 102 | fi 103 | ;; 104 | 105 | reload) 106 | if [ -f $pidfile ]; then 107 | kill -HUP $(cat $pidfile) 108 | fi 109 | ;; 110 | 111 | restart) 112 | bash $(basename $0) stop 113 | bash $(basename $0) start 114 | ;; 115 | 116 | *) 117 | echo "Usage: $0 start|run|stop|reload|restart|status" 118 | ;; 119 | esac 120 | -------------------------------------------------------------------------------- /src/plugins/AccessCount/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Passport.plugins.AccessCount 4 | ~~~~~~~~~~~~~~ 5 | 6 | PV and IP plugins for statistical access. 7 | 8 | :copyright: (c) 2018 by taochengwei. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | from __future__ import absolute_import 13 | from utils.tool import plugin_logger, get_current_timestamp 14 | from libs.base import PluginBase 15 | from config import PLUGINS 16 | from flask import request, g 17 | import datetime, time, json 18 | 19 | __plugin_name__ = "AccessCount" 20 | __description__ = "IP、PV、UV统计插件" 21 | __author__ = "staugur" 22 | __version__ = "0.1.0" 23 | __license__ = "MIT" 24 | if PLUGINS["AccessCount"] in ("true", "True", True): 25 | __state__ = "enabled" 26 | else: 27 | __state__ = "disabled" 28 | 29 | 30 | def getPluginClass(): 31 | return AccessCount 32 | 33 | 34 | class AccessCount(PluginBase): 35 | """ 记录与统计每天访问数据 """ 36 | 37 | @property 38 | def get_today(self): 39 | """ 获取现在时间可见串 """ 40 | return datetime.datetime.now().strftime("%Y%m%d") 41 | 42 | def Record_ip_pv(self, *args, **kwargs): 43 | """ 记录ip、ip、uv """ 44 | resp = kwargs.get("response") or args[0] 45 | data = { 46 | "status_code": resp.status_code, 47 | "method": request.method, 48 | "ip": g.ip, 49 | "url": request.url, 50 | "referer": request.headers.get('Referer'), 51 | "agent": request.headers.get("User-Agent"), 52 | "TimeInterval": "%0.2fs" %float(time.time() - g.startTime), 53 | "clickTime": get_current_timestamp() 54 | } 55 | pvKey = "passport:AccessCount:pv" 56 | uvKey = "passport:AccessCount:uv" 57 | clickKey = "passport:AccessCount:clicklog" 58 | pipe = self.redis.pipeline() 59 | pipe.hincrby(pvKey, self.get_today, 1) 60 | pipe.hincrby(uvKey, request.base_url, 1) 61 | #pipe.rpush(clickKey, json.dumps(data)) 62 | try: 63 | pipe.execute() 64 | except: 65 | pass 66 | 67 | def register_hep(self): 68 | return {"after_request_hook": self.Record_ip_pv} 69 | 70 | def register(): 71 | return dict( 72 | hep=dict(after_request=AccessCount().Record_ip_pv) 73 | ) 74 | -------------------------------------------------------------------------------- /src/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | # Plugins Package -------------------------------------------------------------------------------- /src/plugins/oauth2_coding/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | passport.plugins.oauth2_coding 4 | ~~~~~~~~~~~~~~ 5 | 6 | 使用coding登录 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | #: Importing these two modules is the first and must be done. 13 | #: 首先导入这两个必须模块 14 | from __future__ import absolute_import 15 | from libs.base import PluginBase 16 | #: Import the other modules here, and if it's your own module, use the relative Import. eg: from .lib import Lib 17 | #: 在这里导入其他模块, 如果有自定义包目录, 使用相对导入, 如: from .lib import Lib 18 | from flask import Blueprint, request, jsonify, g, flash, redirect, url_for 19 | from utils.web import OAuth2, dfr, oauth2_name2type, checkGet_ssoRequest, oauth2_genderconverter 20 | from config import PLUGINS 21 | from libs.auth import Authentication 22 | 23 | #:Your plug-in name must be consistent with the plug-in directory name. 24 | #:你的插件名称,必须和插件目录名称等保持一致. 25 | __plugin_name__ = "oauth2_coding" 26 | #: Plugin describes information. What does it do? 27 | #: 插件描述信息,什么用处. 28 | __description__ = "Connection Coding with OAuth2" 29 | #: Plugin Author 30 | #: 插件作者 31 | __author__ = "Mr.tao " 32 | #: Plugin Version 33 | #: 插件版本 34 | __version__ = "0.1.0" 35 | #: Plugin Url 36 | #: 插件主页 37 | __url__ = "https://www.saintic.com" 38 | #: Plugin License 39 | #: 插件许可证 40 | __license__ = "MIT" 41 | #: Plugin License File 42 | #: 插件许可证文件 43 | __license_file__= "LICENSE" 44 | #: Plugin Readme File 45 | #: 插件自述文件 46 | __readme_file__ = "README" 47 | #: Plugin state, enabled or disabled, default: enabled 48 | #: 插件状态, enabled、disabled, 默认enabled 49 | name = "coding" 50 | if PLUGINS[name]["ENABLE"] in ("true", "True", True): 51 | __state__ = "enabled" 52 | else: 53 | __state__ = "disabled" 54 | 55 | coding = OAuth2(name, 56 | client_id = PLUGINS[name]["APP_ID"], 57 | client_secret = PLUGINS[name]["APP_KEY"], 58 | redirect_url = PLUGINS[name]["REDIRECT_URI"], 59 | authorize_url = "https://coding.net/oauth_authorize.html", 60 | access_token_url = "https://coding.net/api/oauth/access_token", 61 | get_userinfo_url = "https://coding.net/api/account/current_user", 62 | scope = "user", 63 | verify_state = False, 64 | ) 65 | 66 | plugin_blueprint = Blueprint("oauth2_coding", "oauth2_coding") 67 | @plugin_blueprint.route("/login") 68 | def login(): 69 | """ 跳转此OAuth应用登录以授权 70 | 此路由地址:/oauth2/coding/login 71 | """ 72 | return coding.authorize() 73 | 74 | @plugin_blueprint.route("/authorized") 75 | def authorized(): 76 | """ 授权回调路由 77 | 此路由地址:/oauth2/coding/authorized 78 | """ 79 | # 加密的sso参数值 80 | sso = request.args.get("sso") or None 81 | # 换取access_token 82 | resp = coding.authorized_response() 83 | if resp and isinstance(resp, dict) and "access_token" in resp: 84 | # 根据access_token获取用户基本信息 85 | user = coding.get_userinfo(resp["access_token"]) 86 | if user["code"] != 0: 87 | flash(user["msg"].keys()) 88 | return redirect(g.redirect_uri) 89 | user = user["data"] 90 | # 处理第三方登录逻辑 91 | auth = Authentication(g.mysql, g.redis) 92 | # 第三方账号登录入口`oauth2_go` 93 | avatar = "https://coding.net" + user["avatar"] if user["avatar"].startswith("/") else user["avatar"] 94 | goinfo = auth.oauth2_go(name=name, signin=g.signin, tokeninfo=resp, userinfo=dict(openid=user["id"], nick_name=user["name"], gender=oauth2_genderconverter(user["sex"]), avatar=avatar, domain_name=user["global_key"], signature=user["slogan"], location=user.get("location")), uid=g.uid) 95 | goinfo = dfr(goinfo) 96 | if goinfo["pageAction"] == "goto_signIn": 97 | """ 未登录流程->已经绑定过账号,需要设置登录态 """ 98 | uid = goinfo["goto_signIn_data"]["guid"] 99 | # 记录登录日志 100 | auth.brush_loginlog(dict(identity_type=oauth2_name2type(name), uid=uid, success=True), login_ip=g.ip, user_agent=request.headers.get("User-Agent")) 101 | # 设置登录态 102 | return coding.goto_signIn(uid=uid, sso=sso) 103 | elif goinfo["pageAction"] == "goto_signUp": 104 | """ 未登录流程->执行注册绑定功能 """ 105 | return coding.goto_signUp(openid=goinfo["goto_signUp_data"]["openid"], sso=sso) 106 | else: 107 | # 已登录流程->正在绑定第三方账号:反馈绑定结果 108 | if goinfo["success"]: 109 | # 绑定成功,返回原页面 110 | flash(u"已绑定") 111 | else: 112 | # 绑定失败,返回原页面 113 | flash(goinfo["msg"]) 114 | # 跳回绑定设置页面 115 | return redirect(url_for("front.userset", _anchor="bind")) 116 | else: 117 | flash(u'Access denied: reason=%s error=%s' % ( 118 | request.args.get('error'), 119 | request.args.get('error_description') 120 | )) 121 | return redirect(g.redirect_uri) 122 | 123 | #: 返回插件主类 124 | def getPluginClass(): 125 | return OAuth2_Coding_Main 126 | 127 | #: 插件主类, 不强制要求名称与插件名一致, 保证getPluginClass准确返回此类 128 | class OAuth2_Coding_Main(PluginBase): 129 | """ 继承自PluginBase基类 """ 130 | 131 | def register_tep(self): 132 | """注册模板入口, 返回扩展点名称及扩展的代码, 其中include点必须是实际的HTML文件, string点必须是HTML代码.""" 133 | tep = {"auth_signIn_socialLogin_include": "connect_coding.html"} 134 | return tep 135 | 136 | def register_bep(self): 137 | """注册蓝图入口, 返回蓝图路由前缀及蓝图名称""" 138 | bep = {"prefix": "/oauth2/coding", "blueprint": plugin_blueprint} 139 | return bep 140 | 141 | def register(): 142 | om = OAuth2_Coding_Main() 143 | return dict( 144 | tep=om.register_tep(), 145 | bep=om.register_bep(), 146 | ) 147 | -------------------------------------------------------------------------------- /src/plugins/oauth2_coding/templates/connect_coding.html: -------------------------------------------------------------------------------- 1 | {%- if request.args.sso %} 2 | {% set url = url_for('oauth2_coding.login', sso=request.args.sso) %} 3 | {%- else %} 4 | {% set url = url_for('oauth2_coding.login') %} 5 | {%- endif -%} 6 | 7 |   -------------------------------------------------------------------------------- /src/plugins/oauth2_gitee/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | passport.plugins.oauth2_gitee 4 | ~~~~~~~~~~~~~~ 5 | 6 | 使用gitee登录 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | #: Importing these two modules is the first and must be done. 13 | #: 首先导入这两个必须模块 14 | from __future__ import absolute_import 15 | from libs.base import PluginBase 16 | #: Import the other modules here, and if it's your own module, use the relative Import. eg: from .lib import Lib 17 | #: 在这里导入其他模块, 如果有自定义包目录, 使用相对导入, 如: from .lib import Lib 18 | from flask import Blueprint, request, jsonify, g, flash, redirect, url_for 19 | from utils.web import OAuth2, dfr, oauth2_name2type, checkGet_ssoRequest 20 | from config import PLUGINS 21 | from libs.auth import Authentication 22 | 23 | #:Your plug-in name must be consistent with the plug-in directory name. 24 | #:你的插件名称,必须和插件目录名称等保持一致. 25 | __plugin_name__ = "oauth2_gitee" 26 | #: Plugin describes information. What does it do? 27 | #: 插件描述信息,什么用处. 28 | __description__ = "Connection Gitee with OAuth2" 29 | #: Plugin Author 30 | #: 插件作者 31 | __author__ = "Mr.tao " 32 | #: Plugin Version 33 | #: 插件版本 34 | __version__ = "0.1.0" 35 | #: Plugin Url 36 | #: 插件主页 37 | __url__ = "https://www.saintic.com" 38 | #: Plugin License 39 | #: 插件许可证 40 | __license__ = "MIT" 41 | #: Plugin License File 42 | #: 插件许可证文件 43 | __license_file__= "LICENSE" 44 | #: Plugin Readme File 45 | #: 插件自述文件 46 | __readme_file__ = "README" 47 | #: Plugin state, enabled or disabled, default: enabled 48 | #: 插件状态, enabled、disabled, 默认enabled 49 | name = "gitee" 50 | if PLUGINS[name]["ENABLE"] in ("true", "True", True): 51 | __state__ = "enabled" 52 | else: 53 | __state__ = "disabled" 54 | 55 | gitee = OAuth2(name, 56 | client_id = PLUGINS[name]["APP_ID"], 57 | client_secret = PLUGINS[name]["APP_KEY"], 58 | redirect_url = PLUGINS[name]["REDIRECT_URI"], 59 | authorize_url = "https://gitee.com/oauth/authorize", 60 | access_token_url = "https://gitee.com/oauth/token", 61 | get_userinfo_url = "https://gitee.com/api/v5/user", 62 | scope = "user_info" 63 | ) 64 | 65 | plugin_blueprint = Blueprint("oauth2_gitee", "oauth2_gitee") 66 | @plugin_blueprint.route("/login") 67 | def login(): 68 | """ 跳转此OAuth应用登录以授权 69 | 此路由地址:/oauth2/gitee/login 70 | """ 71 | return gitee.authorize() 72 | 73 | @plugin_blueprint.route("/authorized") 74 | def authorized(): 75 | """ 授权回调路由 76 | 此路由地址:/oauth2/gitee/authorized 77 | """ 78 | # 加密的sso参数值 79 | sso = request.args.get("sso") or None 80 | # 换取access_token 81 | resp = gitee.authorized_response() 82 | print resp 83 | if resp and isinstance(resp, dict) and "access_token" in resp: 84 | # 根据access_token获取用户基本信息 85 | user = gitee.get_userinfo(resp["access_token"]) 86 | if not "id" in user: 87 | flash(user.get("status", "Gitee error")) 88 | return redirect(g.redirect_uri) 89 | # 处理第三方登录逻辑 90 | auth = Authentication(g.mysql, g.redis) 91 | # 第三方账号登录入口`oauth2_go` 92 | goinfo = auth.oauth2_go(name=name, signin=g.signin, tokeninfo=resp, userinfo=dict(openid=user["id"], nick_name=user["name"], gender=2, avatar=user["avatar_url"], domain_name=user["login"], signature=user["bio"]), uid=g.uid) 93 | goinfo = dfr(goinfo) 94 | if goinfo["pageAction"] == "goto_signIn": 95 | """ 未登录流程->已经绑定过账号,需要设置登录态 """ 96 | uid = goinfo["goto_signIn_data"]["guid"] 97 | # 记录登录日志 98 | auth.brush_loginlog(dict(identity_type=oauth2_name2type(name), uid=uid, success=True), login_ip=g.ip, user_agent=request.headers.get("User-Agent")) 99 | # 设置登录态 100 | return gitee.goto_signIn(uid=uid, sso=sso) 101 | elif goinfo["pageAction"] == "goto_signUp": 102 | """ 未登录流程->执行注册绑定功能 """ 103 | return gitee.goto_signUp(openid=goinfo["goto_signUp_data"]["openid"], sso=sso) 104 | else: 105 | # 已登录流程->正在绑定第三方账号:反馈绑定结果 106 | if goinfo["success"]: 107 | # 绑定成功,返回原页面 108 | flash(u"已绑定") 109 | else: 110 | # 绑定失败,返回原页面 111 | flash(goinfo["msg"]) 112 | # 跳回绑定设置页面 113 | return redirect(url_for("front.userset", _anchor="bind")) 114 | else: 115 | flash(u'Access denied: reason=%s error=%s' % ( 116 | resp.get('error'), 117 | resp.get('error_description') 118 | )) 119 | return redirect(g.redirect_uri) 120 | 121 | #: 返回插件主类 122 | def getPluginClass(): 123 | return OAuth2_Gitee_Main 124 | 125 | #: 插件主类, 不强制要求名称与插件名一致, 保证getPluginClass准确返回此类 126 | class OAuth2_Gitee_Main(PluginBase): 127 | """ 继承自PluginBase基类 """ 128 | 129 | def register_tep(self): 130 | """注册模板入口, 返回扩展点名称及扩展的代码, 其中include点必须是实际的HTML文件, string点必须是HTML代码.""" 131 | tep = {"auth_signIn_socialLogin_include": "connect_gitee.html"} 132 | return tep 133 | 134 | def register_bep(self): 135 | """注册蓝图入口, 返回蓝图路由前缀及蓝图名称""" 136 | bep = {"prefix": "/oauth2/gitee", "blueprint": plugin_blueprint} 137 | return bep 138 | 139 | def register(): 140 | om = OAuth2_Gitee_Main() 141 | return dict( 142 | tep=om.register_tep(), 143 | bep=om.register_bep(), 144 | ) 145 | 146 | -------------------------------------------------------------------------------- /src/plugins/oauth2_gitee/templates/connect_gitee.html: -------------------------------------------------------------------------------- 1 | {%- if request.args.sso %} 2 | {% set url = url_for('oauth2_gitee.login', sso=request.args.sso) %} 3 | {%- else %} 4 | {% set url = url_for('oauth2_gitee.login') %} 5 | {%- endif -%} 6 | 7 |   -------------------------------------------------------------------------------- /src/plugins/oauth2_github/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | passport.plugins.oauth2_github 4 | ~~~~~~~~~~~~~~ 5 | 6 | 使用GitHub登录 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | #: Importing these two modules is the first and must be done. 13 | #: 首先导入这两个必须模块 14 | from __future__ import absolute_import 15 | from libs.base import PluginBase 16 | #: Import the other modules here, and if it's your own module, use the relative Import. eg: from .lib import Lib 17 | #: 在这里导入其他模块, 如果有自定义包目录, 使用相对导入, 如: from .lib import Lib 18 | from flask import Blueprint, request, jsonify, g, flash, redirect, url_for 19 | from utils.web import OAuth2, dfr, oauth2_name2type, checkGet_ssoRequest 20 | from config import PLUGINS 21 | from libs.auth import Authentication 22 | 23 | #:Your plug-in name must be consistent with the plug-in directory name. 24 | #:你的插件名称,必须和插件目录名称等保持一致. 25 | __plugin_name__ = "oauth2_github" 26 | #: Plugin describes information. What does it do? 27 | #: 插件描述信息,什么用处. 28 | __description__ = "Connection GitHub with OAuth2" 29 | #: Plugin Author 30 | #: 插件作者 31 | __author__ = "Mr.tao " 32 | #: Plugin Version 33 | #: 插件版本 34 | __version__ = "0.1.0" 35 | #: Plugin Url 36 | #: 插件主页 37 | __url__ = "https://www.saintic.com" 38 | #: Plugin License 39 | #: 插件许可证 40 | __license__ = "MIT" 41 | #: Plugin License File 42 | #: 插件许可证文件 43 | __license_file__= "LICENSE" 44 | #: Plugin Readme File 45 | #: 插件自述文件 46 | __readme_file__ = "README" 47 | #: Plugin state, enabled or disabled, default: enabled 48 | #: 插件状态, enabled、disabled, 默认enabled 49 | name = "github" 50 | if PLUGINS[name]["ENABLE"] in ("true", "True", True): 51 | __state__ = "enabled" 52 | else: 53 | __state__ = "disabled" 54 | 55 | github = OAuth2(name, 56 | client_id = PLUGINS[name]["APP_ID"], 57 | client_secret = PLUGINS[name]["APP_KEY"], 58 | redirect_url = PLUGINS[name]["REDIRECT_URI"], 59 | authorize_url = "https://github.com/login/oauth/authorize", 60 | access_token_url = "https://github.com/login/oauth/access_token", 61 | get_userinfo_url = "https://api.github.com/user" 62 | ) 63 | 64 | plugin_blueprint = Blueprint("oauth2_github", "oauth2_github") 65 | @plugin_blueprint.route("/login") 66 | def login(): 67 | """ 跳转此OAuth应用登录以授权 68 | 此路由地址:/oauth2/github/login 69 | """ 70 | return github.authorize() 71 | 72 | @plugin_blueprint.route("/authorized") 73 | def authorized(): 74 | """ 授权回调路由 75 | 此路由地址:/oauth2/github/authorized 76 | """ 77 | # 加密的sso参数值 78 | sso = request.args.get("sso") or None 79 | # 换取access_token 80 | resp = github.authorized_response() 81 | resp = github.url_code(resp) 82 | if resp and isinstance(resp, dict) and "access_token" in resp: 83 | # 根据access_token获取用户基本信息 84 | user = github.get_userinfo_for_github(resp["access_token"]) 85 | # 处理第三方登录逻辑 86 | auth = Authentication(g.mysql, g.redis) 87 | # 第三方账号登录入口`oauth2_go` 88 | goinfo = auth.oauth2_go(name=name, signin=g.signin, tokeninfo=resp, userinfo=dict(openid=user["id"], nick_name=user["name"], gender=2, avatar=user["avatar_url"], domain_name=user["login"], signature=user["bio"], location=user.get("location")), uid=g.uid) 89 | goinfo = dfr(goinfo) 90 | if goinfo["pageAction"] == "goto_signIn": 91 | """ 未登录流程->已经绑定过账号,需要设置登录态 """ 92 | uid = goinfo["goto_signIn_data"]["guid"] 93 | # 记录登录日志 94 | auth.brush_loginlog(dict(identity_type=oauth2_name2type(name), uid=uid, success=True), login_ip=g.ip, user_agent=request.headers.get("User-Agent")) 95 | # 设置登录态 96 | return github.goto_signIn(uid=uid, sso=sso) 97 | elif goinfo["pageAction"] == "goto_signUp": 98 | """ 未登录流程->执行注册绑定功能 """ 99 | return github.goto_signUp(openid=goinfo["goto_signUp_data"]["openid"], sso=sso) 100 | else: 101 | # 已登录流程->正在绑定第三方账号:反馈绑定结果 102 | if goinfo["success"]: 103 | # 绑定成功,返回原页面 104 | flash(u"已绑定") 105 | else: 106 | # 绑定失败,返回原页面 107 | flash(goinfo["msg"]) 108 | # 跳回绑定设置页面 109 | return redirect(url_for("front.userset", _anchor="bind")) 110 | else: 111 | flash(u'Access denied: reason=%s error=%s' % ( 112 | request.args.get('error'), 113 | request.args.get('error_description') 114 | )) 115 | return redirect(g.redirect_uri) 116 | 117 | #: 返回插件主类 118 | def getPluginClass(): 119 | return OAuth2_Github_Main 120 | 121 | #: 插件主类, 不强制要求名称与插件名一致, 保证getPluginClass准确返回此类 122 | class OAuth2_Github_Main(PluginBase): 123 | """ 继承自PluginBase基类 """ 124 | 125 | def register_tep(self): 126 | """注册模板入口, 返回扩展点名称及扩展的代码, 其中include点必须是实际的HTML文件, string点必须是HTML代码.""" 127 | tep = {"auth_signIn_socialLogin_include": "connect_github.html"} 128 | return tep 129 | 130 | def register_bep(self): 131 | """注册蓝图入口, 返回蓝图路由前缀及蓝图名称""" 132 | bep = {"prefix": "/oauth2/github", "blueprint": plugin_blueprint} 133 | return bep 134 | 135 | def register(): 136 | om = OAuth2_Github_Main() 137 | return dict( 138 | tep=om.register_tep(), 139 | bep=om.register_bep(), 140 | ) 141 | 142 | -------------------------------------------------------------------------------- /src/plugins/oauth2_github/templates/connect_github.html: -------------------------------------------------------------------------------- 1 | {%- if request.args.sso %} 2 | {% set url = url_for('oauth2_github.login', sso=request.args.sso) %} 3 | {%- else %} 4 | {% set url = url_for('oauth2_github.login') %} 5 | {%- endif -%} 6 | 7 |   -------------------------------------------------------------------------------- /src/plugins/oauth2_qq/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | passport.plugins.oauth2_qq 4 | ~~~~~~~~~~~~~~ 5 | 6 | 使用qq登录 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | #: Importing these two modules is the first and must be done. 13 | #: 首先导入这两个必须模块 14 | from __future__ import absolute_import 15 | from libs.base import PluginBase 16 | #: Import the other modules here, and if it's your own module, use the relative Import. eg: from .lib import Lib 17 | #: 在这里导入其他模块, 如果有自定义包目录, 使用相对导入, 如: from .lib import Lib 18 | import json 19 | from flask import Blueprint, request, jsonify, g, flash, redirect, url_for 20 | from utils.web import OAuth2, dfr, oauth2_name2type, checkGet_ssoRequest, oauth2_genderconverter 21 | from config import PLUGINS 22 | from libs.auth import Authentication 23 | 24 | #:Your plug-in name must be consistent with the plug-in directory name. 25 | #:你的插件名称,必须和插件目录名称等保持一致. 26 | __plugin_name__ = "oauth2_qq" 27 | #: Plugin describes information. What does it do? 28 | #: 插件描述信息,什么用处. 29 | __description__ = "Connection QQ with OAuth2" 30 | #: Plugin Author 31 | #: 插件作者 32 | __author__ = "Mr.tao " 33 | #: Plugin Version 34 | #: 插件版本 35 | __version__ = "0.1.0" 36 | #: Plugin Url 37 | #: 插件主页 38 | __url__ = "https://www.saintic.com" 39 | #: Plugin License 40 | #: 插件许可证 41 | __license__ = "MIT" 42 | #: Plugin License File 43 | #: 插件许可证文件 44 | __license_file__= "LICENSE" 45 | #: Plugin Readme File 46 | #: 插件自述文件 47 | __readme_file__ = "README" 48 | #: Plugin state, enabled or disabled, default: enabled 49 | #: 插件状态, enabled、disabled, 默认enabled 50 | name = "qq" 51 | if PLUGINS[name]["ENABLE"] in ("true", "True", True): 52 | __state__ = "enabled" 53 | else: 54 | __state__ = "disabled" 55 | 56 | qq = OAuth2(name, 57 | client_id = PLUGINS[name]["APP_ID"], 58 | client_secret = PLUGINS[name]["APP_KEY"], 59 | redirect_url = PLUGINS[name]["REDIRECT_URI"], 60 | authorize_url = "https://graph.qq.com/oauth2.0/authorize", 61 | access_token_url = "https://graph.qq.com/oauth2.0/token", 62 | access_token_method = "get", 63 | get_openid_url = "https://graph.qq.com/oauth2.0/me", 64 | get_openid_method = "get", 65 | get_userinfo_url = "https://graph.qq.com/user/get_user_info", 66 | get_userinfo_method = "get" 67 | ) 68 | 69 | plugin_blueprint = Blueprint("oauth2_qq", "oauth2_qq") 70 | @plugin_blueprint.route("/login") 71 | def login(): 72 | """ 跳转此OAuth应用登录以授权 73 | 此路由地址:/oauth2/qq/login 74 | """ 75 | return qq.authorize() 76 | 77 | @plugin_blueprint.route("/authorized") 78 | def authorized(): 79 | """ 授权回调路由 80 | 此路由地址:/oauth2/qq/authorized 81 | """ 82 | # 加密的sso参数值 83 | sso = request.args.get("sso") or None 84 | # 换取access_token 85 | resp = qq.authorized_response() 86 | if "callback" in resp: 87 | resp = json.loads(resp[10:-3]) 88 | else: 89 | resp = qq.url_code(resp) 90 | if resp and isinstance(resp, dict) and "access_token" in resp: 91 | # 获取用户唯一标识 92 | openid = json.loads(qq.get_openid(resp["access_token"])[10:-3]).get("openid") 93 | # 根据access_token获取用户基本信息 94 | user = qq.get_userinfo(resp["access_token"], openid=openid, oauth_consumer_key=PLUGINS[name]["APP_ID"]) 95 | if int(user.get("ret", 0)) < 0: 96 | flash(user.get("msg")) 97 | return redirect(g.redirect_uri) 98 | # 处理第三方登录逻辑 99 | auth = Authentication(g.mysql, g.redis) 100 | # 第三方账号登录入口`oauth2_go` 101 | goinfo = auth.oauth2_go(name=name, signin=g.signin, tokeninfo=resp, userinfo=dict(openid=openid, nick_name=user["nickname"], gender=oauth2_genderconverter(user["gender"]), avatar=user["figureurl_qq_2"] or user["figureurl_qq_1"], location="%s %s" %(user.get("province"), user.get("city"))), uid=g.uid) 102 | goinfo = dfr(goinfo) 103 | if goinfo["pageAction"] == "goto_signIn": 104 | """ 未登录流程->已经绑定过账号,需要设置登录态 """ 105 | uid = goinfo["goto_signIn_data"]["guid"] 106 | # 记录登录日志 107 | auth.brush_loginlog(dict(identity_type=oauth2_name2type(name), uid=uid, success=True), login_ip=g.ip, user_agent=request.headers.get("User-Agent")) 108 | # 设置登录态 109 | return qq.goto_signIn(uid=uid, sso=sso) 110 | elif goinfo["pageAction"] == "goto_signUp": 111 | """ 未登录流程->openid没有对应账号,执行注册或绑定功能 """ 112 | return qq.goto_signUp(openid=goinfo["goto_signUp_data"]["openid"], sso=sso) 113 | else: 114 | # 已登录流程->正在绑定第三方账号:反馈绑定结果 115 | if goinfo["success"]: 116 | # 绑定成功,返回原页面 117 | flash(u"已绑定") 118 | else: 119 | # 绑定失败,返回原页面 120 | flash(goinfo["msg"]) 121 | # 跳回绑定设置页面 122 | return redirect(url_for("front.userset", _anchor="bind")) 123 | else: 124 | flash(u'Access denied: reason=%s error=%s' % ( 125 | resp.get('error'), 126 | resp.get('error_description') 127 | )) 128 | return redirect(g.redirect_uri) 129 | 130 | #: 返回插件主类 131 | def getPluginClass(): 132 | return OAuth2_QQ_Main 133 | 134 | #: 插件主类, 不强制要求名称与插件名一致, 保证getPluginClass准确返回此类 135 | class OAuth2_QQ_Main(PluginBase): 136 | """ 继承自PluginBase基类 """ 137 | 138 | def register_tep(self): 139 | """注册模板入口, 返回扩展点名称及扩展的代码, 其中include点必须是实际的HTML文件, string点必须是HTML代码.""" 140 | tep = {"auth_signIn_socialLogin_include": "connect_qq.html"} 141 | return tep 142 | 143 | def register_bep(self): 144 | """注册蓝图入口, 返回蓝图路由前缀及蓝图名称""" 145 | bep = {"prefix": "/oauth2/qq", "blueprint": plugin_blueprint} 146 | return bep 147 | 148 | 149 | def register(): 150 | om = OAuth2_QQ_Main() 151 | return dict( 152 | tep=om.register_tep(), 153 | bep=om.register_bep(), 154 | ) 155 | 156 | -------------------------------------------------------------------------------- /src/plugins/oauth2_qq/templates/connect_qq.html: -------------------------------------------------------------------------------- 1 | {%- if request.args.sso %} 2 | {% set url = url_for('oauth2_qq.login', sso=request.args.sso) %} 3 | {%- else %} 4 | {% set url = url_for('oauth2_qq.login') %} 5 | {%- endif -%} 6 | 7 |   -------------------------------------------------------------------------------- /src/plugins/oauth2_weibo/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | passport.plugins.oauth2_weibo 4 | ~~~~~~~~~~~~~~ 5 | 6 | 使用weibo登录 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | #: Importing these two modules is the first and must be done. 13 | #: 首先导入这两个必须模块 14 | from __future__ import absolute_import 15 | from libs.base import PluginBase 16 | #: Import the other modules here, and if it's your own module, use the relative Import. eg: from .lib import Lib 17 | #: 在这里导入其他模块, 如果有自定义包目录, 使用相对导入, 如: from .lib import Lib 18 | from flask import Blueprint, request, jsonify, g, flash, redirect, url_for 19 | from utils.web import OAuth2, dfr, oauth2_name2type, checkGet_ssoRequest, oauth2_genderconverter 20 | from config import PLUGINS 21 | from libs.auth import Authentication 22 | 23 | #:Your plug-in name must be consistent with the plug-in directory name. 24 | #:你的插件名称,必须和插件目录名称等保持一致. 25 | __plugin_name__ = "oauth2_weibo" 26 | #: Plugin describes information. What does it do? 27 | #: 插件描述信息,什么用处. 28 | __description__ = "Connection Weibo with OAuth2" 29 | #: Plugin Author 30 | #: 插件作者 31 | __author__ = "Mr.tao " 32 | #: Plugin Version 33 | #: 插件版本 34 | __version__ = "0.1.0" 35 | #: Plugin Url 36 | #: 插件主页 37 | __url__ = "https://www.saintic.com" 38 | #: Plugin License 39 | #: 插件许可证 40 | __license__ = "MIT" 41 | #: Plugin License File 42 | #: 插件许可证文件 43 | __license_file__= "LICENSE" 44 | #: Plugin Readme File 45 | #: 插件自述文件 46 | __readme_file__ = "README" 47 | #: Plugin state, enabled or disabled, default: enabled 48 | #: 插件状态, enabled、disabled, 默认enabled 49 | name = "weibo" 50 | if PLUGINS[name]["ENABLE"] in ("true", "True", True): 51 | __state__ = "enabled" 52 | else: 53 | __state__ = "disabled" 54 | 55 | weibo = OAuth2(name, 56 | client_id = PLUGINS[name]["APP_ID"], 57 | client_secret = PLUGINS[name]["APP_KEY"], 58 | redirect_url = PLUGINS[name]["REDIRECT_URI"], 59 | authorize_url = "https://api.weibo.com/oauth2/authorize", 60 | access_token_url = "https://api.weibo.com/oauth2/access_token", 61 | get_openid_url = "https://api.weibo.com/2/account/get_uid.json", 62 | get_userinfo_url = "https://api.weibo.com/2/users/show.json" 63 | ) 64 | 65 | plugin_blueprint = Blueprint("oauth2_weibo", "oauth2_weibo") 66 | @plugin_blueprint.route("/login") 67 | def login(): 68 | """ 跳转此OAuth应用登录以授权 69 | 此路由地址:/oauth2/weibo/login 70 | """ 71 | return weibo.authorize() 72 | 73 | @plugin_blueprint.route("/authorized") 74 | def authorized(): 75 | """ 授权回调路由 76 | 此路由地址:/oauth2/weibo/authorized 77 | """ 78 | # 加密的sso参数值 79 | sso = request.args.get("sso") or None 80 | # 换取access_token 81 | resp = weibo.authorized_response() 82 | if resp and isinstance(resp, dict) and "access_token" in resp: 83 | # 根据access_token获取用户唯一标识 84 | openid = weibo.get_openid(resp["access_token"]).get("uid") 85 | # 根据access_token获取用户基本信息 86 | user = weibo.get_userinfo(resp["access_token"], uid=openid) 87 | if user.get("error_code"): 88 | flash("error_code: %s, error_description: %s" %(user.get("error_code"), user.get("error"))) 89 | return redirect(g.redirect_uri) 90 | # 处理第三方登录逻辑 91 | auth = Authentication(g.mysql, g.redis) 92 | # 第三方账号登录入口`oauth2_go` 93 | goinfo = auth.oauth2_go(name=name, signin=g.signin, tokeninfo=resp, userinfo=dict(openid=openid, nick_name=user["screen_name"], gender=oauth2_genderconverter(user["gender"]), avatar=user["profile_image_url"], domain_name=user["domain"], signature=user["description"], location=user.get("location")), uid=g.uid) 94 | goinfo = dfr(goinfo) 95 | if goinfo["pageAction"] == "goto_signIn": 96 | """ 未登录流程->已经绑定过账号,需要设置登录态 """ 97 | uid = goinfo["goto_signIn_data"]["guid"] 98 | # 记录登录日志 99 | auth.brush_loginlog(dict(identity_type=oauth2_name2type(name), uid=uid, success=True), login_ip=g.ip, user_agent=request.headers.get("User-Agent")) 100 | # 设置登录态 101 | return weibo.goto_signIn(uid=uid, sso=sso) 102 | elif goinfo["pageAction"] == "goto_signUp": 103 | """ 未登录流程->执行注册绑定功能 """ 104 | return weibo.goto_signUp(openid=goinfo["goto_signUp_data"]["openid"], sso=sso) 105 | else: 106 | # 已登录流程->正在绑定第三方账号:反馈绑定结果 107 | if goinfo["success"]: 108 | # 绑定成功,返回原页面 109 | flash(u"已绑定") 110 | else: 111 | # 绑定失败,返回原页面 112 | flash(goinfo["msg"]) 113 | # 跳回绑定设置页面 114 | return redirect(url_for("front.userset", _anchor="bind")) 115 | else: 116 | flash(u'Access denied: reason=%s error=%s' % ( 117 | request.args.get('error'), 118 | request.args.get('error_description') 119 | )) 120 | return redirect(g.redirect_uri) 121 | 122 | #: 返回插件主类 123 | def getPluginClass(): 124 | return OAuth2_Weibo_Main 125 | 126 | #: 插件主类, 不强制要求名称与插件名一致, 保证getPluginClass准确返回此类 127 | class OAuth2_Weibo_Main(PluginBase): 128 | """ 继承自PluginBase基类 """ 129 | 130 | def register_tep(self): 131 | """注册模板入口, 返回扩展点名称及扩展的代码, 其中include点必须是实际的HTML文件, string点必须是HTML代码.""" 132 | tep = {"auth_signIn_socialLogin_include": "connect_weibo.html"} 133 | return tep 134 | 135 | def register_bep(self): 136 | """注册蓝图入口, 返回蓝图路由前缀及蓝图名称""" 137 | bep = {"prefix": "/oauth2/weibo", "blueprint": plugin_blueprint} 138 | return bep 139 | 140 | def register(): 141 | om = OAuth2_Weibo_Main() 142 | return dict( 143 | tep=om.register_tep(), 144 | bep=om.register_bep(), 145 | ) 146 | -------------------------------------------------------------------------------- /src/plugins/oauth2_weibo/templates/connect_weibo.html: -------------------------------------------------------------------------------- 1 | {%- if request.args.sso %} 2 | {% set url = url_for('oauth2_weibo.login', sso=request.args.sso) %} 3 | {%- else %} 4 | {% set url = url_for('oauth2_weibo.login') %} 5 | {%- endif -%} 6 | 7 |   -------------------------------------------------------------------------------- /src/plugins/ssoserver/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | passport.plugins.ssoserver 4 | ~~~~~~~~~~~~~~ 5 | 6 | SSO Server with http://www.cnblogs.com/ywlaker/p/6113927.html 7 | 8 | :copyright: (c) 2017 by staugur. 9 | :license: MIT, see LICENSE for more details. 10 | """ 11 | 12 | #: Importing these two modules is the first and must be done. 13 | #: 首先导入这两个必须模块 14 | from __future__ import absolute_import 15 | from libs.base import PluginBase 16 | #: Import the other modules here, and if it's your own module, use the relative Import. eg: from .lib import Lib 17 | #: 在这里导入其他模块, 如果有自定义包目录, 使用相对导入, 如: from .lib import Lib 18 | from config import SYSTEM 19 | from utils.tool import logger 20 | from utils.web import verify_sessionId 21 | from flask import Blueprint, request, jsonify, g, redirect, url_for 22 | 23 | #:Your plug-in name must be consistent with the plug-in directory name. 24 | #:你的插件名称,必须和插件目录名称等保持一致. 25 | __plugin_name__ = "ssoserver" 26 | #: Plugin describes information. What does it do? 27 | #: 插件描述信息,什么用处. 28 | __description__ = "SSO Server" 29 | #: Plugin Author 30 | #: 插件作者 31 | __author__ = "Mr.tao " 32 | #: Plugin Version 33 | #: 插件版本 34 | __version__ = "0.1.0" 35 | #: Plugin Url 36 | #: 插件主页 37 | __url__ = "https://www.saintic.com" 38 | #: Plugin License 39 | #: 插件许可证 40 | __license__ = "MIT" 41 | #: Plugin License File 42 | #: 插件许可证文件 43 | __license_file__= "LICENSE" 44 | #: Plugin Readme File 45 | #: 插件自述文件 46 | __readme_file__ = "README" 47 | #: Plugin state, enabled or disabled, default: enabled 48 | #: 插件状态, enabled、disabled, 默认enabled 49 | __state__ = "enabled" 50 | 51 | sso_blueprint = Blueprint("sso", "sso") 52 | @sso_blueprint.route("/") 53 | def index(): 54 | """sso入口,仅判断是否为sso请求,最终重定向到登录页""" 55 | sso = request.args.get("sso") 56 | if verify_sessionId(sso): 57 | return redirect(url_for("front.signIn", sso=sso)) 58 | return redirect(url_for("front.signIn")) 59 | 60 | @sso_blueprint.route("/validate", methods=["POST"]) 61 | def validate(): 62 | res = dict(msg=None, success=False) 63 | Action = request.args.get("Action") 64 | if request.method == "POST": 65 | if Action == "validate_ticket": 66 | ticket = request.form.get("ticket") 67 | app_name = request.form.get("app_name") 68 | get_userinfo = True if request.form.get("get_userinfo") in (1, True, "1", "True", "true", "on") else False 69 | get_userbind = True if request.form.get("get_userbind") in (1, True, "1", "True", "true", "on") else False 70 | if ticket and app_name: 71 | resp = g.api.usersso.ssoGetWithTicket(ticket) 72 | logger.debug("sso validate ticket resp: {}".format(resp)) 73 | if resp and isinstance(resp, dict): 74 | # 此时表明ticket验证通过,应当返回如下信息: 75 | # dict(uid=所需, sid=所需,source=xx) 76 | if g.api.userapp.getUserApp(app_name): 77 | # app_name有效,验证全部通过 78 | res.update(success=True, uid=resp["uid"], sid=resp["sid"], expire=SYSTEM["SESSION_EXPIRE"]) 79 | # 有效,此sid已登录客户端中注册app_name且向uid中注册已登录的sid 80 | res.update(register=dict( 81 | Client = g.api.usersso.ssoRegisterClient(sid=resp["sid"], app_name=app_name), 82 | UserSid = g.api.usersso.ssoRegisterUserSid(uid=resp["uid"], sid=resp["sid"]) 83 | )) 84 | if get_userinfo is True: 85 | userinfo = g.api.userprofile.getUserProfile(uid=resp["uid"], getBind=get_userbind) 86 | res.update(userinfo=userinfo) 87 | else: 88 | res.update(msg="No such app_name") 89 | else: 90 | res.update(msg="Invaild ticket or expired") 91 | else: 92 | res.update(msg="Empty ticket or app_name") 93 | elif Action == "validate_sync": 94 | token = request.form.get("token") 95 | uid = request.form.get("uid") 96 | if uid and token and len(uid) == 22 and len(token) == 32: 97 | syncToken = g.api.usersso.ssoGetUidCronSyncToken(uid) 98 | if syncToken and syncToken == token: 99 | res.update(success=True) 100 | else: 101 | res.update(msg="Invaild token") 102 | else: 103 | res.update(msg="Invaild uid or token") 104 | else: 105 | res.update(msg="Invaild Action") 106 | return jsonify(res) 107 | 108 | #: 返回插件主类 109 | def getPluginClass(): 110 | return SSOServerMain 111 | 112 | #: 插件主类, 不强制要求名称与插件名一致, 保证getPluginClass准确返回此类 113 | class SSOServerMain(PluginBase): 114 | 115 | def register_bep(self): 116 | """注册蓝图入口, 返回蓝图路由前缀及蓝图名称""" 117 | bep = {"prefix": "/sso", "blueprint": sso_blueprint} 118 | return bep 119 | 120 | def register(): 121 | return dict( 122 | bep=SSOServerMain().register_bep() 123 | ) 124 | -------------------------------------------------------------------------------- /src/static/css/ImgCropping.css: -------------------------------------------------------------------------------- 1 | .l-btn{ 2 | display: inline-block; 3 | outline: none; 4 | resize: none; 5 | border: none; 6 | padding:5px 10px; 7 | background: #009688; 8 | color: #fff; 9 | border:solid 1px #009688; 10 | border-radius: 3px; 11 | font-size: 14px; 12 | } 13 | .l-btn:hover{ 14 | background: #088A08; 15 | animation: anniu 1s infinite; 16 | } 17 | .l-btn:active{ 18 | box-shadow: 0 2px 3px rgba(0,0,0,.2) inset; 19 | } 20 | .l-btn-disable, 21 | .l-btn-disable:hover, 22 | .l-btn-disable:focus, 23 | .l-btn-disable.active { 24 | cursor:not-allowed; 25 | color: #fff; background-color: #a6aaad; 26 | } 27 | .hidden{ 28 | display: none; 29 | } 30 | .tailoring-container, .tailoring-container div, .tailoring-container p{ 31 | margin: 0;padding: 0; 32 | box-sizing: border-box; 33 | -webkit-box-sizing: border-box; 34 | -moz-box-sizing: border-box; 35 | } 36 | .tailoring-container{ 37 | position: fixed; 38 | width: 100%; 39 | height: 100%; 40 | z-index: 1000; 41 | top: 0; 42 | left: 0; 43 | } 44 | .tailoring-container .black-cloth{ 45 | position: fixed; 46 | width: 100%; 47 | height: 100%; 48 | background: #111; 49 | opacity: 0; 50 | z-index: 1001; 51 | } 52 | .tailoring-container .tailoring-content{ 53 | position: absolute; 54 | width: 768px; 55 | height: 560px; 56 | background: #fff; 57 | z-index: 1002; 58 | left: 0; 59 | top: 0; 60 | 61 | /*left: 50%; 62 | top: 50%; 63 | transform: translate(-50%,-50%); 64 | -weblit-transform: translate(-50%,-50%); 65 | -moz-transform: translate(-50%,-50%); 66 | -ms-transform: translate(-50%,-50%); 67 | -o-transform: translate(-50%,-50%);*/ 68 | 69 | border-radius: 10px; 70 | box-shadow: 0 0 10px #000; 71 | padding: 10px; 72 | } 73 | 74 | .tailoring-content-one{ 75 | height: 40px; 76 | width: 100%; 77 | border-bottom: 1px solid #DDD ; 78 | } 79 | .tailoring-content .choose-btn{ 80 | float: left; 81 | } 82 | .tailoring-content .close-tailoring{ 83 | display: inline-block; 84 | height: 30px; 85 | width: 30px; 86 | border-radius: 100%; 87 | background: #eee; 88 | color: #fff; 89 | font-size: 22px; 90 | text-align: center; 91 | line-height: 30px; 92 | float: right; 93 | cursor: pointer; 94 | } 95 | .tailoring-content .close-tailoring:hover{ 96 | background: #ccc; 97 | } 98 | 99 | .tailoring-content .tailoring-content-two{ 100 | width: 100%; 101 | height: 460px; 102 | position: relative; 103 | padding: 5px 0; 104 | } 105 | .tailoring-content .tailoring-box-parcel{ 106 | width: 520px; 107 | height: 450px; 108 | position: absolute; 109 | left: 0; 110 | border: solid 1px #ddd; 111 | } 112 | .tailoring-content .preview-box-parcel{ 113 | display: inline-block; 114 | width: 228px; 115 | height: 450px; 116 | position: absolute; 117 | right: 0; 118 | padding: 4px 14px; 119 | } 120 | .preview-box-parcel p{ 121 | color: #555; 122 | } 123 | .previewImg{ 124 | width: 200px; 125 | height: 200px; 126 | overflow: hidden; 127 | } 128 | .preview-box-parcel .square{ 129 | margin-top: 10px; 130 | border: solid 1px #ddd; 131 | } 132 | .preview-box-parcel .circular{ 133 | border-radius: 100%; 134 | margin-top: 10px; 135 | border: solid 1px #ddd; 136 | } 137 | 138 | .tailoring-content .tailoring-content-three{ 139 | width: 100%; 140 | height: 40px; 141 | border-top: 1px solid #DDD ; 142 | padding-top: 10px; 143 | } 144 | .sureCut{ 145 | float: right; 146 | } 147 | 148 | @media all and (max-width: 768px) { 149 | .tailoring-container .tailoring-content{ 150 | width: 100%; 151 | min-width: 320px; 152 | height: 460px; 153 | } 154 | .tailoring-content .tailoring-content-two{ 155 | height: 360px; 156 | } 157 | .tailoring-content .tailoring-box-parcel{ 158 | height: 350px; 159 | } 160 | .tailoring-container .tailoring-box-parcel{ 161 | width: 100%; 162 | } 163 | .tailoring-container .preview-box-parcel{ 164 | display: none; 165 | } 166 | 167 | } -------------------------------------------------------------------------------- /src/static/css/Vidage.min.css: -------------------------------------------------------------------------------- 1 | .Vidage__backdrop,.Vidage__image{position:absolute;top:0;right:0;bottom:0;left:0}.Vidage--allow .Vidage__video{display:block}.Vidage--allow .Vidage__image{display:none}.Vidage{position:fixed;top:0;right:0;bottom:0;left:0;z-index:-1}.Vidage,.Vidage__video{min-width:100%;min-height:100%}.Vidage__video{position:absolute;top:50%;left:50%;width:auto;height:auto;-webkit-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%);display:none}.Vidage__image{background-image:url(../images/bg.jpg);background-position:50%;background-repeat:no-repeat;background-size:cover}.Vidage__backdrop{background-color:#1b1c1d;opacity:.2} -------------------------------------------------------------------------------- /src/static/css/auth.css: -------------------------------------------------------------------------------- 1 | body { 2 | overflow:hidden; 3 | } 4 | .login { 5 | height:auto; 6 | width:300px; 7 | padding:20px; 8 | background-color:rgba(16,16,16,0.24); 9 | border-radius:10px; 10 | position:absolute; 11 | left:50%; 12 | top:40%; 13 | margin:-160px 0 0 -150px; 14 | z-index:99; 15 | } 16 | .login h1 { 17 | text-align:center; 18 | color:#fff; 19 | font-size:22px; 20 | margin-bottom:20px; 21 | } 22 | .fetures,.thirds,.fly-footer { 23 | margin-top:10px; 24 | color:#fff; 25 | font-size:14px; 26 | } 27 | .fetures { 28 | overflow:auto; 29 | zoom:1; 30 | } 31 | .fetures a { 32 | color:#fff; 33 | } 34 | .fetures a:hover { 35 | color:#dedede; 36 | } 37 | .fetures .signup { 38 | float:left; 39 | } 40 | .fetures .forgot { 41 | float:right; 42 | } 43 | .thirds { 44 | clear:both; 45 | font-size:smaller; 46 | } 47 | .fly-footer { 48 | margin-top:0; 49 | border-top:none; 50 | padding:10px 0 5px; 51 | text-align:center; 52 | } 53 | .fly-footer a { 54 | color:#fff; 55 | } 56 | .fly-footer a:hover { 57 | color:#dedede; 58 | } 59 | .thirds a i { 60 | color:#dedede; 61 | } 62 | 63 | .sms-button { display: inline-block; width: 80px; font-weight: bold; font-size: 14px; text-align: right; color: #009688; } 64 | 65 | .big_tips i { 66 | font-size: 22px; 67 | } 68 | .sub_tips { 69 | font-size: 12px; 70 | color: #808080; 71 | } -------------------------------------------------------------------------------- /src/static/css/cropper.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Cropper v3.1.3 3 | * https://github.com/fengyuanchen/cropper 4 | * 5 | * Copyright (c) 2014-2017 Chen Fengyuan 6 | * Released under the MIT license 7 | * 8 | * Date: 2017-10-21T10:03:37.133Z 9 | */.cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal,.cropper-wrap-box{bottom:0;left:0;position:absolute;right:0;top:0}.cropper-canvas,.cropper-wrap-box{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline-color:rgba(51,153,255,.75);outline:1px solid #39f;overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.33333%;left:0;top:33.33333%;width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.33333%;top:0;width:33.33333%}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:after,.cropper-center:before{background-color:#eee;content:" ";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:e-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:n-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:w-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:s-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:e-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:n-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:w-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:ne-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nw-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:sw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:se-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width:768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width:992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width:1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:" ";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC")}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed} 10 | /*# sourceMappingURL=cropper.min.css.map */ -------------------------------------------------------------------------------- /src/static/css/docs.css: -------------------------------------------------------------------------------- 1 | html{background: none;} 2 | .fly-header{position: relative; top: 0;} 3 | html body{margin-top: 0;} 4 | 5 | 6 | .fly-body-docs .fly-header .fly-nav, 7 | .fly-body-docs .fly-footer{display: none;} 8 | 9 | 10 | /* 新增和编辑 */ 11 | .fly-docs-write, 12 | .fly-docs-preview{position: fixed; left: 0; top: 60px; bottom: 0; width: 50%; box-sizing: border-box;} 13 | .fly-docs-editor{position: absolute; width: 100%; height: 100%; padding: 20px; border: none; overflow: auto; box-sizing: border-box; resize: none;} 14 | .fly-docs-form{position: fixed; top: 12px; left: 10px; width: 50%; padding: 0; z-index: 10001; box-sizing: border-box;} 15 | .fly-docs-form .layui-input{} 16 | .fly-docus-btn{position: absolute; right: 15px; bottom: 15px; z-index: 10000;} 17 | 18 | 19 | .fly-docs-preview{left: auto; right: 0; border-left: 1px solid #e2e2e2; padding: 20px; overflow: auto;} 20 | 21 | 22 | 23 | 24 | /* markdown 文档 */ 25 | .fly-md-text{line-height: 24px;} 26 | .fly-md-text h2{margin: 35px 0 20px;} 27 | .fly-md-text h3{margin: 20px 0;} 28 | .fly-md-text h4, 29 | .fly-md-text h5, 30 | .fly-md-text h6{font-weight: 700;} 31 | 32 | .fly-md-text blockquote{margin: 10px 0; padding: 15px; line-height: 22px; border-left: 5px solid #009688; border-radius: 0 2px 2px 0; background-color: #f2f2f2;} 33 | .fly-md-text pre{font-size: 14px; color: #666;} 34 | .fly-md-text ul{padding: 0;} 35 | .fly-md-text ul, 36 | .fly-md-text ol{margin-bottom: 10px; padding-left: 15px;} 37 | .fly-md-text ol li{list-style-type: decimal;} 38 | .fly-md-text ul ul li, 39 | .fly-md-text ol ul li{list-style-type: circle;} 40 | .fly-md-text strong,.fly-md-text em{padding: 0 3px;} 41 | .fly-md-text code, 42 | .fly-md-text pre{font-family: Monaco,Menlo,Consolas,"Courier New",monospace;} 43 | .fly-md-text code{padding: 3px 5px; background-color: #f2f2f2; border-radius: 2px;} 44 | .fly-md-text pre code{padding: 0; background: none; border-radius: 0;} 45 | 46 | 47 | 48 | .fly-md-text .fly-md-notice{text-align: center; padding: 50px 20px;} 49 | 50 | 51 | /* 文档详情 */ 52 | .fly-docs-container{padding: 20px;} 53 | .fly-docs-info{padding: 10px 0;} 54 | .fly-docs-info>span{display: inline-block; margin-right: 10px; line-height: 26px; padding: 0 10px; border: 1px solid #e6e6e6; border-radius: 2px; color: #666;} 55 | .fly-docs-admin{padding: 10px 0;} 56 | .fly-docs-title{margin-bottom: 10px; padding-bottom: 10px; color: #000; border-bottom: 1px solid #e6e6e6;} 57 | .fly-docs-container .fly-md-text{padding: 20px 0;} 58 | 59 | .fly-md-dir{position: fixed; top: 0; bottom: 0; left: 0; width: 220px; padding: 20px; border-right: 1px solid #f2f2f2; overflow: auto; background-color: #f2f2f2; box-sizing: border-box; transition: all .3s; -webkit-transition: all .3s;} 60 | .fly-md-dir:before{content: '目录'; position: relative; width: 100%; display: block; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #e6e6e6; font-size: 20px; box-sizing: border-box;} 61 | 62 | #FLY-spread-dir{position: fixed; left: 15px; bottom: 15px; width: 50px; height: 50px; line-height: 50px; text-align: center; background-color: #2F9688; border-radius: 5px; color: #fff;} 63 | #FLY-spread-dir .layui-icon{font-size: 20px;} 64 | 65 | /* 移动端展开模式 */ 66 | .fly-docs-spread .fly-md-dir{transform: translate3d(0, 0, 0) !important;} 67 | 68 | 69 | /* 产品文档 */ 70 | @media screen and (max-width: 1620px){ 71 | .fly-body-docs .layui-container{padding-left: 220px; width: 100%;} 72 | } 73 | 74 | @media screen and (max-width: 768px){ 75 | .fly-body-docs .layui-container{padding-left: 0;} 76 | .fly-md-dir{transform: translate3d(-220px, 0, 0); z-index: 999999999999;} 77 | #FLY-spread-dir{display: block !important;} 78 | } -------------------------------------------------------------------------------- /src/static/images/avatar/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/avatar/default.png -------------------------------------------------------------------------------- /src/static/images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/bg.jpg -------------------------------------------------------------------------------- /src/static/images/coding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/coding.png -------------------------------------------------------------------------------- /src/static/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/favicon.png -------------------------------------------------------------------------------- /src/static/images/gitee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/gitee.png -------------------------------------------------------------------------------- /src/static/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/github.png -------------------------------------------------------------------------------- /src/static/images/login.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/login.mp4 -------------------------------------------------------------------------------- /src/static/images/logo-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/logo-1.png -------------------------------------------------------------------------------- /src/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/logo.png -------------------------------------------------------------------------------- /src/static/images/qq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/qq.png -------------------------------------------------------------------------------- /src/static/images/vaptcha-loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/vaptcha-loading.gif -------------------------------------------------------------------------------- /src/static/images/vip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/vip.png -------------------------------------------------------------------------------- /src/static/images/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/wechat.png -------------------------------------------------------------------------------- /src/static/images/weibo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/weibo.png -------------------------------------------------------------------------------- /src/static/images/wrz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/images/wrz.png -------------------------------------------------------------------------------- /src/static/js/Vidage.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.Vidage=e()}}(function(){return function e(t,n,r){function o(a,u){if(!n[a]){if(!t[a]){var s="function"==typeof require&&require;if(!u&&s)return s(a,!0);if(i)return i(a,!0);var l=new Error("Cannot find module '"+a+"'");throw l.code="MODULE_NOT_FOUND",l}var c=n[a]={exports:{}};t[a][0].call(c.exports,function(e){var n=t[a][1][e];return o(n?n:e)},c,c.exports,e,t,n,r)}return n[a].exports}for(var i="function"==typeof require&&require,a=0;a0?i=setTimeout(o,t-c):(i=null,n||(l=e.apply(u,a),i||(u=a=null)))}var i,a,u,s,l;return null==t&&(t=100),function(){u=this,a=arguments,s=r();var c=n&&!i;return i||(i=setTimeout(o,t)),c&&(l=e.apply(u,a),u=a=null),l}}},{"date-now":1}],3:[function(e,t,n){"use strict";function r(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}function o(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;var r=Object.getOwnPropertyNames(t).map(function(e){return t[e]});if("0123456789"!==r.join(""))return!1;var o={};return"abcdefghijklmnopqrst".split("").forEach(function(e){o[e]=e}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},o)).join("")}catch(e){return!1}}var i=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,u=Object.prototype.propertyIsEnumerable;t.exports=o()?Object.assign:function(e,t){for(var n,o,s=r(e),l=1;l1&&void 0!==arguments[1]?arguments[1]:{};o(this,e);var r={helperClass:"Vidage--allow",videoRemoval:!1};this.options=(0,l.default)(r,n),this._name=this.constructor.name,this.element=(0,d.default)(t,this._name),this.init()}return i(e,[{key:"init",value:function(){var e=this;this.element.addEventListener("canplay",function(){return e.handler()}),window.addEventListener("resize",(0,u.default)(function(){return e.handler()},250))}},{key:"handler",value:function(){var e=document.body;(0,p.default)()?(this.element.pause(),this.options.videoRemoval&&(0,v.removeVideo)(this.element),e.classList.remove(this.options.helperClass)):(this.options.videoRemoval&&(0,v.restoreVideo)(this.element),this.element.play(),e.classList.add(this.options.helperClass))}}]),e}();n.default=h,t.exports=n.default},{"./helpers/feature-detect":5,"./helpers/handle-video-selector":6,"./helpers/validate-selector":7,debounce:2,"object-assign":3}],5:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.default=function(){var e={touch:!!("ontouchstart"in window||window.navigator&&window.navigator.msPointerEnabled&&window.MSGesture||window.DocumentTouch&&document instanceof DocumentTouch),ie:window.navigator.userAgent.indexOf("MSIE")>0||!!window.navigator.userAgent.match(/Trident.*rv\:11\./),small:window.matchMedia("(max-width: 34em)").matches};return e.touch&&!e.ie||e.small},t.exports=n.default},{}],6:[function(e,t,n){"use strict";function r(e){document.body.contains(e)||i.insertAdjacentElement("afterbegin",e)}function o(e){null===i&&(i=e.parentNode),document.body.contains(e)&&i.removeChild(e)}Object.defineProperty(n,"__esModule",{value:!0}),n.restoreVideo=r,n.removeVideo=o;var i=null},{}],7:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.default=function(e,t){if("undefined"==typeof e)throw new Error(t+" requires a video selector as first argument.");if(e="string"==typeof e?document.querySelector(e):e,"video"!==e.nodeName.toLowerCase())throw new Error(t+" requires a valid video selector. You passed a <"+e.nodeName+">");return e},t.exports=n.default},{}]},{},[4])(4)}); -------------------------------------------------------------------------------- /src/static/layui/css/modules/code.css: -------------------------------------------------------------------------------- 1 | html #layuicss-skincodecss{display:none;position:absolute;width:1989px}.layui-code-h3,.layui-code-view{position:relative;font-size:12px}.layui-code-view{display:block;margin:10px 0;padding:0;border:1px solid #eee;border-left-width:6px;background-color:#FAFAFA;color:#333;font-family:Courier New}.layui-code-h3{padding:0 10px;height:40px;line-height:40px;border-bottom:1px solid #eee}.layui-code-h3 a{position:absolute;right:10px;top:0;color:#999}.layui-code-view .layui-code-ol{position:relative;overflow:auto}.layui-code-view .layui-code-ol li{position:relative;margin-left:45px;line-height:20px;padding:0 10px;border-left:1px solid #e2e2e2;list-style-type:decimal-leading-zero;*list-style-type:decimal;background-color:#fff}.layui-code-view .layui-code-ol li:first-child{padding-top:10px}.layui-code-view .layui-code-ol li:last-child{padding-bottom:10px}.layui-code-view pre{margin:0}.layui-code-notepad{border:1px solid #0C0C0C;border-left-color:#3F3F3F;background-color:#0C0C0C;color:#C2BE9E}.layui-code-notepad .layui-code-h3{border-bottom:none}.layui-code-notepad .layui-code-ol li{background-color:#3F3F3F;border-left:none}.layui-code-demo .layui-code{visibility:visible!important;margin:-15px;border-top:none;border-right:none;border-bottom:none}.layui-code-demo .layui-tab-content{padding:15px;border-top:none} -------------------------------------------------------------------------------- /src/static/layui/css/modules/laydate/default/laydate.css: -------------------------------------------------------------------------------- 1 | .laydate-set-ym,.layui-laydate,.layui-laydate *,.layui-laydate-list{box-sizing:border-box}html #layuicss-laydate{display:none;position:absolute;width:1989px}.layui-laydate *{margin:0;padding:0}.layui-laydate{position:absolute;z-index:66666666;margin:5px 0;border-radius:2px;font-size:14px;-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;animation-name:laydate-downbit}.layui-laydate-main{width:272px}.layui-laydate-content td,.layui-laydate-header *,.layui-laydate-list li{transition-duration:.3s;-webkit-transition-duration:.3s}@keyframes laydate-downbit{0%{opacity:.3;transform:translate3d(0,-5px,0)}100%{opacity:1;transform:translate3d(0,0,0)}}.layui-laydate-static{position:relative;z-index:0;display:inline-block;margin:0;-webkit-animation:none;animation:none}.laydate-ym-show .laydate-next-m,.laydate-ym-show .laydate-prev-m{display:none!important}.laydate-ym-show .laydate-next-y,.laydate-ym-show .laydate-prev-y{display:inline-block!important}.laydate-time-show .laydate-set-ym span[lay-type=month],.laydate-time-show .laydate-set-ym span[lay-type=year],.laydate-time-show .layui-laydate-header .layui-icon,.laydate-ym-show .laydate-set-ym span[lay-type=month]{display:none!important}.layui-laydate-header{position:relative;line-height:30px;padding:10px 70px 5px}.layui-laydate-header *{display:inline-block;vertical-align:bottom}.layui-laydate-header i{position:absolute;top:10px;padding:0 5px;color:#999;font-size:18px;cursor:pointer}.layui-laydate-header i.laydate-prev-y{left:15px}.layui-laydate-header i.laydate-prev-m{left:45px}.layui-laydate-header i.laydate-next-y{right:15px}.layui-laydate-header i.laydate-next-m{right:45px}.laydate-set-ym{width:100%;text-align:center;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.laydate-set-ym span{padding:0 10px;cursor:pointer}.laydate-time-text{cursor:default!important}.layui-laydate-content{position:relative;padding:10px;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.layui-laydate-content table{border-collapse:collapse;border-spacing:0}.layui-laydate-content td,.layui-laydate-content th{width:36px;height:30px;padding:5px;text-align:center}.layui-laydate-content td{position:relative;cursor:pointer}.laydate-day-mark{position:absolute;left:0;top:0;width:100%;line-height:30px;font-size:12px;overflow:hidden}.laydate-day-mark::after{position:absolute;content:'';right:2px;top:2px;width:5px;height:5px;border-radius:50%}.layui-laydate-footer{position:relative;height:46px;line-height:26px;padding:10px}.layui-laydate-footer span{display:inline-block;vertical-align:top;height:26px;line-height:24px;padding:0 10px;border:1px solid #C9C9C9;border-radius:2px;background-color:#fff;font-size:12px;cursor:pointer;white-space:nowrap;transition:all .3s}.layui-laydate-list>li,.layui-laydate-range .layui-laydate-main{display:inline-block;vertical-align:middle}.layui-laydate-footer span:hover{color:#5FB878}.layui-laydate-footer span.layui-laydate-preview{cursor:default;border-color:transparent!important}.layui-laydate-footer span.layui-laydate-preview:hover{color:#666}.layui-laydate-footer span:first-child.layui-laydate-preview{padding-left:0}.laydate-footer-btns{position:absolute;right:10px;top:10px}.laydate-footer-btns span{margin:0 0 0 -1px}.layui-laydate-list{position:absolute;left:0;top:0;width:100%;height:100%;padding:10px;background-color:#fff}.layui-laydate-list>li{position:relative;width:33.3%;height:36px;line-height:36px;margin:3px 0;text-align:center;cursor:pointer}.laydate-month-list>li{width:25%;margin:17px 0}.laydate-time-list>li{height:100%;margin:0;line-height:normal;cursor:default}.laydate-time-list p{position:relative;top:-4px;line-height:29px}.laydate-time-list ol{height:181px;overflow:hidden}.laydate-time-list>li:hover ol{overflow-y:auto}.laydate-time-list ol li{width:130%;padding-left:33px;height:30px;line-height:30px;text-align:left;cursor:pointer}.layui-laydate-hint{position:absolute;top:115px;left:50%;width:250px;margin-left:-125px;line-height:20px;padding:15px;text-align:center;font-size:12px}.layui-laydate-range{width:546px}.layui-laydate-range .laydate-main-list-1 .layui-laydate-content,.layui-laydate-range .laydate-main-list-1 .layui-laydate-header{border-left:1px solid #e2e2e2}.layui-laydate,.layui-laydate-hint{border:1px solid #d2d2d2;box-shadow:0 2px 4px rgba(0,0,0,.12);background-color:#fff;color:#666}.layui-laydate-header{border-bottom:1px solid #e2e2e2}.layui-laydate-header i:hover,.layui-laydate-header span:hover{color:#5FB878}.layui-laydate-content{border-top:none 0;border-bottom:none 0}.layui-laydate-content th{font-weight:400;color:#333}.layui-laydate-content td{color:#666}.layui-laydate-content td.laydate-selected{background-color:#B5FFF8}.laydate-selected:hover{background-color:#00F7DE!important}.layui-laydate-content td:hover,.layui-laydate-list li:hover{background-color:#eee;color:#333}.laydate-time-list li ol{margin:0;padding:0;border:1px solid #e2e2e2;border-left-width:0}.laydate-time-list li:first-child ol{border-left-width:1px}.laydate-time-list>li:hover{background:0 0}.layui-laydate-content .laydate-day-next,.layui-laydate-content .laydate-day-prev{color:#d2d2d2}.laydate-selected.laydate-day-next,.laydate-selected.laydate-day-prev{background-color:#f8f8f8!important}.layui-laydate-footer{border-top:1px solid #e2e2e2}.layui-laydate-hint{color:#FF5722}.laydate-day-mark::after{background-color:#5FB878}.layui-laydate-content td.layui-this .laydate-day-mark::after{display:none}.layui-laydate-footer span[lay-type=date]{color:#5FB878}.layui-laydate .layui-this{background-color:#009688!important;color:#fff!important}.layui-laydate .laydate-disabled,.layui-laydate .laydate-disabled:hover{background:0 0!important;color:#d2d2d2!important;cursor:not-allowed!important;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.laydate-theme-molv{border:none}.laydate-theme-molv.layui-laydate-range{width:548px}.laydate-theme-molv .layui-laydate-main{width:274px}.laydate-theme-molv .layui-laydate-header{border:none;background-color:#009688}.laydate-theme-molv .layui-laydate-header i,.laydate-theme-molv .layui-laydate-header span{color:#f6f6f6}.laydate-theme-molv .layui-laydate-header i:hover,.laydate-theme-molv .layui-laydate-header span:hover{color:#fff}.laydate-theme-molv .layui-laydate-content{border:1px solid #e2e2e2;border-top:none;border-bottom:none}.laydate-theme-molv .laydate-main-list-1 .layui-laydate-content{border-left:none}.laydate-theme-grid .laydate-month-list>li,.laydate-theme-grid .laydate-year-list>li,.laydate-theme-grid .layui-laydate-content td,.laydate-theme-grid .layui-laydate-content thead,.laydate-theme-molv .layui-laydate-footer{border:1px solid #e2e2e2}.laydate-theme-grid .laydate-selected,.laydate-theme-grid .laydate-selected:hover{background-color:#f2f2f2!important;color:#009688!important}.laydate-theme-grid .laydate-selected.laydate-day-next,.laydate-theme-grid .laydate-selected.laydate-day-prev{color:#d2d2d2!important}.laydate-theme-grid .laydate-month-list,.laydate-theme-grid .laydate-year-list{margin:1px 0 0 1px}.laydate-theme-grid .laydate-month-list>li,.laydate-theme-grid .laydate-year-list>li{margin:0 -1px -1px 0}.laydate-theme-grid .laydate-year-list>li{height:43px;line-height:43px}.laydate-theme-grid .laydate-month-list>li{height:71px;line-height:71px} -------------------------------------------------------------------------------- /src/static/layui/css/modules/layer/default/icon-ext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/layui/css/modules/layer/default/icon-ext.png -------------------------------------------------------------------------------- /src/static/layui/css/modules/layer/default/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/layui/css/modules/layer/default/icon.png -------------------------------------------------------------------------------- /src/static/layui/css/modules/layer/default/loading-0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/layui/css/modules/layer/default/loading-0.gif -------------------------------------------------------------------------------- /src/static/layui/css/modules/layer/default/loading-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/layui/css/modules/layer/default/loading-1.gif -------------------------------------------------------------------------------- /src/static/layui/css/modules/layer/default/loading-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/layui/css/modules/layer/default/loading-2.gif -------------------------------------------------------------------------------- /src/static/layui/font/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/layui/font/iconfont.eot -------------------------------------------------------------------------------- /src/static/layui/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/layui/font/iconfont.ttf -------------------------------------------------------------------------------- /src/static/layui/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/layui/font/iconfont.woff -------------------------------------------------------------------------------- /src/static/layui/font/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/layui/font/iconfont.woff2 -------------------------------------------------------------------------------- /src/static/mymod/forgot.js: -------------------------------------------------------------------------------- 1 | /* 2 | forgot忘记密码页面 3 | */ 4 | layui.define(['base', 'form', 'layer'], function(exports) { 5 | var base = layui.base, 6 | form = layui.form, 7 | layer = layui.layer, 8 | $ = layui.jquery; 9 | //表单自定义校验 10 | form.verify({ 11 | passwd: function(value, item) { //value:表单的值、item:表单的DOM对象 12 | if (value.length < 6 || value.length > 30) { 13 | return '密码长度应在6到30个字符之间!'; 14 | } 15 | } 16 | }); 17 | //登录按钮事件 18 | form.on("submit(forgot)", function(data) { 19 | base.ajax("/api/forgotpass/", function(res) { 20 | layer.msg("重置成功", { 21 | icon: 1, 22 | time: 2000 23 | }, function() { 24 | location.href = res.nextUrl; 25 | }); 26 | }, { 27 | data: data.field, 28 | method: "post", 29 | msgprefix: false, 30 | beforeSend: function() { 31 | $("#submitbutton").attr({ 32 | disabled: "disabled" 33 | }); 34 | $('#submitbutton').addClass("layui-disabled"); 35 | }, 36 | complete: function() { 37 | $('#submitbutton').removeAttr("disabled"); 38 | $('#submitbutton').removeClass("layui-disabled"); 39 | }, 40 | fail: function(res) { 41 | layer.msg(res.msg, { 42 | icon: 7, 43 | time: 3000 44 | }); 45 | } 46 | }); 47 | return false; 48 | }); 49 | //输出接口 50 | exports('forgot', null); 51 | }); -------------------------------------------------------------------------------- /src/static/mymod/message.js: -------------------------------------------------------------------------------- 1 | /* 2 | 我的消息页面 3 | */ 4 | 5 | layui.define(["passport", "util", "layer", "laytpl"], function(exports) { 6 | 'use strict'; 7 | var passport = layui.passport, 8 | util = layui.util, 9 | layer = layui.layer, 10 | laytpl = layui.laytpl, 11 | $ = layui.jquery; 12 | laytpl.config({ 13 | open: '<%', 14 | close: '%>' 15 | }); 16 | var delAll = $('#LAY_delallmsg'), 17 | minemsg = $('#LAY_minemsg'), 18 | msgtpl = '<%# var len = d.data.length;var msgtypemap = {system: "系统消息", product: "产品消息"};var msgstatusmap = {1: "未读", 0: "已读"};var msgmarkmap = {0: "未读", 1: "已读"};\ 19 | if(len === 0){ %>\ 20 |
您暂时没有消息
\ 21 | <%# } else { %>\ 22 |
    \ 23 | <%# for(var i = 0; i < len; i++){ %>\ 24 |
  • \ 25 |
    <% d.data[i].msgContent %>
    \ 26 |

    <% msgstatusmap[d.data[i].msgStatus] %>的<% msgtypemap[d.data[i].msgType] %>:<% layui.util.timeAgo(d.data[i].msgTime*1000) %>标记为<% msgmarkmap[d.data[i].msgStatus] %>删除消息

    \ 27 |
  • \ 28 | <%# } %>\ 29 |
\ 30 | <%# } %>', 31 | delEnd = function(clear) { 32 | // 当clear为true时或者没有消息列表时清空并显示没有消息 33 | if (clear || minemsg.find('.mine-msg li').length === 0) { 34 | minemsg.html('
您暂时没有消息
'); 35 | } 36 | }; 37 | //获取用户消息列表 38 | passport.ajax("/api/user/message/?Action=getList&desc=true&msgStatus=" + passport.getUrlQuery("status", 1), function(res) { 39 | if (res.code == 0) { 40 | var html = laytpl(msgtpl).render(res); 41 | minemsg.html(html); 42 | if (res.data.length > 0) { 43 | delAll.removeClass('layui-hide'); 44 | } 45 | } 46 | }, { 47 | method: "get", 48 | msgprefix: "拉取用户消息失败:" 49 | }); 50 | //标记消息状态 51 | minemsg.on('click', '.mine-msg li .fly-mark', function() { 52 | var othis = $(this).parents('li'), 53 | id = othis.attr('data-id'), 54 | status = parseInt(othis.attr('data-status')); 55 | var msgstatusmap = { 56 | 1: "未读", 57 | 0: "已读" 58 | }, 59 | antimsgstatusmap = { 60 | 0: "未读", 61 | 1: "已读" 62 | }; 63 | passport.ajax("/api/user/message/?Action=markMessage", function(res) { 64 | if (res.code == 0) { 65 | popup("标记消息成功"); 66 | othis.remove(); 67 | delEnd(); 68 | } 69 | }, { 70 | data: { 71 | msgId: id 72 | }, 73 | method: "post", 74 | msgprefix: "标记消息失败:" 75 | }); 76 | }); 77 | //删除一条消息 78 | minemsg.on('click', '.mine-msg li .fly-remove', function() { 79 | var othis = $(this).parents('li'), 80 | id = othis.attr('data-id'); 81 | passport.ajax("/api/user/message/?Action=delMessage", function(res) { 82 | if (res.code == 0) { 83 | popup("删除消息成功"); 84 | othis.remove(); 85 | delEnd(); 86 | } 87 | }, { 88 | data: { 89 | msgId: id 90 | }, 91 | method: "delete", 92 | msgprefix: "删除消息失败:" 93 | }); 94 | }); 95 | //删除全部消息 96 | delAll.on('click', function() { 97 | var othis = $(this); 98 | layer.confirm('确定清空消息吗?
此操作将清空所有已读、未读消息!', { 99 | icon: 3, 100 | title: '温馨提示' 101 | }, function(index) { 102 | passport.ajax("/api/user/message/?Action=clearMessage", function(res) { 103 | if (res.code == 0) { 104 | layer.close(index); 105 | othis.addClass('layui-hide'); 106 | delEnd(true); 107 | popup("清空消息成功"); 108 | } 109 | }, { 110 | method: "delete", 111 | msgprefix: "清空消息失败:" 112 | }); 113 | }); 114 | }); 115 | //输出接口 116 | exports('message', null); 117 | }); -------------------------------------------------------------------------------- /src/static/mymod/oauthguide.js: -------------------------------------------------------------------------------- 1 | /* 2 | OAuthGuide第三方引导页面 3 | */ 4 | layui.define(['base', 'form', 'layer', 'element'], function(exports) { 5 | var base = layui.base, 6 | form = layui.form, 7 | layer = layui.layer, 8 | element = layui.element, 9 | $ = layui.jquery; 10 | //显示当前tab 11 | if (location.hash) { 12 | element.tabChange('guide', location.hash.replace(/^#/, '')); 13 | } 14 | //监听tab切换 15 | element.on('tab(guide)', function() { 16 | var othis = $(this), 17 | layid = othis.attr('lay-id'); 18 | if (layid) { 19 | location.hash = layid; 20 | } 21 | }); 22 | //表单自定义校验 23 | form.verify({ 24 | passwd: function(value, item) { //value:表单的值、item:表单的DOM对象 25 | if (value.length < 6 || value.length > 30) { 26 | return '密码长度应在6到30个字符之间!'; 27 | } 28 | } 29 | }); 30 | //绑定登录事件 31 | form.on("submit(bindLogin)", function(data) { 32 | var url = base.getUrlQuery("sso") ? "/OAuthGuide?Action=bindLogin&sso=" + base.getUrlQuery("sso") : "/OAuthGuide?Action=bindLogin"; 33 | console.log(url); 34 | base.ajax(url, function(res) { 35 | layer.msg("绑定成功,跳转中", { 36 | icon: 1, 37 | time: 2000 38 | }, function() { 39 | location.href = res.nextUrl; 40 | }); 41 | }, { 42 | data: data.field, 43 | method: "post", 44 | msgprefix: false, 45 | beforeSend: function() { 46 | $("#submitbutton").attr({ 47 | disabled: "disabled" 48 | }); 49 | $('#submitbutton').addClass("layui-disabled"); 50 | }, 51 | complete: function() { 52 | $('#submitbutton').removeAttr("disabled"); 53 | $('#submitbutton').removeClass("layui-disabled"); 54 | }, 55 | fail: function(res) { 56 | layer.msg(res.msg, { 57 | icon: 7, 58 | time: 3000 59 | }); 60 | } 61 | }); 62 | return false; 63 | }); 64 | //直接登录事件 65 | form.on("submit(directLogin)", function(data) { 66 | var url = base.getUrlQuery("sso") ? "/OAuthGuide?Action=directLogin&sso=" + base.getUrlQuery("sso") : "/OAuthGuide?Action=directLogin"; 67 | console.log(url); 68 | base.ajax(url, function(res) { 69 | layer.msg("登录成功,跳转中", { 70 | icon: 1, 71 | time: 2000 72 | }, function() { 73 | location.href = res.nextUrl; 74 | }); 75 | }, { 76 | data: data.field, 77 | method: "post", 78 | msgprefix: false, 79 | beforeSend: function() { 80 | $("#submitbutton2").attr({ 81 | disabled: "disabled" 82 | }); 83 | $('#submitbutton2').addClass("layui-disabled"); 84 | }, 85 | complete: function() { 86 | $('#submitbutton2').removeAttr("disabled"); 87 | $('#submitbutton2').removeClass("layui-disabled"); 88 | }, 89 | fail: function(res) { 90 | layer.msg(res.msg, { 91 | icon: 7, 92 | time: 3000 93 | }); 94 | } 95 | }); 96 | return false; 97 | }); 98 | //输出接口 99 | exports('oauthguide', null); 100 | }); -------------------------------------------------------------------------------- /src/static/mymod/signin.js: -------------------------------------------------------------------------------- 1 | /* 2 | signin登录页面 3 | */ 4 | layui.define(['base', 'form', 'layer'], function(exports) { 5 | var base = layui.base, 6 | form = layui.form, 7 | layer = layui.layer, 8 | $ = layui.jquery; 9 | //表单自定义校验 10 | form.verify({ 11 | passwd: function(value, item) { //value:表单的值、item:表单的DOM对象 12 | if (value.length < 6 || value.length > 30) { 13 | return '密码长度应在6到30个字符之间!'; 14 | } 15 | } 16 | }); 17 | //登录按钮事件 18 | form.on("submit(signIn)", function(data) { 19 | var url = base.getUrlQuery("sso") ? "/signIn?sso=" + base.getUrlQuery("sso") : "/signIn"; 20 | base.ajax(url, function(res) { 21 | location.href = res.nextUrl; 22 | }, { 23 | data: data.field, 24 | method: "post", 25 | msgprefix: false, 26 | beforeSend: function() { 27 | $("#submitbutton").attr({ 28 | disabled: "disabled" 29 | }); 30 | $('#submitbutton').addClass("layui-disabled"); 31 | }, 32 | complete: function() { 33 | $('#submitbutton').removeAttr("disabled"); 34 | $('#submitbutton').removeClass("layui-disabled"); 35 | }, 36 | fail: function(res) { 37 | layer.msg(res.msg, { 38 | icon: 7, 39 | time: 3000 40 | }); 41 | } 42 | }); 43 | return false; 44 | }); 45 | //输出接口 46 | exports("signin", null); 47 | }); -------------------------------------------------------------------------------- /src/static/mymod/signup.js: -------------------------------------------------------------------------------- 1 | /* 2 | signup注册页面 3 | */ 4 | layui.define(['base', 'form', 'layer'], function(exports) { 5 | var base = layui.base, 6 | form = layui.form, 7 | layer = layui.layer, 8 | $ = layui.jquery; 9 | //表单自定义校验 10 | form.verify({ 11 | passwd: function(value, item) { //value:表单的值、item:表单的DOM对象 12 | if (value.length < 6 || value.length > 30) { 13 | return '密码长度应在6到30个字符之间!'; 14 | } 15 | }, 16 | repasswd: function(value, item) { //value:表单的值、item:表单的DOM对象 17 | if (value.length < 6 || value.length > 30) { 18 | return '密码长度应在6到30个字符之间!'; 19 | } 20 | var passwd = $('input[name="password"]').val(); 21 | if (passwd != value) { 22 | return '密码和重复密码不同,请重新输入!'; 23 | } 24 | }, 25 | terms: function(value, item) { //value:表单的值、item:表单的DOM对象 26 | if (!document.getElementById("terms").checked) { 27 | return "请阅读并同意服务条款后继续!"; 28 | } 29 | } 30 | }); 31 | //登录按钮事件 32 | form.on("submit(signUp)", function(data) { 33 | base.ajax("/signUp", function(res) { 34 | layer.msg("注册成功", { 35 | icon: 1, 36 | time: 2000 37 | }, function() { 38 | location.href = res.nextUrl; 39 | }); 40 | }, { 41 | data: data.field, 42 | method: "post", 43 | msgprefix: false, 44 | beforeSend: function() { 45 | $("#submitbutton").attr({ 46 | disabled: "disabled" 47 | }); 48 | $('#submitbutton').addClass("layui-disabled"); 49 | }, 50 | complete: function() { 51 | $('#submitbutton').removeAttr("disabled"); 52 | $('#submitbutton').removeClass("layui-disabled"); 53 | }, 54 | fail: function(res) { 55 | layer.msg(res.msg, { 56 | icon: 7, 57 | time: 3000 58 | }); 59 | } 60 | }); 61 | return false; 62 | }); 63 | //输出接口 64 | exports("signup", null); 65 | }); -------------------------------------------------------------------------------- /src/static/spop-0.1.3/spop.min.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";/*! 2 | * smallPop 0.1.2 | https://github.com/silvio-r/spop 3 | * Copyright (c) 2015 Sílvio Rosa @silvior_ 4 | * MIT license 5 | */.spop-container{z-index:2000;position:fixed}.spop-container,.spop-container *,.spop-container :after,.spop-container :before{box-sizing:border-box}.spop--top-left{top:0;left:0}.spop--top-left .spop{-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}.spop--top-center{top:0;left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}.spop--top-center .spop{-webkit-transform-origin:50% 0;-ms-transform-origin:50% 0;transform-origin:50% 0}.spop--top-right{top:0;right:0}.spop--top-right .spop{-webkit-transform-origin:100% 0;-ms-transform-origin:100% 0;transform-origin:100% 0}.spop--center{top:50%;left:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}.spop--center .spop{-webkit-transform-origin:50% 0;-ms-transform-origin:50% 0;transform-origin:50% 0}.spop--bottom-left{bottom:0;left:0}.spop--bottom-left .spop{-webkit-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.spop--bottom-center{bottom:0;left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}.spop--bottom-center .spop{-webkit-transform-origin:50% 100%;-ms-transform-origin:50% 100%;transform-origin:50% 100%}.spop--bottom-right{bottom:0;right:0}.spop--bottom-right .spop{-webkit-transform-origin:100% 100%;-ms-transform-origin:100% 100%;transform-origin:100% 100%}@media screen and (max-width:30em){.spop--bottom-center,.spop--bottom-left,.spop--bottom-right,.spop--top-center,.spop--top-left,.spop--top-right{top:auto;bottom:0;left:0;right:0;margin-left:0;-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}.spop--bottom-center .spop,.spop--bottom-left .spop,.spop--bottom-right .spop,.spop--top-center .spop,.spop--top-left .spop,.spop--top-right .spop{-webkit-transform-origin:50% 100%;-ms-transform-origin:50% 100%;transform-origin:50% 100%}.spop{border-bottom:1px solid rgba(0,0,0,.15)}}.spop{position:relative;min-height:56px;line-height:1.25;font-size:14px;-webkit-transform:translateZ(0);transform:translateZ(0)}@media screen and (min-width:30em){.spop{border-radius:2px;width:320px;margin:.7em}}.spop--error,.spop--info,.spop--success,.spop--warning{color:#fff;background-color:#454A56}@-webkit-keyframes spopIn{0%{-webkit-transform:scale(.2,.2);transform:scale(.2,.2)}95%{-webkit-transform:scale(1.1,1.1);transform:scale(1.1,1.1)}100%{-webkit-transform:scale(1,1);transform:scale(1,1)}}@keyframes spopIn{0%{-webkit-transform:scale(.2,.2);transform:scale(.2,.2)}95%{-webkit-transform:scale(1.1,1.1);transform:scale(1.1,1.1)}100%{-webkit-transform:scale(1,1);transform:scale(1,1)}}@-webkit-keyframes spopOut{0%{opacity:1;-webkit-transform:scale(1,1);transform:scale(1,1)}20%{-webkit-transform:scale(1.1,1.1);transform:scale(1.1,1.1)}100%{opacity:0;-webkit-transform:scale(0,0);transform:scale(0,0)}}@keyframes spopOut{0%{opacity:1;-webkit-transform:scale(1,1);transform:scale(1,1)}20%{-webkit-transform:scale(1.1,1.1);transform:scale(1.1,1.1)}100%{opacity:0;-webkit-transform:scale(0,0);transform:scale(0,0)}}.spop--out{-webkit-animation:spopOut .4s ease-in-out;animation:spopOut .4s ease-in-out}.spop--in{-webkit-animation:spopIn .4s ease-in-out;animation:spopIn .4s ease-in-out}.spop-body{padding:1.4em}.spop-body p{margin:0}.spop-body a{color:#fff;text-decoration:underline}.spop-body a:hover{color:rgba(255,255,255,.8);text-decoration:none}.spop-title{margin-top:0;margin-bottom:.25em;color:#fff}.spop-close{position:absolute;right:0;top:0;height:32px;width:32px;padding-top:7px;padding-right:7px;font-size:22px;font-weight:700;text-align:right;line-height:.6;color:#fff;opacity:.5}.spop-close:hover{opacity:.7;cursor:pointer}.spop-icon{position:absolute;top:13px;left:16px;width:30px;height:30px;border-radius:50%;-webkit-animation:spopIn .4s .4s ease-in-out;animation:spopIn .4s .4s ease-in-out}.spop-icon:after,.spop-icon:before{content:"";position:absolute;display:block}.spop-icon+.spop-body{padding-left:4.2em}.spop-icon--error,.spop-icon--info{border:2px solid #3a95ed}.spop-icon--error:before,.spop-icon--info:before{top:5px;left:11px;width:4px;height:4px;background-color:#3a95ed}.spop-icon--error:after,.spop-icon--info:after{top:12px;left:11px;width:4px;height:9px;background-color:#3a95ed}.spop-icon--error{border-color:#ff5656}.spop-icon--error:before{top:16px;background-color:#ff5656}.spop-icon--error:after{top:5px;background-color:#ff5656}.spop-icon--success{border:2px solid #2ecc54}.spop-icon--success:before{top:7px;left:7px;width:13px;height:8px;border-bottom:3px solid #2ecc54;border-left:3px solid #2ecc54;-webkit-transform:rotate(-45deg);-ms-transform:rotate(-45deg);transform:rotate(-45deg)}.spop-icon--warning{border:2px solid #fcd000}.spop-icon--warning:before{top:7px;left:7px;width:0;height:0;border-style:solid;border-color:transparent transparent #fcd000;border-width:0 6px 10px} -------------------------------------------------------------------------------- /src/static/spop-0.1.3/spop.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * smallPop 0.1.2 | https://github.com/silvio-r/spop 3 | * Copyright (c) 2015 Sílvio Rosa @silvior_ 4 | * MIT license 5 | */ 6 | !function(){"use strict";function t(t,o){return"string"==typeof t?(o||document).getElementById(t):t||null}function o(t,o){t.classList?t.classList.remove(o):t.className=t.className.replace(new RegExp("(^|\\b)"+o.split(" ").join("|")+"(\\b|$)","gi")," ")}function e(t,o){for(var e in o)o.hasOwnProperty(e)&&(t[e]=o[e]);return t}var s,i,p,n,r,c,l,h,a=390,u=function(o,p){if(this.defaults={template:null,style:"info",autoclose:!1,position:"top-right",icon:!0,group:!1,onOpen:!1,onClose:!1},i=e(this.defaults,spop.defaults),"string"==typeof o||"string"==typeof p)s={template:o,style:p||i.style};else{if("object"!=typeof o)return console.error("Invalid arguments."),!1;s=o}this.opt=e(i,s),t("spop--"+this.opt.group)&&this.remove(t("spop--"+this.opt.group)),this.open()};u.prototype.create=function(o){p=t(this.getPosition("spop--",this.opt.position)),n=this.opt.icon?'':"",r='
×
'+n+'
'+o+"
",p||(this.popContainer=document.createElement("div"),this.popContainer.setAttribute("class","spop-container "+this.getPosition("spop--",this.opt.position)),this.popContainer.setAttribute("id",this.getPosition("spop--",this.opt.position)),document.body.appendChild(this.popContainer),p=t(this.getPosition("spop--",this.opt.position))),this.pop=document.createElement("div"),this.pop.setAttribute("class","spop spop--out spop--in "+this.getStyle("spop--",this.opt.style)),this.opt.group&&"string"==typeof this.opt.group&&this.pop.setAttribute("id","spop--"+this.opt.group),this.pop.setAttribute("role","alert"),this.pop.innerHTML=r,p.appendChild(this.pop)},u.prototype.getStyle=function(t,o){return c={success:"success",error:"error",warning:"warning"},t+(c[o]||"info")},u.prototype.getPosition=function(t,o){return l={"top-left":"top-left","top-center":"top-center","top-right":"top-right","bottom-left":"bottom-left","bottom-center":"bottom-center","bottom-right":"bottom-right"},t+(l[o]||"top-right")},u.prototype.open=function(){this.create(this.opt.template),this.opt.onOpen&&this.opt.onOpen(),this.close()},u.prototype.close=function(){this.opt.autoclose&&"number"==typeof this.opt.autoclose&&(this.autocloseTimer=setTimeout(this.remove.bind(this,this.pop),this.opt.autoclose)),this.pop.addEventListener("click",this.addListeners.bind(this),!1)},u.prototype.addListeners=function(t){h=t.target.getAttribute("data-spop"),"close"===h&&(this.autocloseTimer&&clearTimeout(this.autocloseTimer),this.remove(this.pop))},u.prototype.remove=function(t){this.opt.onClose&&this.opt.onClose(),o(t,"spop--in"),setTimeout(function(){document.body.contains(t)&&t.parentNode.removeChild(t)},a)},window.spop=function(t,o){return t&&window.addEventListener?new u(t,o):!1},spop.defaults={}}(); -------------------------------------------------------------------------------- /src/static/videos/bg.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/videos/bg.mp4 -------------------------------------------------------------------------------- /src/static/videos/bg.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/passport/8a3dcd635b0f3a202f440d61a98fa25b37fe3419/src/static/videos/bg.webm -------------------------------------------------------------------------------- /src/templates/auth/OAuthGuide.html: -------------------------------------------------------------------------------- 1 | {% extends "auth/base.html" %} 2 | 3 | {% block title %}第三方账号登录引导{% endblock %} 4 | 5 | {% block head %} 6 | 7 | 11 | {% endblock %} 12 | 13 | {% block container %} 14 | 71 | {% endblock %} 72 | 73 | {% block script %} 74 | 79 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/auth/base.html: -------------------------------------------------------------------------------- 1 | {% extends "public/base.html" %} 2 | 3 | {% block baseTitle %} 4 | {% block title %}{% endblock %} - SaintIC Passport 5 | {% endblock %} 6 | 7 | {% block baseHead %} 8 | 9 | 10 | {% block head %}{% endblock %} 11 | {% endblock %} 12 | 13 | {% block baseHtml %} 14 |
15 |
16 | 20 |
21 |
22 | {% block container %}{% endblock %} 23 | {% endblock %} 24 | 25 | {% block baseScript %} 26 | 27 | 31 | {% block script %}{% endblock %} 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /src/templates/auth/forgot.html: -------------------------------------------------------------------------------- 1 | {% extends "auth/base.html" %} 2 | 3 | {% block title %}忘记密码{% endblock %} 4 | 5 | {% block container %} 6 | 31 | {% endblock %} 32 | 33 | {% block script %} 34 | 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /src/templates/auth/signIn.html: -------------------------------------------------------------------------------- 1 | {% extends "auth/base.html" %} 2 | 3 | {% block title %}登录{% endblock %} 4 | 5 | {% block container %} 6 | 27 | {% endblock %} 28 | 29 | {% block script %} 30 | 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /src/templates/auth/signUp.html: -------------------------------------------------------------------------------- 1 | {% extends "auth/base.html" %} 2 | 3 | {% block title %}注册{% endblock %} 4 | 5 | {% block container %} 6 |