背景
项目通过Flask构建的服务端需要增加用户认证功能,而服务端除了为前端页面提供接口以外,还需要开放API接口供其他系统调用,同样的新增的用户认证功能也要同时适配这两种应用场景。
需求分析
- 前端页面为了保证访问页面的流畅以及浏览器兼容性,最好是使用token或session做用户认证,因为前端有跨域需求,调用cookie比较麻烦,因此这里直接使用token。
- 而在API接口调用的需求中,使用basic认证会比较方便,只需要在请求头附带账号密码即可,如果使用token验证方式使用API接口的话,则需要先从认证端口获取一个token,再在后续的api请求中附带这个token,对接方面会增加复杂度。
- 从flask_httpauth库中可以看到flask封装好的几种认证方式:Basic认证、Digest认证、Token认证等,通过继承对应的这几个类即可实现这几种认证方式,但由于不同认证方式是通过scheme这个参数配置的,因此不存在能同时适配多种认证方式的封装,需要自行开发;
代码
以下是基于HTTPAuth类进行的封装,通过重写authenticate、get_auth这两个类,在收到认证请求时根据请求头(Basic Auth认证对应请求头为Basic,Token认证对应请求头为Bearer)选择对应的解析以及验证方式,从而实现了对两种认证请求的同时支持。
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, BadSignature, SignatureExpired
from flask import make_response, jsonify
from flask import make_response, jsonify, session, request
from flask_httpauth import HTTPAuth
from werkzeug.datastructures import Authorization
from base64 import b64decode,b64encode
class AuthBase(HTTPAuth):
def __init__(self, *args,**kwargs):
super().__init__(*args,**kwargs)
self.verify_password_callback = None
self.verify_token_callback = None
self.auth_verify()
def verify_password(self, f):
self.verify_password_callback = f
return f
def verify_token(self, f):
self.verify_token_callback = f
return f
def authenticate(self, auth, stored_password):
if auth:
if auth.type.upper() == 'BASIC' :
if auth:
username = auth.username
client_password = auth.password
else:
username = ""
client_password = ""
if self.verify_password_callback:
return self.ensure_sync(self.verify_password_callback)(
username, client_password)
elif auth.type.upper() == 'BEARER' :
token = auth['token']
if self.verify_token_callback:
return self.ensure_sync(self.verify_token_callback)(token)
else:
token = ""
if self.verify_token_callback:
return self.ensure_sync(self.verify_token_callback)(token)
def get_auth(self):
auth = None
if self.header is None or self.header == 'Authorization':
auth = request.authorization
if auth is None and 'Authorization' in request.headers:
try:
header = request.headers['Authorization'].encode('utf-8')
auth_type = header.split(b' ', 1)[0].decode('utf-8')
if auth_type.upper() == 'BASIC' :
credentials = header.split(b' ', 1)[1]
encoded_username, encoded_password = b64decode(credentials).split(b':', 1)
try:
username = encoded_username.decode('utf-8')
password = encoded_password.decode('utf-8')
except UnicodeDecodeError:
# try to decode again with latin-1, which should always work
username = encoded_username.decode('latin1')
password = encoded_password.decode('latin1')
auth = Authorization(auth_type, {'username': username, 'password': password})
elif auth_type.upper() == 'BEARER' :
token = header.split(b' ', 1)[1]
auth = Authorization(auth_type, {'token': token})
except (ValueError, KeyError):
pass
elif self.header in request.headers:
auth = Authorization(self.scheme,{'token': request.headers[self.header]})
if auth is not None and auth.type.upper() not in ['BASIC','BEARER']:
auth = None
return auth
使用方式上和flask原生认证类一样,通过被@verify_token、@verify_password装饰器装饰的函数实现Basic、Token认证逻辑,@login_required装饰器实现认证保护。
以下是一个参考示例:
class AuthToken(AuthBase):
def __init__(self, *args,**kwargs):
super().__init__(*args,**kwargs)
self.auth_token()
def auth_token(self):
@self.verify_password
def __verify_password(account,passwd):
return_value = None
if account == "":
return None
else :
FlaskMsg(f">>> BASIC验证: 用户[{account}]")
try :
pwd = db.query_pwd(account)
if passwd == pwd :
FlaskMsg(f">>> BASIC验证: 用户[{account}]验证通过")
return_value = account
except Exception as e:
FlaskMsg(f">>> BASIC验证失败\n异常信息:{e}")
return_value = None
finally:
return return_value
@self.verify_token
def verify_token(token):
form_data = unpack_auth_token(token)
if form_data == False :
return False
elif 'account' in form_data:
return True
else :
return False
def generate_auth_token(form_data):
s = Serializer(SECRET_KEY, expires_in=TOKEN_EXPIRATION)
return s.dumps(form_data)
def unpack_auth_token(token):
s = Serializer(SECRET_KEY, expires_in=TOKEN_EXPIRATION)
try:
form_data = s.loads(token)
except SignatureExpired:
FlaskMsg(f">>> Token已过期,当前Token刷新时间[{TOKEN_EXPIRATION}]s")
return False
except BadSignature:
FlaskMsg(f">>> Token校验不通过,Token: [{token}]")
return False
return form_data
验证
- Basic Auth认证:Postman在Authorization中设置Basic Auth用户名密码后,可以调用api端口返回数据
- Token认证:Postman在Authorization中设置Token为从认证接口获取的Token后,同样可以调用api端口返回数据