web身份验证常用方法探索

web开发都要涉及身份认证的问题。

这里简单做了一个梳理,常用身份验证的方法。

HTTP基本身份认证

HTTP 协议中内置的基本身份验证(Basic auth)是最基本的身份验证形式。使用它时,登录凭据随每个请求一起发送到请求标头中:

"Authorization: Basic eWFuZyUzQTEyMzQ1Ng=="

用户名和密码未加密。相反,用户名和密码使用符号连接在一起以形成单个字符串:。然后使用 base64 对此字符串进行编码。username:password

这种方法是无状态的,因此客户端必须为每个请求提供凭据。它适用于 API 调用以及不需要持久会话的简单身份验证工作流。

优点

  • 由于执行的操作不多,因此使用该方法可以快速完成身份验证。
  • 易于实现。
  • 所有主要浏览器均支持。

缺点

  • 安全性低
  • 凭据必须随每个请求一起发送
  • 只能使用无效的凭据重写凭据来注销用户。
  • 有账号密码的情况下基本没有反爬能力。

常用包

  • flask-HTTPAuth
  • FastAPI:HTTP BasicAuth
from flask import Flask
from flask_httpauth import HTTPBasicAuth
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__)
auth = HTTPBasicAuth()

users = {
    "username": generate_password_hash("password"),
}


@auth.verify_password
def verify_password(username, password):
    if username in users and check_password_hash(users.get("username"), password):
        return username


@app.route("/")
@auth.login_required
def index():
    return f"You have successfully logged in, {auth.current_user()}"


if __name__ == "__main__":
    app.run()

HTTP 摘要验证

HTTP Digest Auth(或 Digest Access Auth)是 HTTP 基本验证的一种更安全的形式。主要区别在于 HTTP 摘要验证的密码是以 MD5 哈希形式代替纯文本形式发送的,因此它比基本身份验证更安全。

是另一种http 认证协议,试图修复基本认证的缺陷。改进点:

  • 客户端通过发送摘要,而不是用户名,密码,解决密码暴露的风险;
  • 防止恶意用户捕获并重放认证的握手过程,使用随机数,客户端使用用户名,密码和随机数生成新的摘要;
  • 防止对报文内容的篡改;
  • 防范常见的攻击形式;

流程

  • 客户端发起登录请求
  • 服务端计算随机数,以及支持的算法列表发到客户端, response header 中WWW-Authenticate 字段。
  • 户端输入用户名,密码,通过算法根据用户名,密码,随机数等生成摘要,然后将摘要放在Authorization首部中发送到服务器
  • 服务端进行通过客户端提交的随机数和存储在服务器中的密码生成摘要,进行比对,如果一样,则认证通过,如果客户端用自己随机数对服务器进行质询,就会创建客户端摘要,服务端可以预先将下一个随机数计算出来,下发给客户端;

字段解释

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,表示服务器在告知客户端用新的随机数来重试,而不再要求用户重新输入用户名和密码了

优点

  • 由于密码不是以纯文本形式发送的,因此比基本身份验证更安全。
  • 易于实现。
  • 所有主要浏览器均支持。

缺点

  • 凭据必须随每个请求一起发送。
  • 只能使用无效的凭据重写凭据来注销用户。
  • 与基本身份验证相比,由于无法使用 bcrypt,因此密码在服务器上的安全性较低。
  • 容易受到中间人攻击。

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

基于会话的验证

使用基于会话的身份验证(或称会话 cookie 验证、基于 cookie 的验证)时,用户状态存储在服务器上。它不需要用户在每个请求中提供用户名或密码,而是在登录后由服务器验证凭据。如果凭据有效,它将生成一个会话,并将其存储在一个会话存储中,然后将其会话 ID 发送回浏览器。浏览器将这个会话 ID 存储为 cookie,该 cookie 可以在向服务器发出请求时随时发送。

基于会话的身份验证是有状态的。每次客户端请求服务器时,服务器必须将会话放在内存中,以便将会话 ID 绑定到关联的用户。

优点

  • 后续登录速度更快,因为不需要凭据。
  • 改善用户体验。
  • 相当容易实现。许多框架(例如 Django)都是开箱即用的。

