有一个接口,需要用户登录后才能访问,有两种实现方式。一种是从flask_httpauth导入HTTPBasicAuth,再创建HTTPBasicAuth的对象;另一种是自定义装饰器,被装饰的函数,需登录才能访问。
一.HTTPBasicAuth
from flask import Flask,jsonify,g
from flask_script import Manager
from flask_httpauth import HTTPBasicAuth
from flask_restful import Api,Resource
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
app=Flask(__name__)
app.config['SECRET_KEY']='F1ask!'
manager=Manager(app)
auth=HTTPBasicAuth()
api=Api(app)
@app.route('/')
def index():
return 'Flask-HTTPAuth身份认证'
@auth.verify_password
def verify_password(username_or_token,password): # 回调函数,需要认证时自动调用,认证成功返回True,认证失败返回False
if username_or_token=='xiaoming' and password=='123456':
g.username=username_or_token # 首次认证时使用用户名和密码,下次认证使用token,生成token时要用用户名(在generate_token()函数的return中),所以存入g
return True
s=Serializer(app.config['SECRET_KEY']) # 上面的if不通过时,说明可能用的token,下面开始认证token
try:
data=s.loads(username_or_token)
g.username=data['username']
return True
except:
return False
@auth.error_handler # 自定义认证失败时的错误信息
def unauthorized():
return jsonify({'error':'unauthorized access'}),401
@app.route('/test')
@auth.login_required # 被此装饰器装饰的路由,需认证成功才能访问,类似Flask-Login的@login_required。可通过浏览器访问,也可用postman模拟发送请求,需在Authorization里Type选Basic Auth,并填写Username和Password,使用token时将token填入Username,Password不用填
def test():
return '认证成功才能看到此页面'
class UserAPI(Resource): # 复制的rest_flask-restful.py
decorators=[auth.login_required] # 将需要的装饰器写在列表内,会自动将装饰器应用到类内每个方法上。若只需应用到一个方法上,则在方法前加@装饰器,如@auth.login_required
def get(self,uid):
return {'User1':'GET'}
def put(self,uid):
return {'User1':'PUT'}
def delete(self,uid):
return {'User1':'DELETE'}
api.add_resource(UserAPI,'/user/<int:uid>')
@app.route('/generate_token')
@auth.login_required
def generate_token():
s=Serializer(app.config['SECRET_KEY'],expires_in=3600)
return s.dumps({'username':g.username})
if __name__=='__main__':
manager.run()
有两种测试方法(后面有补充,建议看),登录成功后都返回{'User1':'GET'}。这两种方法本质上是同一种,因为抓包后数据一致。
- http://127.0.0.1:5000/user/1
- xiaoming:123456@127.0.0.1/user/1
- 虽然是GET,但不能用http://127.0.0.1:5000/user/1?username=xiaoming&password=123456,原因后面说
第一种测试方法
访问后浏览器提示输入用户名和密码
输完后点确定,抓包看数据
第二种测试方法
直接访问,火狐会出个提示,chrome不会
点确定后抓包,数据与第一种一模一样,就不放图了。
第二种的@什么意思?
由于第二种用@那样我是第一次见,所以查了下,下图参考链接
什么是basic认证?
先说一个词,basic认证,详细请看MDN和维基百科,MDN若想看英文的,将链接中的zh-CN改为en-US,页面底部也能选择语言,维基若打不开,去镜像站搜HTTP基本认证,或Basic access authentication。下面截取一部分解释,懂的可以跳过;不想看的,我总结了一句话:basic认证将用户名和密码的组合进行base64编码,然后放入请求头的Authorization中,请求头的值是Basic base64编码后的字符串,如Basic eGlhb21pbmc6MTIzNDU2,注意中间有空格。
插一句,尽管MDN里说chrome可能将@前的参数移除,但我这里目前chrome和基于Chromium的edge正常,没有被移除,版本都是正式版90。
抓包中的数据是什么?
看完basic认证的介绍,将Basic后面的字符串进行base64解码,就是xiaoming:123456
为什么第三种不能用?
虽然与前两种同为GET,但第三种用?拼接参数进行访问,浏览器依然弹登录框。要想知道原因,先看HTTPBasicAuth的源码
箭头处接收了头信息,对用户名和密码进行base64解码。说明浏览器将@前的参数作为basic认证的身份验证凭证信息进行base64编码,传给后端,HTTPBasicAuth模块再将收到的数据进行base64解码,得到用户名和密码,再进行验证。
而用?拼接参数进行访问,要这样写,而且前端输入的type可以用正则,如type=inputs.regex(r'^\d{6}$')
# 前端输入,类似前端html里的表单
parser=reqparse.RequestParser(bundle_errors=True)
parser.add_argument('username',type=str,help='请输入用户名',required=True)
parser.add_argument('password',type=str,help='请输入密码',required=True)
# 后端接收,类似从html表单中取数据
args=parser.parse_args()
username=args.get('username')
password=args.get('password')
总结
basic认证要有请求头Authorization,还要对数据进行base64编码和解码。第三种用?拼接参数,前端发数据时请求头不是这样,后端接收数据的方式(或源头,即从哪接收)也不同,更没base64解码,这根本不是basic认证。
为什么前两种可以?
前两种,无论是浏览器弹的登录框(因用HTTPBasicAuth会弹),还是用@,浏览器都是将用户名和密码作为basic认证的凭证进行base64编码(因第一种是"通常的或默认的"访问方式,由HTTPBasicAuth源码可知它是basic认证,所以第一种是basic认证;又因这两种方法的抓包数据一致,所以第二种也是basic认证),传给后端,后端刚好用了HTTPBasicAuth模块,这个模块就是用的basic认证,所以能对传来的base64编码的数据进行解码,然后验证是否正确,正确就登录成功。
接口地址写法补充
前面说了两种接口测试方法
- http://127.0.0.1:5000/user/1
- xiaoming:123456@127.0.0.1/user/1
由于代码是GET请求,第一种写法,浏览器会弹登录框,而这个接口在实际调用时没有登录框,所以可以在调用时手动带上请求头Authorization(例如postman里Headers里填上它,也可以后端代码模拟),它的值是Basic 用户名和密码的组合的base64编码,注意中间有空格。后端使用HTTPBasicAuth模块时,请求头Authorization的值是Basic 用户名:密码的base64编码,注意中间有空格。
第二种写法,看上去不如第一种美观;而且在不抓包的情况下,用户名和密码相对第二种来说不容易泄漏。深究安全方面的话,文章最后再说。
二.自定义装饰器,被装饰的函数,需登录才能访问
代码太多了,贴一下gitee,装饰器的代码在util.py,用户登录的代码在user.py。
下面我只写出部分代码,自行导入相关模块,运行的前提是已注册用户,即数据库中已有用户。注册的接口请去gitee里看,懒得注册就直接向数据库手动写入数据,user表的字段是timestamp、username、password、phone,第一个是时间戳。
装饰器代码
def check_user():
auth=request.headers.get('Authorization') # 登录后的每条请求,请求头中都有Authorization,即token。postman的Authorization的Type要为No Auth,否则会跟Headers中的Authorization冲突,导致获取不到正确的token
if not auth:
abort(401,error='请先登录') # 第一个参数是状态码,第二个参数是要传的消息,可以不用error,可以用其他词
mobile=cache.get(auth)
if not mobile:
abort(401,error='无效的令牌')
u=User.query.filter(User.phone==mobile).first()
if not u:
abort(401,error='该用户不存在或已被删除')
g.user=u
def login_required(func): # 自定义装饰器,被装饰的函数,需登录才能访问。也可以用flask_httpauth的HTTPBasicAuth,在第10个项目中
def wrapper(*args,**kwargs):
check_user()
return func(*args,**kwargs)
return wrapper
密码登录代码,这里我将gitee里的POST改成get了,即这里第4行里删掉了原有的location='form',def post(self)改为def get(self)
parser1=reqparse.RequestParser(bundle_errors=True)
parser1.add_argument('mobile',type=inputs.regex(r'^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$'),help='请输入11位手机号',required=True,location=['form','args']) # 短信登录时的手机号。申请重置密码时用的get,所以加上args,它对应GET请求
parser5=parser1.copy()
parser5.add_argument('password',type=str,help='请输入密码',required=True) # 密码登录时的密码
class UserResource(Resource): # 用户的类视图
def get(self): # 密码登录
args=parser5.parse_args()
mobile=args.get('mobile')
password=args.get('password')
u=User.query.filter(User.phone==mobile).first()
if u:
if check_password_hash(u.password,password): # 第一个参数是数据库中正确的密码,第二个参数是前端输入的密码
token=str(uuid.uuid4()).replace('-','')+str(random.randint(100,999)) # uuid4()基于随机数生成uuid对象(from https://www.cnblogs.com/lijingchn/p/5299000.html),再转为字符串(字符串中有多个'-'),再将字符串中的'-'去掉,后面再跟随机的三位数,整体作为token
cache.set(token,mobile,timeout=60*60*24*7) # 保存用户状态,即是否已登录
return {'message':'用户登录成功','status':200,'token':token}
return {'error':'用户名或密码错误','status':400}
api.add_resource(UserResource,'/user')
测试方法
由于这里改成GET了,所以http://127.0.0.1:5000/user?mobile=xxx&password=xxx;前面那种@的写法不能用,原因同样,这不是basic认证。
如果不改成GET,还用POST,所以http://127.0.0.1:5000/user,需要在调用接口时手动带上form-data,例如postman里Body里的form-data那样,也可以后端代码模拟。
安全方面
无论是GET还是POST,只要没有SSL,数据都能被抓包,所以建议接口使用HTTPS,下面还是引用前面的MDN和维基百科
由于用户 ID 与密码是是以明文的形式在网络中进行传输的(尽管采用了 base64 编码,但是 base64 算法是可逆的),所以基本验证方案并不安全。基本验证方案应与 HTTPS / TLS 协议搭配使用。假如没有这些安全方面的增强,那么基本验证方案不应该被来用保护敏感或者极具价值的信息。
基本认证 并没有为传送凭证(英语:transmitted credentials)提供任何机密性的保护。仅仅使用 Base64 编码并传输,而没有使用任何 加密 或 散列算法。因此,基本认证常常和 HTTPS 一起使用,以提供机密性。
现存的浏览器保存认证信息直到标签页或浏览器被关闭,或者用户清除历史记录。[3]HTTP没有为服务器提供一种方法指示客户端丢弃这些被缓存的密钥。这意味着服务器端在用户不关闭浏览器的情况下,并没有一种有效的方法来让用户退出。
同时 HTTP 并没有提供退出机制。但是,在一些浏览器上,存在清除凭证(credentials )缓存的方法。