├── .gitignore ├── LICENSE ├── README.md ├── client ├── 1.png ├── 2.png ├── JsClient.html ├── PythonClient.py └── ShellClient.sh └── server ├── main.py ├── requirements.txt └── utils ├── Signature.py ├── __init__.py └── tool.py /.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 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mr.tao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flask-apiSign-demo 2 | Api签名验证样例 3 | 4 | ## 使用 5 | ``` 6 | cd server ; pip install -r requirements.txt 7 | python main.py 8 | ``` 9 | 10 | ## 客户端 11 | 12 | ##### Python、Shell 13 | 14 | ![](client/1.png) 15 | 16 | ##### JavaScript(HTML) 17 | 18 | ![](client/2.png) 19 | 20 | ## 签名描述 21 | 22 | #### 一、起因 23 | 24 | 为了实现基本的防抓取机制,对绝大多数采用了 Api 签名验证,在保证签名秘钥不泄露的前提下,具有一定的数据抓取防御能力。 25 | 1. 请求参数是否被篡改; 26 | 2. 请求来源是否合法; 27 | 3. 请求是否具有唯一性。 28 | 29 | #### 二、经过 30 | 31 | 1. 前提准备 32 | 33 | 接口提供方生成用户密钥,包含: 34 | 35 | 1.1 accesskey_id, 标识用户 36 | 37 | 1.2 accesskey_secret, 用户加密串(严格保管,仅用于加密不参与通信) 38 | 39 | 2. 公共参数 40 | 41 | 2.1 accesskey_id, 标识用户 42 | 43 | 2.2 version, 后端接口版本号 44 | 45 | 2.3 timestamp, 10位时间戳(客户端生成时间戳可以适当减几秒) 46 | 47 | 2.4 signature, uri请求参数签名(除signature外所有) 48 | 49 | *# 另可以定义其他参数,比如signMethod* 50 | 51 | 3. 签名过程 52 | 53 | 3.1.对除签名外的所有请求参数按key做的升序排列。 54 | 55 | `例如:有b=2一个私有参数,另加上公共参数后,按key排序后为:accesskey_id、b、timestamp、version` 56 | 57 | 3.2 把排序后的参数以"**参数名=参数值&**"的形式连接,末尾再加上"**accesskey_secret**",得到拼装字符串。 58 | 59 | `例如:accesskey_id=test&b=2×tamp=1511232761&version=v1&accesskey_secret` 60 | 61 | 3.3 将上一步得到的字符串MD5加密并转化为大写。 62 | 63 | `例如:signature=F833B331E572FD9D3D64A8D0737490B0` 64 | 65 | 3.4 最终请求示例。 66 | 67 | `timestamp=1511232761&b=2&version=v1&accesskey_id=test&signature=F833B331E572FD9D3D64A8D0737490B0` 68 | 69 | 4. 验证请求 70 | 71 | 4.1 验证版本(非必要) 72 | 73 | 4.2 验证时间戳是否有效(小于等于服务器时间戳且在30s之内请求有效) 74 | 75 | 4.3 验证accesskey_id是否有效 76 | 77 | 4.4 验证签名 78 | 79 | #### 三、返回 80 | 81 | 1. 正确返回 82 | ``` 83 | { 84 | "ping": "pong" 85 | } 86 | ``` 87 | 88 | 2. 错误返回 89 | 90 | 2.1 版本错误 91 | ``` 92 | { 93 | "msg": "Invalid version", 94 | "success": false 95 | } 96 | ``` 97 | 98 | 2.2 时间戳错误 99 | ``` 100 | { 101 | "msg": "Invalid timestamp", 102 | "success": false 103 | } 104 | ``` 105 | 106 | #### 四、参考 107 | 108 | 1. [API接口签名验证(详细描述主流签名方式)](http://www.hello1010.com/api-sign "API接口签名验证(详细描述主流签名方式)") 109 | 110 | 2. [Api:签名验证机制(提及关于POST签名说明)](https://github.com/Eliacy/YYMiOS/wiki/Api%EF%BC%9A%E7%AD%BE%E5%90%8D%E9%AA%8C%E8%AF%81%E6%9C%BA%E5%88%B6 "Api:签名验证机制(提及关于POST签名说明)") 111 | -------------------------------------------------------------------------------- /client/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/flask-apiSign-demo/18e38137af5a1217d435b919af4244e7025be568/client/1.png -------------------------------------------------------------------------------- /client/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staugur/flask-apiSign-demo/18e38137af5a1217d435b919af4244e7025be568/client/2.png -------------------------------------------------------------------------------- /client/JsClient.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | JavaScript Client 7 | 8 | 16 | 17 | 18 |

 19 | 
 20 | 
 21 | 
