flask或flask-restful接口开发之用户登录验证

有一个接口,需要用户登录后才能访问,有两种实现方式。一种是从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 )缓存的方法。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值