├── .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 | 
15 |
16 | ##### JavaScript(HTML)
17 |
18 | 
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 |
--------------------------------------------------------------------------------