Http 基础认证
通过向服务端发送用户名和密码(http header 中增加 Authorization key ,value 为username:password 的Base64 进行编码后进行认证的一种方式),如下图:

交互流程:
1.客户端访问登录接口 http://127.0.0.1:5000/flask/students/login

2. 服务端接口上使用@auth.login_required 标明需要登录认证,当请求进入时,会调用@auth.verify_password 装饰的verify_password(user_name_or_token,password)函数,这里以flask 接口为例
@students.route('/login')
# 访问路由需要登录
@auth.login_required
def test_index():
if g.current_user is None:
return jsonify({'code': 401,'message':'用户密码验证未通过'})
stu = {
'id': 0,
'name':'zxl'
}
return jsonify({'data':stu})
@auth.verify_password
def verify_password(user_name_or_token,password):
if user_name_or_token == '' or password == '':
return False
# 基于用户名 & 密码验证
user = User.query.filter_by(name = user_name_or_token).first()
if not user:
return False
else:
# 获取到用户信息,全局对象中设置current_user
if user.verify_password(password):
g.current_user = user
return True
return False
- 针对以上的验证密码的流程,当未输入用户名或密码时,或者不存在当前用户以及 密码认证不通过时,返回false ,服务端认为认证不通过时,返回401 未授权
4.用户带上正确的用户名和密码,请求接口

缺点
验证方式简单,但用户的密码被直接暴露,存在风险
密码被恶意截取后,造成重放攻击
Http 摘要认证
是另一种http 认证协议,试图修复基本认证的缺陷。改进点:
- 客户端通过发送摘要,而不是用户名,密码,解决密码暴露的风险;
- 防止恶意用户捕获并重放认证的握手过程,使用随机数,客户端使用用户名,密码和随机数生成新的摘要;
- 防止对报文内容的篡改;
- 防范常见的攻击形式;
摘要认证过程
1.客户端请求服务端 http://127.0.0.1:5000/flask/students/login_digest

2.服务端计算随机数,以及支持的算法列表发到客户端, response header 中WWW-Authenticate 字段


3.客户端输入用户名,密码,通过算法根据用户名,密码,随机数等生成摘要,然后将摘要放在Authorization首部中发送到服务器

Authoriaztion 首部字段:

4.服务端进行通过客户端提交的随机数和存储在服务器中的密码生成摘要,进行比对,如果一样,则认证通过,如果客户端用自己随机数对服务器进行质询,就会创建客户端摘要,服务端可以预先将下一个随机数计算出来,下发给客户端;

摘要认证各个首部参数含义
WWW-Authentication:定义使用http的哪一种方式(Basic、Digest、Bearer等)进行认证来获得受保护资源
realm:表示Web服务器中受保护文档的安全域,用来指示需要哪个域的用户名和密码
nonce:服务端向客户端发送质询时附带的一个随机数,每次401 会产生一个新的随机数,用来避免重放攻击
qop:安全保障值, 值为auth 表明要进行认证,值为auth-int 表明要进行完整性认证
nc:nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量。例如,在响应的第一个请求中,客户端将发送“nc=00000001”。这个指示值的目的是让服务器保持这个计数器的一个副本,以便检测重复的请求
cnonce:客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护
response:实际的摘要,以证明用户知道密码
Authorization-Info:用于返回一些与授权会话相关的附加信息
nextnonce:下一个服务端随机数,使客户端可以预先发送正确的摘要
rspauth:响应摘要,用于客户端对服务端进行认证
stale:当密码摘要使用的随机数过期时,服务器可以返回一个附带有新随机数的401响应,并指定stale=true,表示服务器在告知客户端用新的随机数来重试,而不再要求用户重新输入用户名和密码了
摘要的计算
1.安全性相关数据A1
A1=<user>:<realm>:<password> (MD5算法)
A1=MD5(<user>:<realm>:<password>):<nonce>:<cnonce>(MD5-sess算法)
2.报文相关数据
A2=<request-method>:<uri-directive-value>(qop未定义或者auth)
A2=<request-method>:<uri-directive-value>:MD5(<request-entity-body>)(qop为auth:-int)
3.计算摘要response 字段
response=MD5(MD5(A1):<nonce>:MD5(A2))(qop未定义)
response=MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))(qop = auth)
response= MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))(qop=auth-int)
flask 摘要认证的案例
auth = HTTPDigestAuth() # 摘要认证
# 摘要认证请求接口
@students.route('/login_digest')
# 访问路由需要登录
@auth.login_required
def test_index_digest():
stu = {
'id': 0,
'name':'zxl'
}
return jsonify({'data':stu})
#密码验证
@auth.get_password
def get_pw(user_name):
user = User.query.filter_by(name=user_name).first()
if user is not None:
# 模拟在服务器中存储密码
return user.password
return None
由于支持http协议的浏览器已经实现了生成摘要的相关步骤,所以通过以上可实现摘要认证的过程(稍后在服务端类库源代码中可以看到摘要认证的相关实现)
令牌token认证
基于http 协议的通信本身是无状态的,也就是用户的每次请求之间是互相独立的,系统为了在同一个用户的多次请求之间建立关联关系,即对用户数据的共享,传统的解决方案利用cookie,session 机制,但这样存在一个缺点是服务器需要维持大量session ,势必会影响服务器性能。token 机制解决这个问题,它通过针对每一个用户生成一个带有过期时间的标识,用户再下一次请求header 中带上即可,服务端通过解析token, 可以知道用户身份,flask 中itsdangerous 库通过TimedJSONWebSignatureSerializer 类实现了token 的生成与验证;
我们模拟下前后端的交互流程:
1.用户首次通过用户名,密码进行登录;
2.服务端密码验证通过后利用用户id 生成token,下发到客户端;
3.客户端携带token 请求获取用户信息;
4.当token 在服务端验证发现过期时,给客户端进行提示,客户端跳转到登录页面,输入用户名,密码继续2的流程;
class User(db.Model):
__tablename__ = 'user_account'
id = Column(Integer, primary_key=True)
name = Column(String(30))
fullname = Column(String(30))
# 生成用户token
def generate_auth_token(self,expiration):
s = Serializer(config['dev'].SECRET_KEY,expires_in = expiration)
token = s.dumps({'id': self.id})
print('token',token)
return token
@staticmethod
# token 验证
def verify_auth_token(token):
s = Serializer(config['dev'].SECRET_KEY)
try:
data = s.loads(token)
except Exception as ex:
print('ex', ex)
return None
return User.query.get(data['id']) is not None
# 和basic 基础认证配合,密码认证通过后生成token
@students.route('/tokens', methods=['GET'])
# auth = HTTPBasicAuth()
@auth.login_required
def get_token():
if g.current_user is None:
return auth_error()
return jsonify({
'token': g.current_user.generate_auth_token(expiration=7200),
'expiration': 7200
})
# 基于token 认证通过后获取用户信息
@students.route('/get_user_info', methods=['GET'])
# auth = HTTPTokenAuth()
@auth.login_required
def get_user_info():
if g.current_user is None:
return jsonify({'code':401,'message':'token认证不通过'})
stu = {
'id': 0,
'name': 'zxl'
}
return jsonify({'data': stu})
# token 验证
@auth.verify_token
def verify_token(token):
s = Serializer(config['dev'].SECRET_KEY)
try:
data = s.loads(token)
except SignatureExpired as ex:
print('SignatureExpired', ex)
return False
except BadSignature as ex:
print('BadSignature', ex)
return False
if User.query.get(data['id']) is not None:
g.current_user = User.query.get(data['id'])
return True
return False
实现过程
flask_httpauth 中模块这几种实现方式基本类似,父类HTTPAuth 下分别有HTTPBasicAuth,HTTPDigestAuth,HTTPTokenAuth 这几个不同的实现类;
基本认证主体流程:
需要验证的接口上统一带有这个@auth.login_required,login_required 是什么,顺着代码进入,可以看到是父类HTTPAuth 中一个函数