104 | 
105 | 


--------------------------------------------------------------------------------
/client/PythonClient.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | #
 3 | # Python Client
 4 | #
 5 | 
 6 | import hashlib, datetime, time
 7 | 
 8 | md5 = lambda pwd: hashlib.md5(pwd).hexdigest()
 9 | get_current_timestamp = lambda: int(time.mktime(datetime.datetime.now().timetuple()))
10 | 
11 | class RequestClient(object):
12 |     """ 接口签名客户端示例 """
13 | 
14 |     def __init__(self):
15 |         self._version = "v1"
16 |         self._accesskey_id = "demo_id"
17 |         self._accesskey_secret = "demo_secret"
18 | 
19 |     def _sign(self, parameters):
20 |         """ 签名
21 |         @param parameters dict: uri请求参数(包含除signature外的公共参数)
22 |         """
23 |         if "signature" in parameters:
24 |             parameters.pop("signature")
25 |         # NO.1 参数排序
26 |         _my_sorted = sorted(parameters.items(), key=lambda parameters: parameters[0])
27 |         # NO.2 排序后拼接字符串
28 |         canonicalizedQueryString = ''
29 |         for (k, v) in _my_sorted:
30 |             canonicalizedQueryString += '{}={}&'.format(k,v)
31 |         canonicalizedQueryString += self._accesskey_secret
32 |         # NO.3 加密返回签名: signature
33 |         return md5(canonicalizedQueryString).upper()
34 | 
35 |     def make_url(self, params={}):
36 |         """生成请求参数
37 |         @param params dict: uri请求参数(不包含公共参数)
38 |         """
39 |         if not isinstance(params, dict):
40 |             raise TypeError("params is not a dict")
41 |         # 获取当前时间戳
42 |         timestamp = get_current_timestamp() - 5
43 |         # 设置公共参数
44 |         publicParams = dict(accesskey_id=self._accesskey_id, version=self._version, timestamp=timestamp)
45 |         # 添加加公共参数
46 |         for k,v in publicParams.iteritems():
47 |             params[k] = v
48 |         uri = ''
49 |         for k,v in params.iteritems():
50 |             uri += '{}={}&'.format(k,v)
51 |         uri += 'signature=' + self._sign(params)
52 |         return uri
53 | 
54 |     def request(self):
55 |         """测试用例"""
56 |         import requests
57 |         params = dict(c=3,d=4,b=2,a=1)
58 |         url = 'http://127.0.0.1:1798/?'+self.make_url(params)
59 |         return requests.get(url).json()
60 | 


--------------------------------------------------------------------------------
/client/ShellClient.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/bash
 2 | #
 3 | # Shell Client
 4 | #
 5 | 
 6 | params=$1
 7 | # 格式 'key1=value, key2=value,...'
 8 | 
 9 | function make_url() {
10 |     python -c "
11 | import hashlib, datetime, time, re
12 | version='v1'
13 | accesskey_id='demo_id'
14 | accesskey_secret='demo_secret'
15 | md5 = lambda pwd: hashlib.md5(pwd).hexdigest()
16 | comma_pat = re.compile(r'\s*,\s*')
17 | get_current_timestamp = lambda: int(time.mktime(datetime.datetime.now().timetuple()))
18 | def _sign(parameters):
19 |     cqs, _my_sorted = '',sorted(parameters.items(), key=lambda parameters: parameters[0])
20 |     for (k, v) in _my_sorted:cqs += '{}={}&'.format(k,v)
21 |     cqs += accesskey_secret
22 |     return md5(cqs).upper()
23 | def make_url(params=''):
24 |     uri, params = '', dict([i.split('=') for i in re.split(comma_pat, params.strip()) if i])
25 |     for k,v in dict(accesskey_id=accesskey_id, version=version, timestamp=get_current_timestamp()-5).iteritems(): params[k] = v
26 |     for k,v in params.iteritems():uri += '{}={}&'.format(k,v)
27 |     uri += 'signature=' + _sign(params)
28 |     print uri
29 | make_url('${params}')
30 | "
31 | }
32 | 
33 | curl -sL "http://127.0.0.1:1798/?$(make_url)"
34 | echo


