Flask设计带token认证的API
- RESTful风格的API
REST已经成为web services和APIs的标准架构,很多APP的架构基本上是使用RESTful的形式了。
REST的一个特性是无状态的,没有session cookies,如果访问需要验证的接口,客户端请求必需每次都发送用户名和密码。通常在实际app应用中,并不会每次都将用户名和密码发送。所以我们使用token作为认证用户的身份信息。
- 为什么使用Token验证:
在Web领域基于Token的身份验证随处可见。在大多数使用Web API的互联网公司中,tokens 是多用户下处理认证的最佳方式。
以下几点特性会让你在程序中使用基于Token的身份验证
- 无状态、可扩展
- 支持移动设备
- 跨程序调用
- 安全
创建用户数据库
使用Flask-SQLAlchemy (ORM)的模块去管理用户数据库。
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key = True)
username = db.Column(db.String(32), index = True)
password_hash = db.Column(db.String(128))
因为安全的原因,明文密码不可以直接存储,必需经过hash后方可存入数据库。如果数据库被脱了,也是比较难破解的。密码永远不要明文存在数据库中。
Password Hashing
在User类里面使用PassLib库对密码进行hash,添加加密跟验证加密后的密码方法。
from passlib.apps import custom_app_context as pwd_context
class User(db.Model):
# ...
def hash_password(self, password):
self.password_hash = pwd_context.encrypt(password)
def verify_password(self, password):
return pwd_context.verify(password, self.password_hash)
hash算法是单向的,意味着它只能hash密码,但是无法还原密码。
用户注册
客户端发送表单信息,带用户名跟密码,验证用户名跟密码是否合法,是否已被注册。
@app.route('/users/sign_up', methods = ['POST'])
def new_user():
username = request.json.get('username')
password = request.json.get('password')
if username is None or password is None:
abort(400) # missing arguments
if User.query.filter_by(username = username).first() is not None:
abort(400) # existing user
user = User(username = username)
user.hash_password(password)
db.session.add(user)
db.session.commit()
return jsonify({ 'username': user.username }), 201, {'Location': url_for('get_user', id = user.id, _external = True)}
用户登录
用户登录,验证用户名以及密码。
@user_login.route('/login', methods=['GET', 'POST'])
def sign():
error = None
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if verify_password(username, password):
# 号码以及密码验证通过
pass
else:
# 手机号或者密码错误
return jsonify({'status_code':'401','error_message':'Unauthorized'})
token = g.user.generate_auth_token(6000)
status_code = "201"
user_data = {
'status_code': status_code,
'token': token,
'duration': 6000,
"user": {
"id": g.user.id,
"phone": g.user.phone,
"nickname": g.user.nickname,
"avar": '/static/images/user_img/test_user_1.png',
"message": '这个人很懒什么都没留下',
"orderList": []
}
}
json_user_data = jsonify(user_data)
return json_user_data
else:
return jsonify({'status_code': '400', 'error_message': 'INVALID REQUEST'})
在这里我们使用flask的上下文请求对象g。处理请求时用作临时存储的对象,每次请求都会重设这个变量。
@auth.verify_password
def verify_password(username_or_token, password):
# first try to authenticate by token
user = User.verify_auth_token(username_or_token)
if not user:
# try to authenticate with username/password
user = User.query.filter_by(phone=username_or_token).first()
if not user or not user.verify_password(password):
return False
g.user = user
return True
基于Token的认证
因为需要每次请求都要发送用户名和密码,客户端需要把验证信息存储起来进行发送,这样十分不方便,就算在HTTPS下的传输,也是有风险存在的。
比前面的密码验证方法更好的是使用Token认证请求。
原理是第一次客户端与服务器交换过认证信息后得到一个认证token,后面的请求就使用这个token进行请求。
Token通常会给一个过期的时间,当超过这个时间后,就会变成无效,需要产生一个新的token。这样就算token泄漏了,危害也只是在有效的时间内。
生成token和验证token的方法可以附加到User model上实现:
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
class User(db.Model):
# ...
def generate_auth_token(self, expiration = 600):
s = Serializer(app.config['SECRET_KEY'], expires_in = expiration)
return s.dumps({ 'id': self.id })
@staticmethod
def verify_auth_token(token):
s = Serializer(app.config['SECRET_KEY'])
try:
data = s.loads(token)
except SignatureExpired:
return None # valid token, but expired
except BadSignature:
return None # invalid token
user = User.query.get(data['id'])
return user
在generate_auth_token()函数中,token其实就是一个加密过的字典,里面包含了用户的id和默认为10分钟(600秒)的过期时间。
verify_auth_token()的实现是一个静态方法,因为token只是一次解码检索里面的用户id。获取用户id后就可以在数据库中取得用户资料了。
试试使用一个新的接入点,让客户端请求一个token:
@auth.login_required
def get_auth_token():
token = g.user.generate_auth_token()
return jsonify({ 'token': token.decode('ascii') })
给客户端返回一个json,客户端选择在header里面给服务端发送token,服务端获取header的token,使用verify_password
方法验证。
token = request.headers['accesstoken']
user = User.verify_auth_token(token)
if not user:
return jsonify({'status_code': '401', 'error_message': 'Unauthorized'})
一个简单的例子的github
如果想了解flask项目的结构的话参考这篇