缺点

  • 它是有状态的。服务器要在服务端跟踪每个会话。用于存储用户会话信息的会话存储需要在多个服务之间共享以启用身份验证。因此,由于 REST 是无状态协议,它不适用于 RESTful 服务。
  • 即使不需要验证,Cookie 也会随每个请求一起发送
  • 易受 CSRF 攻击。在这里阅读更多关于 CSRF 以及如何在 Flask 中防御它的信息。

常用包

  • flask-login
  • FastApi-Login

flask 样例

from flask import Flask, request
from flask_login import (
    LoginManager,
    UserMixin,
    current_user,
    login_required,
    login_user,
)
from werkzeug.security import generate_password_hash, check_password_hash


app = Flask(__name__)
app.config.update(
    SECRET_KEY="change_this_key",
)

login_manager = LoginManager()
login_manager.init_app(app)


users = {
    "username": generate_password_hash("password"),
}


class User(UserMixin):
    ...


@login_manager.user_loader
def user_loader(username: str):
    if username in users:
        user_model = User()
        user_model.id = username
        return user_model
    return None


@app.route("/login", methods=["POST"])
def login_page():
    data = request.get_json()
    username = data.get("username")
    password = data.get("password")

    if username in users:
        if check_password_hash(users.get(username), password):
            user_model = User()
            user_model.id = username
            login_user(user_model)
        else:
            return "Wrong credentials"
    return "logged in"


@app.route("/")
@login_required
def protected():
    return f"Current user: {current_user.id}"


if __name__ == "__main__":
    app.run()

基于令牌的身份验证

这种方法使用令牌而不是 cookie 来验证用户。用户使用有效的凭据验证身份,服务器返回签名的令牌。这个令牌可用于后续请求。

基于http 协议的通信本身是无状态的,也就是用户的每次请求之间是互相独立的,系统为了在同一个用户的多次请求之间建立关联关系,即对用户数据的共享,传统的解决方案利用cookie,session 机制,但这样存在一个缺点是服务器需要维持大量session ,势必会影响服务器性能。token 机制解决这个问题,它通过针对每一个用户生成一个带有过期时间的标识,用户再下一次请求header 中带上即可,服务端通过解析token, 可以知道用户身份,flask 中itsdangerous 库通过TimedJSONWebSignatureSerializer 类实现了token 的生成与验证;

流程

1.用户首次通过用户名,密码进行登录;
2.服务端密码验证通过后利用用户id 生成token,下发到客户端;
3.客户端携带token 请求获取用户信息;
4.当token 在服务端验证发现过期时,给客户端进行提示,客户端跳转到登录页面,输入用户名,密码继续2的流程;

原理

最常用的令牌是 JSON Web Token(JWT)。JWT 包含三个部分:

标头(包括令牌类型和使用的哈希算法)

负载(包括声明,是关于主题的陈述)

签名(用于验证消息在此过程中未被更改)

这三部分都是 base64 编码的,并使用一个.串联并做哈希。由于它们已编码,因此任何人都可以解码和读取消息。但是,只有验证的用户才能生成有效的签名令牌。令牌使用签名来验证,签名用的是一个私钥。

JSON Web Token(JWT)是一种紧凑的、URL 安全的方法,用于表示要在两方之间转移的声明。JWT 中的声明被编码为一个 JSON 对象,用作一个 JSON Web Signature(JWS)结构的负载,或一个 JSON Web Encryption(JWE)结构的纯文本,从而使声明可以进行数字签名,或使用一个消息验证码 Message Authentication Code(MAC)来做完整性保护和/或加密。——IETF

令牌不必保存在服务端。只需使用它们的签名即可验证它们。近年来,由于 RESTfulAPI 和单页应用(SPA)的出现,令牌的使用量有所增加。

优点

  • 它是无状态的。服务器不需要存储令牌,因为可以使用签名对其进行验证。由于不需要数据库查找,因此可以让请求更快。
  • 适用于微服务架构,其中有多个服务需要验证。我们只需在每一端配置如何处理令牌和令牌密钥即可。

缺点

  • 根据令牌在客户端上的保存方式,它可能导致 XSS(通过 localStorage)或 CSRF(通过 cookie)攻击。
  • 令牌无法被删除。它们只能过期。这意味着如果令牌泄漏,则攻击者可以滥用令牌直到其到期。因此,将令牌过期时间设置为非常小的值(例如 15 分钟)是非常重要的。
  • 需要设置令牌刷新以在到期时自动发行令牌。
  • 删除令牌的一种方法是创建一个将令牌列入黑名单的数据库。这为微服务架构增加了额外的开销并引入了状态。

flask代码

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

一次性密码(验证码)

一次性密码(One Time Password,OTP)通常用作身份验证的确认。OTP 是随机生成的代码,可用于验证用户是否是他们声称的身份。它通常用在启用双因素身份验证的应用中,在用户凭据确认后使用。

要使用 OTP,必须存在一个受信任的系统。这个受信任的系统可以是经过验证的电子邮件或手机号码。

现代 OTP 是无状态的。可以使用多种方法来验证它们。尽管有几种不同类型的 OTP,但基于时间的 OTP(TOTP)可以说是最常见的类型。它们生成后会在一段时间后过期。

由于 OTP 让你获得了额外的一层安全保护,因此建议将 OTP 用于涉及高度敏感数据的应用,例如在线银行和其他金融服务。

流程
实现 OTP 的传统方式:

客户端发送用户名和密码

经过凭据验证后,服务器会生成一个随机代码,将其存储在服务端,然后将代码发送到受信任的系统

用户在受信任的系统上获取代码,然后在 Web 应用上重新输入它

服务器对照存储的代码验证输入的代码,并相应地授予访问权限

工作流程

客户端发送用户名和密码

经过凭据验证后,服务器会使用随机生成的种子生成随机代码,并将种子存储在服务端,然后将代码发送到受信任的系统

用户在受信任的系统上获取代码,然后将其输入回 Web 应用

服务器使用存储的种子验证代码,确保其未过期,并相应地授予访问权限

谷歌身份验证器、微软身份验证器和 FreeOTP 等 OTP 代理如何工作:

注册双因素身份验证(2FA)后,服务器会生成一个随机种子值,并将该种子以唯一 QR 码的形式发送给用户

用户使用其 2FA 应用程序扫描 QR 码以验证受信任的设备

每当需要 OTP 时,用户都会在其设备上检查代码,然后在 Web 应用中输入该代码

服务器验证代码并相应地授予访问权限

优点

  • 添加了一层额外的保护
  • 不会有被盗密码在实现 OTP 的多个站点或服务上通过验证的危险

缺点

  • 你需要存储用于生成 OTP 的种子。
  • 像谷歌验证器这样的 OTP 代理中,如果你丢失了恢复代码,则很难再次设置 OTP 代理
  • 当受信任的设备不可用时(电池耗尽,网络错误等)会出现问题。因此通常需要一个备用设备,这个设备会引入一个额外的攻击媒介。

OAuth 和 OpenID

OAuth/OAuth2 和 OpenID 分别是授权和身份验证的流行形式。它们用于实现社交登录,一种单点登录(SSO)形式。社交登录使用来自诸如 Facebook、Twitter 或谷歌等社交网络服务的现有信息登录到第三方网站,而不是创建一个专用于该网站的新登录帐户

优点

  • 提高安全性。
  • 由于无需创建和记住用户名或密码,因此登录流程更加轻松快捷。
  • 如果发生安全漏洞,由于身份验证是无密码的,因此不会对第三方造成损害。

缺点

  • 现在,你的应用程序依赖于你无法控制的另一个应用。如果 OpenID 系统关闭,则用户将无法登录。
  • 人们通常倾向于忽略 OAuth 应用程序请求的权限。
  • 在你配置的 OpenID 提供方上没有帐户的用户将无法访问你的应用程序。最好的方法是同时实现多种途径。例如用户名和密码以及 OpenID,并让用户自行选择。

总结

对于利用服务端模板的 Web 应用程序,通过用户名和密码进行基于会话的身份验证通常是最合适的。你也可以添加 OAuth 和 OpenID。

对于 RESTful API,建议使用基于令牌的身份验证,因为它是无状态的。

如果必须处理高度敏感的数据,则你可能需要将 OTP 添加到身份验证流中。