--------------------------------------------------------------------------------
/server/main.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | """
 3 |     main
 4 |     ~~~~~~~~~~~~~~
 5 | 
 6 |     flask-apiSign-demo
 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 taochengwei.
15 |     :license: MIT, see LICENSE for more details.
16 | """
17 | 
18 | __author__  = 'taochengwei'
19 | __doc__     = 'Api签名验证Demo'
20 | __date__    = '2017-11-20'
21 | 
22 | from flask import Flask, jsonify
23 | from utils.Signature import Signature
24 | 
25 | # 初始化定义application
26 | app = Flask(__name__)
27 | Sign = Signature()
28 | 
29 | @app.after_request
30 | def after_request(response):
31 |     response.headers['Access-Control-Allow-Origin'] = '*'
32 |     return response
33 | 
34 | @app.errorhandler(500)
35 | def server_error(error=None):
36 |     message = {
37 |         "msg": "Server error",
38 |         "code": 500
39 |     }
40 |     return jsonify(message), 500
41 | 
42 | @app.errorhandler(404)
43 | def not_found(error=None):
44 |     message = {
45 |         "msg": "Not found",
46 |         "code": 404
47 |     }
48 |     return jsonify(message), 404
49 | 
50 | @app.errorhandler(403)
51 | def Permission_denied(error=None):
52 |     message = {
53 |         "msg": "Permission denied",
54 |         "code": 403
55 |     }
56 |     return jsonify(message), 403
57 | 
58 | @app.route("/")
59 | @Sign.signature_required
60 | def index():
61 |     # 正确请求将返回以下内容,否则将被signature_required拦截,返回请求验证信息: {"msg": "Invaild message", "success": False}
62 |     return jsonify(ping="pong")
63 | 
64 | if __name__ == "__main__":
65 |     app.run(host="0.0.0.0", port=1798, debug=True)


--------------------------------------------------------------------------------
/server/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==0.10.1