在用户请求时,先调用这个login_required() 方法,在self.get_auth()中如果是基本认证,则base64 decode 获取用户用户输入的用户名,密码

通过返回的Authorization 对象和取出的密码,通过user = self.authenticate(auth, password) 来认证,在HTTPBasicAuth 类中,使用self.verify_password_callback 回调函数来验证密码

那么这个回调函数又是什么,就是我们的验证方法带有@auth.verify_password 所标注的方法,当我们点击@auth.verify_password 进入时,会跳转到以下的代码, 所以程序会执行到我们自己的验证逻辑上;
def verify_password(self, f):
self.verify_password_callback = f
return f
同理,token验证方式的核心部分
def verify_token(self, f):
self.verify_token_callback = f
return f
def authenticate(self, auth, stored_password):
if auth:
token = auth['token']
else:
token = ""
if self.verify_token_callback:
return self.ensure_sync(self.verify_token_callback)(token)
digest 认证的核心部分,和理论中的算法实现保持一致,有一点是摘要认证的密码需要保存在服务端,用于生成摘要和header Authorization 中reponse 进行比较;
def authenticate(self, auth, stored_password_or_ha1):
if not auth or not auth.username or not auth.realm or not auth.uri \
or not auth.nonce or not auth.response \
or not stored_password_or_ha1:
return False
if not(self.verify_nonce_callback(auth.nonce)) or \
not(self.verify_opaque_callback(auth.opaque)):
return False
if auth.qop and auth.qop not in self.qop: # pragma: no cover
return False
if self.use_ha1_pw:
ha1 = stored_password_or_ha1
else:
a1 = auth.username + ":" + auth.realm + ":" + \
stored_password_or_ha1
ha1 = md5(a1.encode('utf-8')).hexdigest()
if self.algorithm == 'MD5-Sess':
ha1 = md5((ha1 + ':' + auth.nonce + ':' + auth.cnonce).encode(
'utf-8')).hexdigest()
a2 = request.method + ":" + auth.uri
ha2 = md5(a2.encode('utf-8')).hexdigest()
if auth.qop == 'auth':
a3 = ha1 + ":" + auth.nonce + ":" + auth.nc + ":" + \
auth.cnonce + ":auth:" + ha2
else:
a3 = ha1 + ":" + auth.nonce + ":" + ha2
response = md5(a3.encode('utf-8')).hexdigest()
return hmac.compare_digest(response, auth.response)
以上这几种不同的认证实现方式本质上都是通过回调函数来执行用户具体类型信息认证,当认证通过后(返回False/True)来决定接口最终的response响应;
以上如有不正确的地方,欢迎大家一块交流!
网友评论