在基于标准的 HTTP 身份验证系统方案中包括:HTTPBasic 基本认证和HTTPDigest摘要认证。HTTPDigest是对Basic进一步的做的安全升级的优化机制,主要解决的问题避免通过网络传输密码。取而代之的是通过摘要随机参数取代密码。
其实简单的来说的,流程就是:
当然HTTPDigest摘要认证是一个比较复杂的过程,不同的方式,生成的前面的算法不一样。
以下说明参考自:
https://blog.csdn.net/sinat_16643223/article/details/120247211
首先大概的了解一下HTTPDigest摘要认证需要关注的点有:
WWW-Authentication :用来定义使用何种方式(Basic、Digest、Bearer等)去进行认证以获取受保护的资源
realm 表示Web服务器中受保护文档的安全域(比如公司财务信息域和公司员工信息域),用来指示需要哪个域的用户名和密码
qop:保护质量,包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略,【这个比较关键,这样底部不同的策略,后期response签名规则不一样】
nonce:服务端向客户端发送质询时附带的一个随机数,这个数会经常发生变化。客户端计算密码摘要时将其附加上去,使得多次生成同一用户的密码摘要各不相同,用来防止重放攻击
response:这是由用户代理软件计算出的一个字符串,以证明用户知道口令
其中上述的参数是比较用户的,下面我的结合一下摘要认证的过程实现Fastapi中一个摘要认证算法。首先因为这个摘要认证过程其实相对复杂,涉及的问题也多,所以我这里简化一下。
首先查看Fastpai提供的摘要认证源码:
class HTTPDigest(HTTPBase):
def __init__(
self,
*,
scheme_name: Optional[str] = None,
description: Optional[str] = None,
auto_error: bool = True,
):
self.model = HTTPBaseModel(scheme="digest", description=description)
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
async def __call__(
self, request: Request
) -> Optional[HTTPAuthorizationCredentials]:
authorization: str = request.headers.get("Authorization")
scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials):
if self.auto_error:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
else:
return None
if scheme.lower() != "digest":
raise HTTPException(
status_code=HTTP_403_FORBIDDEN,
detail="Invalid authentication credentials",
)
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
通过查看源码发现好像,它是没有实现这个摘要认证过程中发起质询的,而只是简单的对头部信息一个校验而已,通过测试发现,其实却是也是,估计是它没考虑过实现这个吧!反正我是没看到怎么去实现。没办法只能自己去实现:
所以我们的可以依葫芦画瓢的,参考HttpBasic的实现自定义实现摘要认证一个简单的实现为:
def default_verify_password(password, credentials,request:Request):
# 参数校验是否非法,如果非法的话,那么需要重新的发起一下需要重新的输入用户名称和密码
realm = request.state.auth_seeions['realm']
qop = request.state.auth_seeions['qop']
nonce =request.state.auth_seeions['nonce']
unauthorized_headers = {
"WWW-Authenticate": f'Digest realm="{realm}",qop="{qop}",nonce="{nonce}'}
datas = {item[0]: item[1] for item in
[iten.split('=') for iten in credentials.replace('"', '').split(', ')]}
# 验证值信息,验证账号密码信息,验证用户信息,验证报文信息
if datas:
ha1 = generate_ha1(username=datas.get('username'),realm=realm,password=password)
ha2 = generate_ha2(request)
# 计算摘要的公式MD5(MD5(A1):<nonce>:<nc>:<conce>:<qop>:MD5(A2))
response = md5(
f"{ha1}:{datas.get('nonce')}:{datas.get('nc')}:{datas.get('cnonce')}:{datas.get('qop')}:{ha2}".encode(
'utf-8')).hexdigest()
if response != datas.get('response'):
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Unauthorized",
headers=unauthorized_headers,
)
else:
return True
else:
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Unauthorized",
headers=unauthorized_headers,
)
def generate_ha1(username,realm, password):
a1 = username + ":" + realm + ":" + password
a1 = a1.encode('utf-8')
return md5(a1).hexdigest()
def generate_ha2(requet: Request):
a2 = requet.method + ":" + requet.url.path
a2 = a2.encode('utf-8')
return md5(a2).hexdigest()
class HTTPDigest(HTTPBase):
def __init__(
self,
*,
scheme_name: Optional[str] = None,
description: Optional[str] = None,
auto_error: bool = True,
# 表示Web服务器中受保护文档的安全域(比如公司财务信息域和公司员工信息域),用来指示需要哪个域的用户名和密码
realm: Optional[str] = None,
# 保护质量,包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略,(可以为空,但是)不推荐为空值
qop: Optional[str] = "auth, auth-int",
# 由服务器指定的字符串,客户端在后续指向同一个受保护区间的请求中应该在 Authorization 头中原样返回,建议使用 base64 或者 16 进制数
opaque: Optional[str] = None,
):
self.model = HTTPBaseModel(scheme="digest", description=description)
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
self.realm=realm
self.qop = qop
self.opaque = opaque
self.nonce = None
self.random = SystemRandom()
try:
self.random.random()
except NotImplementedError: # pragma: no cover
self.random = Random()
def _generate_random(self):
return md5(str(self.random.random()).encode('utf-8')).hexdigest()
@property
def default_generate_nonce(self):
return self._generate_random()
async def __call__(
self, request: Request
):
authorization: str = request.headers.get("Authorization")
scheme, credentials = get_authorization_scheme_param(authorization)
# 第一次请求,在没有进行认证或认证失败时,服务端需要返回401 Unauthorized,并对客户端发出质询,需要输入用户名和密码等信息
self.request = request
if not (authorization and scheme and credentials):
if self.auto_error:
if self.realm and self.qop:
self.nonce = self.default_generate_nonce
self.unauthorized_headers = {
"WWW-Authenticate": f'Digest realm="{self.realm}",qop="{self.qop}",nonce="{self.nonce}'}
else:
self.unauthorized_headers = {"WWW-Authenticate": f'Digest realm="{self.realm}",'}
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Unauthorized",
headers=self.unauthorized_headers,
)
else:
return None
# 如果认证的是已经通过了那么判断收是摘要认证的模式
if scheme.lower() != "digest":
raise HTTPException(
status_code=HTTP_403_FORBIDDEN,
detail="Invalid authentication credentials",
)
self.request.state.auth_seeions={}
self.request.state.auth_seeions['realm']=self.realm
self.request.state.auth_seeions['qop'] = self.qop
self.request.state.auth_seeions['nonce'] = self.nonce
# self
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
其中上述代码主要值得关注的点是:
计算摘要的公式算法是:MD5(MD5(A1):<nonce>:<nc>:<conce>:<qop>:MD5(A2))
"WWW-Authenticate": f'Digest realm="{self.realm}",qop="{self.qop}",nonce="{self.nonce}'}
通过上面的方式实现后,我们的定义一个关于摘要认证的对象:
security = HTTPDigest(realm="nihao.baiwu.net", qop="auth")
然后定义我们的路由地址:
@app.get("/digest")
async def digest(request:Request,auth: HTTPAuthorizationCredentials = Depends(security)):
# print(credentials)
if default_verify_password(password='123456',credentials=auth.credentials,request=request):
return PlainTextResponse('登入成功')
需要注意的点有:
response = md5(
f"{ha1}:{datas.get('nonce')}:{datas.get('nc')}:{datas.get('cnonce')}:{datas.get('qop')}:{ha2}".encode(
'utf-8')).hexdigest()
这个算法生成是针对于qop使用的auth的方式。
由于操作文档不支持这么模式的验证:
此时我们的可以通 浏览器访问我们的地址:
然后我们的输入错误的用户名和面则会一直弹窗要求输入,如果正确则通过。
甚至我们可以通过requests库来验证:
import requests
from requests.auth import HTTPDigestAuth
url = 'http://127.0.0.1:8000/digest'
resp = requests.get(url,auth=HTTPDigestAuth('admin','123456'))
print(resp.text)
上面仅仅是基本的实现,要扩展的东西很多,用户验证,随机数验证之类的。仅供参考。
以上仅仅是个人结合自己的实际需求,做学习的实践笔记!如有笔误!欢迎批评指正!感谢各位大佬!
END
简书:https://www.jianshu.com/u/d6960089b087
掘金:https://juejin.cn/user/2963939079225608
公众号:微信搜【程序员小钟同学】
小钟同学 | 文 【欢迎一起学习交流】| QQ:308711822
原文作者:程序员小钟同学;如有侵权,请联系管理员删除。