--------------------------------------------------------------------------------
/server/utils/Signature.py:
--------------------------------------------------------------------------------
  1 | # -*- coding: utf-8 -*-
  2 | """
  3 |     utils.Signature
  4 |     ~~~~~~~~~~~~~~
  5 | 
  6 |     Api签名认证
  7 | 
  8 |     :copyright: (c) 2017 by taochengwei.
  9 |     :license: MIT, see LICENSE for more details.
 10 | """
 11 | 
 12 | from .tool import md5, get_current_timestamp
 13 | from functools import wraps
 14 | from flask import request, jsonify
 15 | 
 16 | class Signature(object):
 17 |     """ 接口签名认证 """
 18 | 
 19 |     def __init__(self):
 20 |         self._version = "v1"
 21 |         self._accessKeys = [
 22 |             {"accesskey_id": "demo_id", "accesskey_secret": "demo_secret"}
 23 |         ]
 24 |         # 时间戳有效时长,单位秒
 25 |         self._timestamp_expiration = 30
 26 | 
 27 |     def _check_req_timestamp(self, req_timestamp):
 28 |         """ 校验时间戳
 29 |         @pram req_timestamp str,int: 请求参数中的时间戳(10位)
 30 |         """
 31 |         if len(str(req_timestamp)) == 10:
 32 |             req_timestamp = int(req_timestamp)
 33 |             now_timestamp = get_current_timestamp()
 34 |             if req_timestamp <= now_timestamp and req_timestamp + self._timestamp_expiration >= now_timestamp:
 35 |                 return True
 36 |         return False
 37 | 
 38 |     def _check_req_accesskey_id(self, req_accesskey_id):
 39 |         """ 校验accesskey_id
 40 |         @pram req_accesskey_id str: 请求参数中的用户标识id
 41 |         """
 42 |         if req_accesskey_id in [ i['accesskey_id'] for i in self._accessKeys if "accesskey_id" in i ]:
 43 |             return True
 44 |         return False
 45 | 
 46 |     def _get_accesskey_secret(self, accesskey_id):
 47 |         """ 根据accesskey_id获取对应的accesskey_secret
 48 |         @pram accesskey_id str: 用户标识id
 49 |         """
 50 |         return [ i['accesskey_secret'] for i in self._accessKeys if i.get('accesskey_id') == accesskey_id ][0]
 51 | 
 52 |     def _sign(self, parameters):
 53 |         """ MD5签名
 54 |         @param parameters dict: 除signature外请求的所有查询参数(公共参数和私有参数)
 55 |         """
 56 |         if "signature" in parameters:
 57 |             parameters.pop("signature")
 58 |         accesskey_id = parameters["accesskey_id"]
 59 |         sortedParameters = sorted(parameters.items(), key=lambda parameters: parameters[0])
 60 |         canonicalizedQueryString = ''
 61 |         for (k, v) in sortedParameters:
 62 |             canonicalizedQueryString += k + "=" + v + "&"
 63 |         canonicalizedQueryString += self._get_accesskey_secret(accesskey_id)
 64 |         signature = md5(canonicalizedQueryString).upper()
 65 |         return signature
 66 | 
 67 |     def _verification(self, req_params):
 68 |         """ 校验请求是否有效
 69 |         @param req_params dict: 请求的所有查询参数(公共参数和私有参数)
 70 |         """
 71 |         res = dict(msg=None, success=False)
 72 |         try:
 73 |             req_version = req_params["version"]
 74 |             req_timestamp = req_params["timestamp"]
 75 |             req_accesskey_id = req_params["accesskey_id"]
 76 |             req_signature = req_params["signature"]
 77 |         except KeyError,e:
 78 |             res.update(msg="Invalid public params")
 79 |         except Exception,e:
 80 |             res.update(msg="Unknown server error")
 81 |         else:
 82 |             # NO.1 校验版本
 83 |             if req_version == self._version:
 84 |                 # NO.2 校验时间戳
 85 |                 if self._check_req_timestamp(req_timestamp):
 86 |                     # NO.3 校验accesskey_id
 87 |                     if self._check_req_accesskey_id(req_accesskey_id):
 88 |                         # NO.4 校验签名
 89 |                         if req_signature == self._sign(req_params):
 90 |                             res.update(msg="Verification pass", success=True)
 91 |                         else:
 92 |                             res.update(msg="Invalid query string")
 93 |                     else:
 94 |                         res.update(msg="Invalid accesskey_id")
 95 |                 else:
 96 |                     res.update(msg="Invalid timestamp")
 97 |             else:
 98 |                 res.update(msg="Invalid version")
 99 |         return res
100 | 
101 |     def signature_required(self, f):
102 |         @wraps(f)
103 |         def decorated_function(*args, **kwargs):
104 |             params = request.args.to_dict()
105 |             res = self._verification(params)
106 |             if res["success"] is True:
107 |                 return f(*args, **kwargs)
108 |             else:
109 |                 return jsonify(res)
110 |         return decorated_function
111 | 


--------------------------------------------------------------------------------
/server/utils/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # public utils


--------------------------------------------------------------------------------
/server/utils/tool.py:
--------------------------------------------------------------------------------
 1 | # -*- coding: utf-8 -*-
 2 | """
 3 |     utils.tool
 4 |     ~~~~~~~~~~~~~~
 5 | 
 6 |     Common function.
 7 | 
 8 |     :copyright: (c) 2017 by taochengwei.
 9 |     :license: MIT, see LICENSE for more details.
10 | """
11 | 
12 | import hashlib, time, datetime
13 | 
14 | md5 = lambda pwd: hashlib.md5(pwd).hexdigest()
15 | get_current_timestamp = lambda: int(time.mktime(datetime.datetime.now().timetuple()))
16 | 


--------------------------------------------------------------------------------