若是有幾個緣由可讓你愛上flask這個極其靈活的庫,我想藍圖絕對應該算上一個,部署藍圖之後,你會發現整個程序結構很是清晰,模塊之間相互不影響。藍圖對restful api的最明顯效果就是版本控制;而對整個項目來講,總要有後臺管理系統吧,總要有web管理吧,但這些東西不能所有放到view.py。不僅僅是這樣,若是你是一個經驗豐富的程序員,你應該知道,一個程序最好只有一個入口點,從這個入口點進去,全是單向的,就像一棵樹同樣,入口點就在樹根,而後蔓延到樹幹,樹枝。樹枝和樹枝之間最好不要太多交集,也就是咱們一般所說的 低耦合。python
好了,說了這麼多,咱們舉個簡單例子看看。假設你的一款app已經上架,如今公司要開發新的版本,新版本不但要增長新的接口,可能還要修改之前老的接口。但若是修改老的接口,那老版本的用戶又不能正常訪問了,要是用戶更新不及時,是否是這些接口就不能訪問了?若是我須要這個接口能夠維持一陣子,過一段時間後,再停用,該如何實現呢?android
下面這個截圖是我原先的項目結構。ios
若是要增長接口和修改接口,只能在view.py裏面作,我這個只是舉例,在真實的項目中,一箇中等app,最少有上百個接口,如何快速找到接口,定位問題,是一門很好的學問。程序員
首先,先在app下增長一個文件夾,api_1_0做爲版本1.0存放地方,是咱們的一個樹枝,把以前的view.py遷移到裏面去。結構以下:web
咱們以前老是在view.py裏面運行app,這個確定不行,不能把入口點放在某一個樹枝吧。要把整個app拉出來,在上一級來啓動項目,那麼就再增長一個run.py文件,專門來運行整個項目,整個run.py代碼以下redis
# coding:utf-8 from flask import Flask from config import Conf import redis from qiniu import Auth, put_file, etag, urlsafe_base64_encode def create_app(): app = Flask(__name__) app.config.from_object(Conf) app.secret_key = app.config['SECRET_KEY'] app.redis = redis.Redis(host=app.config['REDIS_HOST'], port=app.config['REDIS_PORT'], db=app.config['REDIS_DB'], password=app.config['REDIS_PASSWORD']) app.q = Auth(access_key=app.config['QINIU_ACCESS_KEY'], secret_key=app.config['QINIU_SECRET_KEY']) app.bucket_name = app.config['BUCKET_NAME'] app.debug = app.config['DEBUG'] return app if __name__ == '__main__': app = create_app() app.run(debug=app.debug, host='0.0.0.0', port=5001)
這邊就是整個項目的總起點,是整個樹的根(其實這邊還能夠再把run.py放在外面,這邊接下來會繼續講)。sql
恩,看起來好了,但咱們仍是沒有看到藍圖。好了,這邊就說到了,以前view.py裏面所有用的app.route,以前沒問題,由於它自己就是根,但是你如今已經不是根了,那就要變換一下,在api_1_0/__init__.py裏,添加以下代碼:json
# coding:utf-8 from flask import Blueprint api = Blueprint('api', __name__) from . import view
這邊解釋一下,首先定義一個藍圖,這個不用講了,你用以前,確定要定義一下吧,藍圖名字就叫api。下面一行代碼有點意思,from . import view, 這個代碼,我發現用藍圖的人,不少都忘寫了。若是你是一個python老手,你應該知道,導入python文件,其實就是運行導入的那個文件。在這邊,你導入一次,就是就是告訴上級,這邊還有個view.py文件,裏面覆蓋了一些接口。整個根就知道,哦,若是有接口訪問這,我就指向你這邊。好好理解上面的粗體字。flask
好了,樹枝這邊已經定義了,那樹根那邊也應該從新寫一下。一個request到樹根那,樹根根據url來指向樹枝,樹根怎麼指向呢?代碼以下:api
def create_app(): app = Flask(__name__) app.config.from_object(Conf) app.secret_key = app.config['SECRET_KEY'] app.redis = redis.Redis(host=app.config['REDIS_HOST'], port=app.config['REDIS_PORT'], db=app.config['REDIS_DB'], password=app.config['REDIS_PASSWORD']) app.q = Auth(access_key=app.config['QINIU_ACCESS_KEY'], secret_key=app.config['QINIU_SECRET_KEY']) app.bucket_name = app.config['BUCKET_NAME'] app.debug = app.config['DEBUG'] from .app_1_0 import api as api_1_0_blueprint app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1000') return app
看到增長的2行代碼沒有,這個就是樹根指向樹枝的代碼。
好了,咱們如今整個流程應該清楚了,請求從樹根進來,樹根根據藍圖指向某一個樹枝,這個樹枝具體處理請求,返回。
那天然,最後一段樹枝具體處理,也要改變咯。
原先view.py代碼改變一下,以下:
# coding:utf-8 from flask import Flask, request, jsonify, g, render_template, redirect, url_for, session, current_app from app.model import User, db_session, MaintainRecord import hashlib import time import uuid from app.util import message_validate import random from functools import wraps from . import api def login_check(f): @wraps(f) def decorator(*args, **kwargs): token = request.headers.get('token') if not token: return jsonify({'code': 0, 'message': '須要驗證'}) phone_number = current_app.redis.get('token:%s' % token) if not phone_number or token != current_app.redis.hget('user:%s' % phone_number, 'token'): return jsonify({'code': 2, 'message': '驗證信息錯誤'}) return f(*args, **kwargs) return decorator @api.before_request def before_request(): token = request.headers.get('token') phone_number = current_app.redis.get('token:%s' % token) if phone_number: g.current_user = User.query.filter_by(phone_number=phone_number).first() g.token = token return @api.route('/login', methods=['POST']) def login(): phone_number = request.get_json().get('phone_number') password = request.get_json().get('password') user = User.query.filter_by(phone_number=phone_number).first() if not user: return jsonify({'code': 0, 'message': '沒有此用戶'}) if user.password != password: return jsonify({'code': 0, 'message': '密碼錯誤'}) m = hashlib.md5() m.update(phone_number) m.update(password) m.update(str(int(time.time()))) token = m.hexdigest() pipeline = current_app.redis.pipeline() pipeline.hmset('user:%s' % user.phone_number, {'token': token, 'nickname': user.nickname, 'app_online': 1}) pipeline.set('token:%s' % token, user.phone_number) pipeline.expire('token:%s' % token, 3600*24*30) pipeline.execute() return jsonify({'code': 1, 'message': '成功登陸', 'nickname': user.nickname, 'token': token}) @api.route('/user') @login_check def user(): user = g.current_user nickname = current_app.redis.hget('user:%s' % user.phone_number, 'nickname') return jsonify({'code': 1, 'nickname': nickname, 'phone_number': user.phone_number}) @api.route('/logout') @login_check def logout(): user = g.current_user pipeline = current_app.redis.pipeline() pipeline.delete('token:%s' % g.token) pipeline.hmset('user:%s' % user.phone_number, {'app_online': 0}) pipeline.execute() return jsonify({'code': 1, 'message': '成功註銷'}) @api.route('/get-qiniu-token') def get_qiniu_token(): key = uuid.uuid4() token = current_app.q.upload_token(current_app.bucket_name, key, 3600) return jsonify({'code': 1, 'key': key, 'token': token}) @api.route('/set-head-picture', methods=['POST']) @login_check def set_head_picture(): head_picture = request.get_json().get('head_picture') user = g.current_user user.head_picture = head_picture try: db_session.commit() except Exception as e: print e db_session.rollback() return jsonify({'code': 0, 'message': '未能成功上傳'}) current_app.redis.hset('user:%s' % user.phone_number, 'head_picture', head_picture) return jsonify({'code': 1, 'message': '成功上傳'}) @api.route('/register-step-1', methods=['POST']) def register_step_1(): """ 接受phone_number,發送短信 """ phone_number = request.get_json().get('phone_number') user = User.query.filter_by(phone_number=phone_number).first() if user: return jsonify({'code': 0, 'message': '該用戶已經存在,註冊失敗'}) validate_number = str(random.randint(100000, 1000000)) result, err_message = message_validate(phone_number, validate_number) if not result: return jsonify({'code': 0, 'message': err_message}) pipeline = current_app.redis.pipeline() pipeline.set('validate:%s' % phone_number, validate_number) pipeline.expire('validate:%s' % phone_number, 60) pipeline.execute() return jsonify({'code': 1, 'message': '發送成功'}) @api.route('/register-step-2', methods=['POST']) def register_step_2(): """ 驗證短信接口 """ phone_number = request.get_json().get('phone_number') validate_number = request.get_json().get('validate_number') validate_number_in_redis = current_app.redis.get('validate:%s' % phone_number) if validate_number != validate_number_in_redis: return jsonify({'code': 0, 'message': '驗證沒有經過'}) pipe_line = current_app.redis.pipeline() pipe_line.set('is_validate:%s' % phone_number, '1') pipe_line.expire('is_validate:%s' % phone_number, 120) pipe_line.execute() return jsonify({'code': 1, 'message': '短信驗證經過'}) @api.route('/register-step-3', methods=['POST']) def register_step_3(): """ 密碼提交 """ phone_number = request.get_json().get('phone_number') password = request.get_json().get('password') password_confirm = request.get_json().get('password_confirm') if len(password) < 7 or len(password) > 30: # 這邊能夠本身拓展條件 return jsonify({'code': 0, 'message': '密碼長度不符合要求'}) if password != password_confirm: return jsonify({'code': 0, 'message': '密碼和密碼確認不一致'}) is_validate = current_app.redis.get('is_validate:%s' % phone_number) if is_validate != '1': return jsonify({'code': 0, 'message': '驗證碼沒有經過'}) pipeline = current_app.redis.pipeline() pipeline.hset('register:%s' % phone_number, 'password', password) pipeline.expire('register:%s' % phone_number, 120) pipeline.execute() return jsonify({'code': 1, 'message': '提交密碼成功'}) @api.route('/register-step-4', methods=['POST']) def register_step_4(): """ 基本資料提交 """ phone_number = request.get_json().get('phone_number') nickname = request.get_json().get('nickname') is_validate = current_app.redis.get('is_validate:%s' % phone_number) if is_validate != '1': return jsonify({'code': 0, 'message': '驗證碼沒有經過'}) password = current_app.redis.hget('register:%s' % phone_number, 'password') new_user = User(phone_number=phone_number, password=password, nickname=nickname) db_session.add(new_user) try: db_session.commit() except Exception as e: print e db_session.rollback() return jsonify({'code': 0, 'message': '註冊失敗'}) finally: current_app.redis.delete('is_validate:%s' % phone_number) current_app.redis.delete('register:%s' % phone_number) return jsonify({'code': 1, 'message': '註冊成功'}) @api.teardown_request def handle_teardown_request(exception): db_session.remove()
這邊有好幾個須要注意的點。
第一,運行的代碼取消掉了,由於統一從run.py來運行,做爲入口點。
第二,原先的app.route也所有改爲api.route, api也從本地的__init__.py中導入。由於你如今表明樹枝,不能表明整棵樹了。
第三,app.redis,能夠用current_app.redis來代替,其實就是我在run.py中定義的一些變量,在整顆樹中使用。
好了,咱們運行一下python run.py試試看吧。這邊須要注意的是,url也不同了哦,client.py中,要用不一樣的url了。client.py運行代碼以下:
if __name__ == '__main__': api = APITest('http://127.0.0.1:5001/api/v1000') u = api.login('13565208554', '123456') print u u1 = api.user() print u1 api.logout()
python run.py返回的url,看看詳細狀況。
整個運行狀況問題,那麼回到最上面提到的問題。老闆看咱們第一個版本作的很是穩定,要開發第二個版本,先無論新增的功能,老闆以爲你上一個版本的login接口寫的很差,直接明文把用戶名和密碼傳輸過去了,安全問題得不到保障。如今要求是login接口重寫,其餘不變,而且上個版本的接口暫時還不能中止。
若是按照咱們之前的想法,再增長一個不一樣的接口唄。嗯!方法不錯,要是要修改20個接口呢?重寫嗎?怎麼分得清哪一個對應哪一個版本?就算此次分清了,再更新5個版本,你還知道原來寫的什麼嗎?
好了,上面一堆廢話就是爲了陪襯項目結構的重要性的。版本1.1,咱們從新寫一個樹枝,這個樹枝跟1.0的樹枝沒有任何交集。有了1.0,1.1就很是好寫了。
把整個api_1_0直接複製到api_1_1中。沒錯,你沒看錯,就是整個複製過去。
而後把api_1_1中的__init__.py修改一下,代碼以下:
# coding:utf-8 from flask import Blueprint api = Blueprint('api1_1', __name__) from . import view
其實就是把藍圖名字修改了一下。整個項目結構以下圖:
在run.py中,增長以下代碼。
def create_app(): app = Flask(__name__) app.config.from_object(Conf) app.secret_key = app.config['SECRET_KEY'] app.redis = redis.Redis(host=app.config['REDIS_HOST'], port=app.config['REDIS_PORT'], db=app.config['REDIS_DB'], password=app.config['REDIS_PASSWORD']) app.q = Auth(access_key=app.config['QINIU_ACCESS_KEY'], secret_key=app.config['QINIU_SECRET_KEY']) app.bucket_name = app.config['BUCKET_NAME'] app.debug = app.config['DEBUG'] from app_1_0 import api as api_1_0_blueprint app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1000') from api_1_1 import api as api_1_1_blueprint app.register_blueprint(api_1_1_blueprint, url_prefix='/api/v1100') return app
好了,整個複製過程完工,看看實際狀況吧。
咱們先不修改登陸接口,就看一下效果。client.py運行代碼
if __name__ == '__main__': api = APITest('http://127.0.0.1:5001/api/v1000') u = api.login('13565208554', '123456') print u u1 = api.user() print u1 api.logout() api = APITest('http://127.0.0.1:5001/api/v1100') u = api.login('13565208554', '123456') print u u1 = api.user() print u1 api.logout()
看看ide調試結果吧。
是否是2個版本同時支持了?結構是否是很是很是清晰?
結構調整到這,那就聽老闆的意思,開始寫新的接口吧。老闆說,上次登陸接口不安全,要修改一下。通常來講,驗證時,都是把用戶名,和 密碼+隨機值+時間戳 的加密方式傳過去,咱們也蕭規曹隨吧,新版本的login接口代碼以下:
@api.route('/login', methods=['POST']) def login(): phone_number = request.get_json().get('phone_number') encryption_str = request.get_json().get('encryption_str') random_str = request.get_json().get('random_str') time_stamp = request.get_json().get('time_stamp') user = User.query.filter_by(phone_number=phone_number).first() if not user: return jsonify({'code': 0, 'message': '沒有此用戶'}) password_in_sql = user.password s = hashlib.sha256() s.update(password_in_sql) s.update(random_str) s.update(time_stamp) server_encryption_str = s.hexdigest() if server_encryption_str != encryption_str: return jsonify({'code': 0, 'message': '密碼錯誤'}) m = hashlib.md5() m.update(phone_number) m.update(user.password) m.update(str(int(time.time()))) token = m.hexdigest() pipeline = current_app.redis.pipeline() pipeline.hmset('user:%s' % user.phone_number, {'token': token, 'nickname': user.nickname, 'app_online': 1}) pipeline.set('token:%s' % token, user.phone_number) pipeline.expire('token:%s' % token, 3600*24*30) pipeline.execute() return jsonify({'code': 1, 'message': '成功登陸', 'nickname': user.nickname, 'token': token})
代碼稍微解釋一下,encryption_str就是加密串,是由密碼+隨機值+時間戳用sha256加密的。傳到服務器,服務器也這樣加密一下,而後看看2者是否是一致。傳輸過程不涉及密碼傳輸。就這麼簡單。
客戶端就不能用以前的代碼了,須要重寫一下。代碼以下:
# coding:utf-8 import requests import json from qiniu import put_file import time import random import hashlib class API_1_0(object): base_url = 'http://127.0.0.1:5001/api/v1000' def __init__(self): self.headers = {} self.token = None self.qiniu_token = None self.qiniu_key = None self.qiniu_base_url = 'http://7xk6rc.com1.z0.glb.clouddn.com/' def login(self, phone_number, password, path='/login'): payload = {'phone_number': phone_number, 'password': password} self.headers = {'content-type': 'application/json'} response = requests.post(url=self.base_url + path, data=json.dumps(payload), headers=self.headers) response_data = json.loads(response.content) self.token = response_data.get('token') return response_data def user(self, path='/user'): self.headers = {'token': self.token} response = requests.get(url=self.base_url + path, headers=self.headers) response_data = json.loads(response.content) return response_data def logout(self, path='/logout'): self.headers = {'token': self.token} response = requests.get(url=self.base_url + path, headers=self.headers) response_data = json.loads(response.content) return response_data def get_qiniu_token(self, path='/get-qiniu-token'): response = requests.get(url=self.base_url + path) response_data = json.loads(response.content) self.qiniu_token = response_data.get('token') self.qiniu_key = response_data.get('key') if self.qiniu_token and self.qiniu_key: print '成功獲取qiniu_token和qiniu_key,分別爲%s和%s' % (self.qiniu_token.encode('utf-8'), self.qiniu_key.encode('utf-8')) localfile = '/home/yudahai/PycharmProjects/blog01/app/my-test.png' ret, info = put_file(self.qiniu_token, self.qiniu_key, localfile) print info.status_code if info.status_code == 200: print '上傳成功' self.head_picture = self.qiniu_base_url + self.qiniu_key print '其url爲:' + self.head_picture.encode('utf-8') else: print '上傳失敗' return response_data def set_head_picture(self, path='/set-head-picture'): payload = {'head_picture': self.head_picture} self.headers = {'token': self.token, 'content-type': 'application/json'} response = requests.post(url=self.base_url + path, data=json.dumps(payload), headers=self.headers) response_data = json.loads(response.content) print response_data.get('message') return response_data def register_step_1(self, phone_number, path='/register-step-1'): payload = {'phone_number': phone_number} self.headers = {'content-type': 'application/json'} response = requests.post(url=self.base_url + path, data=json.dumps(payload), headers=self.headers) response_data = json.loads(response.content) print response_data.get('code') return response_data def register_step_2(self, phone_number, validate_number, path='/register-step-2'): payload = {'phone_number': phone_number, 'validate_number': validate_number} self.headers = {'content-type': 'application/json'} response = requests.post(url=self.base_url + path, data=json.dumps(payload), headers=self.headers) response_data = json.loads(response.content) print response_data.get('code') return response_data def register_step_3(self, phone_number, password, password_confirm, path='/register-step-3'): payload = {'phone_number': phone_number, 'password': password, 'password_confirm': password_confirm} self.headers = {'content-type': 'application/json'} response = requests.post(url=self.base_url + path, data=json.dumps(payload), headers=self.headers) response_data = json.loads(response.content) print response_data.get('code') return response_data def register_step_4(self, phone_number, nickname, path='/register-step-4'): payload = {'phone_number': phone_number, 'nickname': nickname} self.headers = {'content-type': 'application/json'} response = requests.post(url=self.base_url + path, data=json.dumps(payload), headers=self.headers) response_data = json.loads(response.content) print response_data.get('code') return response_data class API_1_1(object): base_url = 'http://127.0.0.1:5001/api/v1100' def __init__(self): self.headers = {} self.token = None self.qiniu_token = None self.qiniu_key = None self.qiniu_base_url = 'http://7xk6rc.com1.z0.glb.clouddn.com/' def login(self, phone_number, password, path='/login'): random_str = str(random.randint(10000, 100000)) time_stamp = str(int(time.time())) s = hashlib.sha256() s.update(password) s.update(random_str) s.update(time_stamp) encryption_str = s.hexdigest() payload = {'phone_number': phone_number, 'encryption_str': encryption_str, 'random_str': random_str, 'time_stamp': time_stamp} self.headers = {'content-type': 'application/json'} response = requests.post(url=self.base_url + path, data=json.dumps(payload), headers=self.headers) response_data = json.loads(response.content) self.token = response_data.get('token') return response_data def user(self, path='/user'): self.headers = {'token': self.token} response = requests.get(url=self.base_url + path, headers=self.headers) response_data = json.loads(response.content) return response_data def logout(self, path='/logout'): self.headers = {'token': self.token} response = requests.get(url=self.base_url + path, headers=self.headers) response_data = json.loads(response.content) return response_data def get_qiniu_token(self, path='/get-qiniu-token'): response = requests.get(url=self.base_url + path) response_data = json.loads(response.content) self.qiniu_token = response_data.get('token') self.qiniu_key = response_data.get('key') if self.qiniu_token and self.qiniu_key: print '成功獲取qiniu_token和qiniu_key,分別爲%s和%s' % (self.qiniu_token.encode('utf-8'), self.qiniu_key.encode('utf-8')) localfile = '/home/yudahai/PycharmProjects/blog01/app/my-test.png' ret, info = put_file(self.qiniu_token, self.qiniu_key, localfile) print info.status_code if info.status_code == 200: print '上傳成功' self.head_picture = self.qiniu_base_url + self.qiniu_key print '其url爲:' + self.head_picture.encode('utf-8') else: print '上傳失敗' return response_data def set_head_picture(self, path='/set-head-picture'): payload = {'head_picture': self.head_picture} self.headers = {'token': self.token, 'content-type': 'application/json'} response = requests.post(url=self.base_url + path, data=json.dumps(payload), headers=self.headers) response_data = json.loads(response.content) print response_data.get('message') return response_data def register_step_1(self, phone_number, path='/register-step-1'): payload = {'phone_number': phone_number} self.headers = {'content-type': 'application/json'} response = requests.post(url=self.base_url + path, data=json.dumps(payload), headers=self.headers) response_data = json.loads(response.content) print response_data.get('code') return response_data def register_step_2(self, phone_number, validate_number, path='/register-step-2'): payload = {'phone_number': phone_number, 'validate_number': validate_number} self.headers = {'content-type': 'application/json'} response = requests.post(url=self.base_url + path, data=json.dumps(payload), headers=self.headers) response_data = json.loads(response.content) print response_data.get('code') return response_data def register_step_3(self, phone_number, password, password_confirm, path='/register-step-3'): payload = {'phone_number': phone_number, 'password': password, 'password_confirm': password_confirm} self.headers = {'content-type': 'application/json'} response = requests.post(url=self.base_url + path, data=json.dumps(payload), headers=self.headers) response_data = json.loads(response.content) print response_data.get('code') return response_data def register_step_4(self, phone_number, nickname, path='/register-step-4'): payload = {'phone_number': phone_number, 'nickname': nickname} self.headers = {'content-type': 'application/json'} response = requests.post(url=self.base_url + path, data=json.dumps(payload), headers=self.headers) response_data = json.loads(response.content) print response_data.get('code') return response_data if __name__ == '__main__': api = API_1_0() u = api.login('13565208554', '123456') print u u1 = api.user() print u1 api.logout() api = API_1_1() u = api.login('13565208554', '123456') print u u1 = api.user() print u1 api.logout()
就是複製一遍,而後把base_url定義成不一樣,僅此而已。其實真是的ios, android也這樣。當須要更新版本,他們複製以前的項目,改變一下base_url,而後修改須要修改的接口。這樣2個版本同時支持的服務器端就寫好了,是否是超級直觀?直接運行一下吧。
{u'message': u'\u6210\u529f\u767b\u5f55', u'code': 1, u'nickname': u'\u6d4b\u8bd5\u7528\u62371', u'token': u'e5a74170d81feb106a066f18f9f2e966'} {u'phone_number': u'13565208554', u'code': 1, u'nickname': u'\u6d4b\u8bd5\u7528\u62371'} {u'message': u'\u6210\u529f\u767b\u5f55', u'code': 1, u'nickname': u'\u6d4b\u8bd5\u7528\u62371', u'token': u'e5a74170d81feb106a066f18f9f2e966'} {u'phone_number': u'13565208554', u'code': 1, u'nickname': u'\u6d4b\u8bd5\u7528\u62371'}
返回的code都是1,兩種方式登陸都支持。老闆要求都達到了,老闆高興,發獎金,咱們也高興(好吧,不作夢了)。
好了,講到這,你們應該對整個項目接口有很是清晰的瞭解了,之後再加接口或者更新版本,對你來講,都是否是問題了吧。