登陆注册
说明:
令牌Token认证,在对HTTP形式的API发请求时,大部分情况我们不是通过用户名密码做验证,而是通过一个令牌[Token来做验证]。
RESTful API无法使用Flask-Login扩展来实现用户认证。因为其没有客户端,通过postman请求,无法设置cookie和session
RESTful API不保存状态,无法依赖Cookie及Session来保存用户信息。需要使用Flask-HTTPAuth扩展,完成RESTful API的用户认证工作
Flask-HTTPAuth提供了几种不同的Auth方法,比如:HTTPBasicAuth、HTTPTokenAuth、MultiAuth、HTTPDigestAuth。
此时,我们就要使用Flask-HTTPAuth扩展中的HTTPTokenAuth对象中提供
”login_required”装饰器来认证视图函数、”error_handler”装饰器来处理错误、”verify_token”装饰器来验证令牌。
认证:
两种方式:一种:包含加密过的用户数据 ;二种:不包含加密的用户数据
1、使用flask的session,但没有使用flask-session扩展,session数据无法存储在服务器上,
所以session的数据会被加密后保存到浏览器的cookies中。
此种方式相当于 token 中包含加密过的用户数据 的情况。
session['uid'] = 123
{'uid': 123}
sadufijklf260398riowehqklasdfnlk;d8rif2309wqeopsk
2、使用flask-session在服务器上存储session数据,session数据不需要存储在客户端中,
但是需要在浏览器的cookies 中存储一个session_id 的值,来标记当前请求对用的session是谁。
session_id = 875456786sdagadh
此种方式相当于 token 中不包含用户数据
流程:
1、用户登录,登录成功后,服务器为此用户生成一个 token(包含代表用户身份信息的加密字符串)
{'uid': 123} 加密成 asdfjpoqwiefojklsd09823uiowejnfy8ij239-0eopifhdv789uiohjrefd8u9ioj
此加密字符串中也包含 时间信息,用于 token 的过期验证
2、在登录请求的响应中,向客户端返回 上一步 生成的 token
3、客户端再次请求服务器时,会在 请求头 中携带 token
4、服务器从请求头中获取 token,进行解密工作,如果解密失败:
1、不是服务器加密的数据,会解密失败
2、超过了有效期
5、如果解密成功,则从 token 数据中获得用户的身份信息,如:uid,可以通过 uid 获得当前登录用户的用户对象
加密解密:
itsdangerous库提供了对信息加签名(Signature)的功能,我们可以通过它来生成并验证令牌。Flask 默认已经安装。
1 from flask import Flask, g 2 from flask_httpauth import HTTPTokenAuth 3 from itsdangerous import TimedJSONWebSignatureSerializer as Serializer 4 from itsdangerous import BadSignature, SignatureExpired 5 6 auth = HTTPTokenAuth() 7 8 app = Flask(__name__) 9 app.config['SECRET_KEY'] = '110' 10 # 实例化了一个针对JSON的签名序列化对象token_serializer。它是有时效性的,60分钟后序列化后的签名即会失效,就无法解密 11 token_serializer = Serializer(app.config['SECRET_KEY'], expires_in=3600) # 参数:key ; 过期时间(秒) 12 13 users = ['jack', 'tom'] 14 for user in users: 15 # .dump(加密信息)对用户信息进行加密,然后生成token;.loads(要解密的加密值)对数据解密 16 token = token_serializer.dumps({'username': user}).decode('utf-8') 17 print('*** token for {}: {} '.format(user, token))
安装:
- pip install flask-httpauth
-
为了简化,我们将Token与用户的关系保存在一个字典中:
使用:
1 import datetime 2 from flask import current_app 3 from flask_sqlalchemy import SQLAlchemy 4 from itsdangerous import TimedJSONWebSignatureSerializer as Serializer 5 6 db = SQLAlchemy() 7 8 # 频道 1 9 class Channel(db.Model): 10 __tablename__ = "channels" 11 12 id = db.Column(db.Integer, primary_key=True) 13 name = db.Column(db.String(16), unique=True, nullable=False) 14 sort = db.Column(db.Integer, nullable=False) 15 articles = db.relationship('Article', backref='channel', lazy='dynamic') 16 17 # 文章 N 18 class Article(db.Model): 19 __tablename__ = "articles" 20 21 id = db.Column(db.Integer, primary_key=True) 22 created_at = db.Column(db.DateTime, default=datetime.datetime.now()) 23 updated_at = db.Column(db.DateTime, default=datetime.datetime.now(), onupdate=datetime.datetime.now()) 24 title = db.Column(db.String(256), nullable=False) 25 content = db.Column(db.String(5000), nullable=False) 26 27 channel_id = db.Column(db.Integer, db.ForeignKey("channels.id")) 28 author_id = db.Column(db.Integer, db.ForeignKey('users.id')) 29 30 # 用户 1 31 class User(db.Model): 32 __tablename__ = 'users' 33 34 id = db.Column(db.Integer, primary_key=True) 35 username = db.Column(db.String(32), unique=True, nullable=False) 36 password = db.Column(db.String(256), nullable=False) 37 articles = db.relationship('Article', backref='author', lazy='dynamic') 38 39 # 通过类方法,生成一个加密数据赋值给token变量,进行加密。 40 def generate_auth_token(self, expires_in=3600): 41 # Serializer(key验证值密钥,过期时间) 设置key密钥和expires_in过期时间实例化Serializer。 42 serializer = Serializer(current_app.config['SECRET_KEY'], expires_in=expires_in) 43 # 通过Serializer实例化的serializer对象 把当前用户的id用序列化器进行了加密,格式为urf-8。 44 token = serializer.dumps({'user_id': self.id}).decode('utf-8') 45 return token 46 47 @classmethod 48 def check_auth_token(cls, token): 49 serializer = Serializer(current_app.config['SECRET_KEY']) 50 try: 51 data = serializer.loads(token) # 从token中,解密数据 52 except: 53 return None 54 if 'user_id' in data: 55 return cls.query.get(data['user_id']) # User.query.get() 56 else: 57 return None
1 # 在 __init__.py 文件中 2 3 from flask import Flask 4 from flask_restful import Api 5 from app import views, ext 6 from app.apis import ChannelList, ChannelDetail, ArticleList, ArticleDetail, UserRegister, UserLogin 7 from app.views import restful_bp 8 9 10 def create_app(): 11 app = Flask(__name__) 12 # session数据加密:from itsdangerous import TimedJSONWebSignatureSerializer 13 # TimedJSONWebSignatureSerializer 数据加密 14 # generate_password_hash,check_password_hash都会依赖app中的secret_key 15 app.config['SECRET_KEY'] = '110' 16 17 ext.init_db(app) 18 ext.init_migrate(app) 19 20 # 注册实例化api扩展。prefix前缀名 21 api = Api(app, prefix='/api') 22 # 注册频道路由 23 api.add_resource(ChannelList, '/ChannelLists', endpoint='ChannelLists') 24 api.add_resource(ChannelDetail, '/ChannelDetails/<int:id>', endpoint='ChannelDetails') 25 # 注册文章路由 26 api.add_resource(ArticleList, '/Articles', endpoint='Articles') 27 api.add_resource(ArticleDetail, '/ArticleDetails/<int:id>', endpoint='ArticleDetails') 28 # 用户注册登陆 29 api.add_resource(UserRegister, '/auth/register', endpoint='user_register') 30 api.add_resource(UserLogin, '/auth/login', endpoint='user_login') 31 32 # 蓝图 33 app.register_blueprint(blueprint=restful_bp) 34 return app
1 import os 2 from flask_httpauth import HTTPTokenAuth 3 from flask_migrate import Migrate 4 from app.models import db 5 6 migrate = Migrate() 7 auth = HTTPTokenAuth() 8 9 def init_db(app): 10 app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(app.root_path, 'sqlite3.db') 11 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 12 db.init_app(app=app) 13 14 def init_migrate(app): 15 migrate.init_app(app=app,db=db)
1 import datetime 2 3 from flask import g 4 from flask_restful import Resource, reqparse, fields, marshal_with, abort 5 from requests import auth 6 from werkzeug.security import generate_password_hash, check_password_hash 7 from app.models import Channel, db, Article, User 8 9 # 输入格式化验证 10 user_parser = reqparse.RequestParser() 11 user_parser.add_argument('username', required=True, type=str) 12 user_parser.add_argument('password', required=True, type=str) 13 # 输出格式化设置 14 user_fields = { 15 'id': fields.Integer, 16 'username': fields.String 17 } 18 19 # 用户注册 20 class UserRegister(Resource): 21 @marshal_with(fields=user_fields) 22 def post(self): 23 args = user_parser.parse_args() 24 # 需要自定义验证,验证用户名是否重复。未写 25 user = User() 26 user.username = args.get('username') 27 user.password = generate_password_hash(args['password']) 28 29 db.session.add(user) 30 db.session.commit() 31 return user, 201 32
33 # 用户登陆 34 class UserLogin(Resource): 35 def post(self): 36 args = user_parser.parse_args() 37 user = User.query.filter_by(username=args['username']).first() 38 if user is None or not check_password_hash(user.password, args['password']): 39 return {'msg': '用户名密码错误'}, 400 40 token = user.generate_auth_token() # 登陆成功为该用户生成一个token认证值,返回给客户端。 41 return {'token': token} # 返回给客户端保存 42 # token验证。装饰器验证成功则True;否则False 43 @auth.verify_token 44 def verify_token(token): 46 # 验证token的回调函数。如果验证成功则使用token中的用户身份信息(user_id)从数据库中查询当前登录用户数据,返回uesr对象;如果验证不成功,则返回None 51 user = User.check_auth_token(token) 52 if user is None: 53 return False 54 # 验证成功后,将当前登录用户的对象设置到g对象中,供后续使用 55 g.user = user 56 return True
@auth.verify_token和@auth.login_required 帮助我们对生成返回给客户端的token进行沿验证。
auth = HTTPTokenAuth()扩展初始化实例时不需要传入app对象,也不需要调用auth.init_app(app)注入应用对象
保护:
@auth.login_required对频道模块的get请求进行保护
1 # 频道模块。get、put、patch、delete 2 class ChannelDetail(Resource): 3 """ 4 GET /channels/123 5 PUT /channels/234 6 PATCH /channels/123 7 DELETE /channels/123 8 """ 9 def get_object(self,id): 10 channel = Channel.query.get(id) 11 if channel is None: 12 return abort(404,message="找不到对象") 13 return channel 14 15 @auth.login_required # 对该函数的请求进行保护,token用户验证。如果该用户的token值错误或者过期,该用户将无法访问此函数 16 @marshal_with(fields=channel_article_fields) 17 def get(self,id): 18 channel = self.get_object(id) 19 return channel,200
1 import datetime 2 from flask import g 3 from flask_restful import Resource, reqparse, fields, marshal_with, abort, marshal 4 from werkzeug.security import generate_password_hash, check_password_hash 5 6 from app.ext import auth 7 from app.models import Channel, db, Article, User 8 9 # ============================ N =================================== 10 # 自定义一个类,用于时间格式化 11 class MyDTFmt(fields.Raw): 12 def format(self, value): 13 return datetime.datetime.strftime(value, '%Y-%m-%d %H:%M:%S') 14 15 16 # 定义参数验证格式 17 article_parser = reqparse.RequestParser() 18 article_parser.add_argument('title', required=True, type=str, help="标题必填") 19 article_parser.add_argument('content', required=True, type=str, help="正文必填") 20 article_parser.add_argument('channel_id', required=True, type=int, help="频道必填") 21 22 # 定义返回输出格式 23 article_fields = { 24 'id': fields.Integer, 25 'url': fields.Url(endpoint='ArticleDetails', absolute=True), 26 'title': fields.String, 27 'content': fields.String, 28 # 等同于:'channel':fields.Nested(channel_fields), 29 'channel': fields.Nested({ # 通过Nested将对象解开 30 'name': fields.String, 31 'url': fields.Url(endpoint="ChannelDetails", absolute=True), 32 "sort": fields.Integer, 33 }), 34 "author": fields.Nested({ 35 'id': fields.Integer, 36 'name': fields.String(attribute='username'), 37 }), 38 'created_at': MyDTFmt, # 进行自定义时间格式化 39 'updated_at': fields.DateTime(dt_format="iso8601") 40 } 41 42 43 # 文章模块。get、post 44 class ArticleList(Resource): 45 46 @auth.login_required 47 @marshal_with(fields=article_fields) 48 def get(self): 49 articles = Article.query.all() 50 return articles, 200 51 52 @auth.login_required 53 @marshal_with(fields=article_fields) 54 def post(self): 55 args = article_parser.parse_args() 56 57 article = Article() 58 article.title = args.get('title') 59 article.content = args.get('content') 60 article.channel_id = args.get('channel_id') 61 article.author_id = g.user.id # 登陆状态下设置文章作者 62 63 db.session.add(article) 64 db.session.commit() 65 return article, 201
验证:
通过postman请求登陆时返回的此用户的token值,在postman中设置上此用户的token值请求登陆访问频道列表
补充:
marshal的使用。api.py
1 import datetime 2 3 from flask import g 4 from flask_restful import Resource, reqparse, fields, marshal_with, abort, marshal 5 from werkzeug.security import generate_password_hash, check_password_hash 6 7 from app.ext import auth 8 from app.models import Channel, db, Article, User 9 10 # 输出格式化设置 11 user_fields = { 12 'id': fields.Integer, 13 'username': fields.String 14 } 15 16 # 用户登陆 17 class UserLogin(Resource): 18 def post(self): 19 args = user_parser.parse_args() 20 user = User.query.filter_by(username=args['username']).first() 21 if user is None or not check_password_hash(user.password, args['password']): 22 return {'msg': '用户名密码错误'}, 400 23 token = user.generate_auth_token() # 登陆成功生成一个token为该用户 24 # return {'token': token} # 返回给客户端保存 25 26 ret = { 27 'code': 0, 28 'msg': '', 29 'data': { 30 # marshal相当于'user':'user.to_dict()' 将对象user与user_fileds中的同名参数进行匹配, 31 # marshal将user对象通过user_fields转化为字典形式传递出去 32 'user': marshal(user, user_fields), 33 'token': token, 34 } 35 } 36 return ret
文件上传
原生实现:在实际生产中上传大型文件,一般采用分段上传
- 上传表单
<form action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="file"> <input type="submit" value="Upload"> </form> {# 设置表单类型为enctype="multipart/form-data" 上传文件才可以生效#}
- 视图函数
通过request对象上的files"file = request.files['file']"获取文件;使用 save() 方法保存文件到指定位置
1 import os 2 from flask import Flask, request, render_template, send_from_directory, url_for 3 from werkzeug.utils import secure_filename 4 5 app = Flask(__name__) 6 # 设置上传存放文件的文件夹。uploads是此项目下的手动创建的目录。注意:手动创建 7 app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'uploads') 8 # 设置最大上传文件的大小值。10 M;一定要设置此值,不设置可能存在漏洞 9 app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 10 11 # 判断文件的类型。原理:对文件名右边以点切割,获取脚标为1的参数,即是扩展名 12 def allowed_file(filename): 13 return '.' in filename and 14 filename.rsplit('.', 1)[1] in {'png', 'jpg', 'jpeg', 'gif'} 15 16 @app.route('/uploads/<filename>') 17 def uploaded_file(filename): 18 """ 19 send_from_directory,从指定的目录中(第一个参数)查找一个指定名字的文件(第二个参数) 20 如果找到,则将文件读入内存,并且最为响应返回给浏览器。只会在开发环境中使用 21 """ 22 return send_from_directory(app.config['UPLOAD_FOLDER'],filename) 23 24 @app.route('/upload-test/', methods=['POST', 'GET']) 25 def upload_test(): 26 if request.method == 'POST': 27 # 从表单中获得名为photo的上传文件对象,通过request.files.get获取。 28 # files类型是字典,一个表单中可以有多个上传文件,可以通过遍历获取多个上传文件 29 file = request.files.get('photo') 30 31 if file: 32 # 首先判断文件的 扩展名 是否被允许 33 if allowed_file(file.filename): 34 # 将文件名做一个安全处理:'my movie.mp4' -> 'my_movie.mp4' 35 filename = secure_filename(file.filename) 36 # 将文件保存到指定目录(第一个参数),以某个文件名(第二个参数) 37 # 第二个参数是保存文件的文件名,可以自定义修改。filename = 'abc.def' 38 file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) 39 # 通过url_for和uploaded_file视图函数,反向解析出文件的访问地址。供前端访问 40 file_url = url_for('uploaded_file', filename=filename) 41 return render_template('upload.html', photo_url=file_url) 42 else: 43 return render_template('upload.html', err='不支持的文件类型') 44 return render_template('upload.html') 45 46 47 if __name__ == '__main__': 48 app.run()
结合flask-wtf实现:
- 表单验证 form
1 from flask_wtf import Form 2 from flask_wtf.file import FileField, FileAllowed, FileRequired 3 4 class PhotoForm(Form): 5 photo = FileField('photo', validators=[ 6 FileRequired(), # 不能为空 7 # 验证文件格式 8 FileAllowed(['jpg', 'png', 'webp'], '只能上传图片') 9 ])
- 视图
form.photo.data.filename和form.photo.data.save进行获取和保存
1 import os 2 from flask import Flask, render_template, url_for 3 from werkzeug.utils import secure_filename 4 from forms import PhotoForm 5 6 app = Flask(__name__) 7 # flask-wtf中生成csrf token必须依赖SECRET_KEY 8 app.config['SECRET_KEY'] = '110' 9 # 设置上传文件的文件夹,在static中 10 app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'static/uploads') 11 12 13 @app.route('/upload/', methods=('GET', 'POST')) 14 def upload(): 15 form = PhotoForm() 16 file_url = '' 17 18 if form.validate_on_submit(): 19 # 获取photo中的数据 20 filename = secure_filename(form.photo.data.filename) 21 form.photo.data.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) 22 file_url = url_for('static', filename='uploads/' + filename) 23 return render_template('upload.html', form=form, file_url=file_url) 24 25 26 if __name__ == '__main__': 27 app.run()
- 模板
1 <body> 2 {% if form.errors %} 3 <div> 4 {{ form.errors }} 5 </div> 6 {% endif %} 7 8 <form action="" method="post" enctype="multipart/form-data"> 9 {{ form.csrf_token() }} 10 <input type="file" name="photo"> 11 <input type="submit" value="Upload"> 12 </form> 13 14 <div> 15 photo: <img src="{{ file_url }}" alt=""> 16 </div> 17 </body>
flask-uploads实现:
安装:
- pip install flask-uploads
- 视图
1 import os 2 from flask import Flask, render_template 3 from flask_uploads import UploadSet, configure_uploads, IMAGES, patch_request_class 4 from flask_wtf import FlaskForm 5 from flask_wtf.file import FileField, FileRequired, FileAllowed 6 7 app = Flask(__name__) 8 app.config['SECRET_KEY'] = '110' 9 10 # 为 flask_uploads 创建一个上传目录 11 app.config['UPLOADED_PHOTOS_DEST'] = os.path.join(app.root_path, 'uploads') 12 # 设置上传文件类型集合。可以将IMAGES[第二个参数]文件中的文件类型上传到名字叫photos[第一个参数]的文件中 13 photos = UploadSet('photos', IMAGES) 14 configure_uploads(app, photos) 15 # 设置最大上传大小, 默认 64 MB 16 patch_request_class(app) 17 18 class UploadForm(FlaskForm): 19 photo = FileField(validators=[ 20 FileAllowed(photos, '只能上传图片!'), 21 FileRequired('文件未选择')]) 22 23 @app.route('/upload/', methods=['GET', 'POST']) 24 def upload_file(): 25 form = UploadForm() 26 if form.validate_on_submit(): 27 # 将 文件(form.photo.data)保存到 photos 上传集合中 28 filename = photos.save(form.photo.data) 29 # 获得文件的 url 30 file_url = photos.url(filename) 31 # 实际项目中,会把文件的 url 保存到数据库中 32 else: 33 file_url = None 34 return render_template('upload.html', form=form, file_url=file_url) 35 36 37 if __name__ == '__main__': 38 app.run()
- 模版
1 <body> 2 <form method="post" enctype="multipart/form-data"> 3 {{ form.csrf_token() }} 4 {{ form.photo }} 5 {% for error in form.photo.errors %} 6 <p>{{ error }}</p> 7 {% endfor %} 8 <input type="submit" value="upload"> 9 </form> 10 11 {% if file_url %} 12 photo:<img src="{{ file_url }}"> 13 {% endif %} 14 </body>
前后端的分离实现:
目前企业中,大多数上传文件的业务会把文件存储在 云存储 中,每个项目会向云存储申请上传的 secret_key,通过 secret_key 和 云存储上传api,将用户的文件上传到云存储中,并返回文件url
- 前端向后端发起申请上传的请求
- 后端根据云存储的 secret_key 及 云存储要求的特定算法(一会会由云存储的sdk提供)计算一个包含有效期的上传令牌(token),并将此 token 返回给前端
- 前端拿到上传 token 后,从本地直接将文件上传到云存储服务器(上传时会一起将 token 提交),上传成功后,云存储返回 url
- 前端将上传后的文件url及表单中的其他信息一起提交给后端进行保存
上线部署
介绍:
WSGI:W web、S server、G gateway、I interface。俗称:web容器,web服务器
Nginx:也是一个服务器软件,反向代理、负载均衡;Nginx 背后可以有多个 WSGI 服务器
Nginx 是不无论什么编程语言都可以使用的代理服务器软件。性能强悍、用户多、漏洞少、稳定性强。
它接收用户的请求后,会转发给真正的运行python代码的服务器WSGI容器,WSGI容器内部会去执行python代码[python代码放在WSGI容器中才可以运行起来]
过程:
用户访问时通过Nginx,Nginx会负载均衡、反向代理到WSGI容器上。python的代码是不可以直接与Ngins交互的,Nginx是不知道如何执行python代码的,
所以需要python代码是在WSGI SERVER容器中运行的,通过WSGI运行就可以提供http服务;执行完后会返回app响应,相应会交给Nginx。最终返回给客户端
WSGI, 是一个协议,pep 333/3333 规范的协议,规定了服务器如何与应用程序交互web容器,执行我们的应用程序(比如:flask + 业务代码)
uWSGI,是一个 服务器软件,实现了 WSGI 协议,可以运行标准的 WSGI 应用程序(Flask 应用)。同时也有自己的特有协议:uwsgi
uwsgi, 是 uWSGI 特有的协议
/ WSGI SERVER 1 <======> Python(Flask app) client <-----> Nginx - WSGI SERVER 2 <======> Python(Flask app) WSGI SERVER 3 <======> Python(Flask app)
WSGI 就像油箱容器,我们的程序就像汽油。把程序放到这个容器中,就可以启动了
安装:
配置:
部署: