本篇介绍下HTTP对用户进行用户名密码的基本验证过程。
01—验证原理
浏览器遇到服务器响应需要Basic验证时,会跳出一个小窗口用来输入用户名和密码,如果没有携带此header头信息服务器端会返回401状态码,并携带header头信息:
WWW-Authenticate: Basic realm="Access to the staging site"
此信息用来告诉客户端需要验证,使用Basic方式。
realm是一个保护区域的描述,可省略。
接下来浏览器要要做基本认证请求,需要携带一个HTTP头信息:
Authorization: Basic dGlueWhhcmU6MTIzNDU2
Basic后是base64编码的用户名和密码,原文是:tinyhare:123456,原文应使用utf8编码。
之后用户名密码会保存到后台,每次给对应的服务器发请求都会自动带上。
注销验证一般需要服务器返回401,WWW-Authenticate: Basic 。
所以浏览器如果需要类似注销的操作,需要访问一个永远返回401的链接(需要服务器端支持),或者向服务器提交一个错误的验证(客户端可以自己解决)。
02—示例程序
此示例使用FastAPI实现,没有过FastAPI的读者建议先学习一些基本的用法,知道怎样配置响应内容,知道Depends函数能在请求到达后提前帮你处理一些问题(这里是用来处理认证相关的东西)。
安装依赖,我使用的是python3
pip3 install uvicorn fastapi aiofiles
源码:auth_basic.py
#! python3
import secrets
import hashlib
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from starlette.responses import Response,RedirectResponse
from fastapi.responses import HTMLResponse
app = FastAPI()
security = HTTPBasic(auto_error=False) # 一定要关掉自动错误,我们手动处理
class GetUsername:
def __init__(self, auto_error: bool = True):
self.auto_error = auto_error
def __call__(self, credentials: HTTPBasicCredentials = Depends(security)):
username = None
if credentials:
username = credentials.username
password = hashlib.md5((credentials.password+"salt").encode('utf8')).hexdigest()
# 这里应该从数据库获取用户信息,密码应该存储加盐计算的散列值,这里用的md5算法
db_username = "tinyhare"
db_password = '207acd61a3c1bd506d7e9a4535359f8a' # password is 123456
chek_username = secrets.compare_digest(username, db_username)
chek_password = secrets.compare_digest(password, db_password)
if not (chek_username and chek_password):
username = None
if username == None and self.auto_error:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Basic"},
)
return username
get_current_username = GetUsername() # 自动返回认证错误
get_current_username2 = GetUsername(auto_error = False)#不自动返回认证错误
@app.get("/", response_class=HTMLResponse)
def read_current_user(username: str = Depends(get_current_username)):
return '''<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.bootcss.com/bowser/1.9.4/bowser.min.js"></script>
<script src="https://code.jquery.com/jquery-1.11.3.js"></script>
<script src="/logout.js"></script>
</head>
<body>
<h1>Hello ''' + username + ''',</h1>
<a href="/logout">Logout link</a><br><br>
<button type="button" onclick="logout('/')">Logout js</button>
</body>
</html>'''
@app.get("/logout")
def logout(username: str = Depends(get_current_username2)):
if username: #认证状态下主动返回401,使浏览器删除认证信息
response = Response(headers={"WWW-Authenticate": "Basic"}, status_code=401)
else: #非认证状态下直接跳转到某个地址
response = RedirectResponse(url="/")
return response
源码:logout.js
function logout(redirUrl) {
secUrl = window.location.href
alert("You will logout from " + bowser.name)
if (bowser.msie) {
document.execCommand('ClearAuthenticationCache', 'false');
} else if (bowser.gecko || bowser.blink) {
$.ajax({
async: false,
url: secUrl,
type: 'GET',
username: 'logout'
});
} else if (bowser.webkit) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", secUrl, true);
xmlhttp.setRequestHeader("Authorization", "Basic logout");
xmlhttp.send();
} else {
alert("Logging out automatically is unsupported for " + bowser.name
+ "You must close the browser to log out.");
}
setTimeout(function () {
window.location.href = redirUrl;
}, 200);
}
启动:
uvicorn --host 0.0.0.0 --port 8000 auth_basic:app --reload
访问:用户名tinyhare,密码123456
注:如果使用Logout link,请不要输入正确的用户名和密码,否则就又一次认证了。
带有认证的请求:
没有认证的请求(浏览器弹出认证窗口后点取消):
可以使用curl模拟请求,这样更直观:
正常认证
curl -svo /dev/null 'http://192.168.56.109:8000/' -H 'Authorization: Basic dGlueWhhcmU6MTIzNDU2'
不带认证,或错误认证
curl -svo /dev/null 'http://192.168.56.109:8000/'