【问题标题】:Websocket, Angular 2 and JSON Web token AuthenticationWebsocket、Angular 2 和 JSON Web 令牌身份验证
【发布时间】:2017-02-03 03:52:19
【问题描述】:

我的 Angular 2 应用(用 typescript 编码)有一个简单的身份验证方案:

  • 用户登录:
  • 服务器返回 JSON Web Token (JWT) abc123...
  • 在每次 API 调用中,应用都会在 Authorization 标头中发送 JWT
  • 服务器验证 JWT 并授予访问权限

现在我想添加 websockets。我想知道如何验证那里的用户。由于我无法控制将哪些标头发送到 websocket 服务器 (WS),因此我无法发送 JWT。

到目前为止我的想法(尚未实现):

  • 客户端打开websocket:let sock = new WebSocket('wss://example.com/channel/');
  • WS 服务器接受握手而不进行任何身份验证检查。标准 HTTP 标头在此阶段可用。
  • 客户端监听套接字上的open 事件。一旦套接字打开:
    • 客户端发送消息type='auth'payload='JWT_VALUE'
  • WS 服务器期望套接字上的第一条消息为auth 类型。一旦收到,服务器读取有效负载,验证JWT_VALUE 并设置isAuthenticated 标志
    • 如果验证失败,服务器断开套接字
    • 如果没有isAuthenticated 的客户端发送任何其他类型的消息,服务器将断开套接字

2 个问题:连接但从不发送 JWT 的客户端可能会占用服务器资源,如果客户端未通过身份验证,更简洁的解决方案会阻止握手。

其他想法:

  • 客户端可以在路径中发送 JWT:new WebSocket('wss://example.com/channel/<JWT>/')
    • 专业人士:此信息在握手期间可用
    • con:路径似乎不是 JWT 的“合适”位置。特别是因为中间代理和访问日志会保存路径;在设计 HTTP API 时,我已经决定不在 u​​rl 中包含 JWT
  • 服务器可以读取客户端的 IP + UserAgent 并与发出 JWT 时由 HTTP 服务器创建的 DB 记录进行匹配。然后服务器会猜测谁在连接
    • 专业人士:此信息可能在握手期间可用(不确定 IP)
    • con:当客户端从一开始就没有提供 JWT 时,“猜测”客户端应该与 JWT 相关联似乎非常不安全。这意味着例如,欺骗受害者的 UA 并使用同一网络(代理、公共 wifi、大学内部网...)的人将能够冒充受害者。

如何在 websockets 上验证客户端?假设用户已经通过 HTTP 登录,并且 Angular 2 应用程序具有 JWT 令牌。

【问题讨论】:

  • 我实际上实现了你的第一个想法 - 在握手后的第一条消息中发送发送 JWT 令牌。这不是一个非常干净的解决方案,但它确实有效。

标签: authentication angular typescript websocket jwt


【解决方案1】:

我选择了以下协议:

1.客户端登录网站并收到一个认证令牌(JSON Web Token)

GET /auth
{
    user: 'maggie',
    pwd:  'secret'
}

// response
{ token: '4ad42f...' }

2.经过身份验证的客户端请求 websocket 连接票

GET /ws_ticket
Authorization: Bearer 4ad42f...

// response: single-use ticket (will only pass validation once)
{ ticket: 'd76a55...', expires: 1475406042 }

3.客户端打开websocket,在query param中发送ticket

var socket = new WebSocket('wss://example.com/channel/?ticket=d76a55...');

4. Websocket 服务器 (PHP) 在接受握手之前验证票证

/**
* Receives the URL used to connect to websocket. Return true to admit user,
* false to reject the connection
*/
function acceptConnection($url){
    $params = parse_str(parse_url($url, PHP_URL_QUERY));
    return validateTicket($params['ticket']);
}

/** Returns true if ticket is valid, never-used, and not expired. */
function validateTicket($ticket){/*...*/}

【讨论】:

  • 感谢您分享您的解决方案。我正在考虑实现类似的东西;即通过查询字符串参数传递身份验证信息。为什么选择在#2 中创建一次性令牌(票证),而不是直接传递 JWT?看起来您的 WSS URL 是安全的,所以传递 JWT 可能是一个更简单的选择,不是吗?
  • @BillyBBone 正如我在 OP 中提到的,我在设计 HTTP API 时已经决定将 JWT 排除在 URL 之外。具体来说,我不希望这些令牌在服务器日志(甚至是我自己的日志)中可见
  • @BeetleJuice 你是如何创建票证的?
  • @ShekharSahu 如果我记得的话,它只是一个随机字符串。任何 UUID 字符串都可以用于我的目的
  • 如果这是一个愚蠢的问题,请原谅我 - 但这是否意味着一旦建立握手,您确定客户端和服务器之间的连接是安全的?如果连接断开,这个过程会重复吗?
【解决方案2】:

客户端打开 websocket,在查询参数中发送用户名和密码

ws://<username>:<password>@<ip-address><path>

示例:new $WebSocket('ws://user:123456@127.0.0.0/util')

【讨论】:

  • 这很有趣;我没有想到这一点,我喜欢简单,但我有一些顾虑。网络流量日志不会存储这些信息吗?这也需要用户输入她的用户名和密码来打开一个 websocket(因为 websockets 是在后台打开的,所以体验不好)或应用程序在本地存储中缓存用户名和密码(我不想这样做)
  • 您可以使用多大的密码会很有趣。如果您可以在服务器上自己验证密码,只需使用令牌作为密码。客户端将密码放在 Authorization: Basic 标头中,它不会显示在网络流量日志中。
【解决方案3】:

使用djangorestframework-jwt 生成您的 JWT,以及以下 Django-Channels 2 中间件。

token 可以通过 djangorestframework-jwt http API 设置,它也将被发送给 WebSocket 连接如果JWT_AUTH_COOKIE 被定义

settings.py

JWT_AUTH = {
    'JWT_AUTH_COOKIE': 'JWT',     # the cookie will also be sent on WebSocket connections
}

routing.py:

from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import path
from json_token_auth import JsonTokenAuthMiddlewareStack
from yourapp.consumers import SocketCostumer

application = ProtocolTypeRouter({
    "websocket": JsonTokenAuthMiddlewareStack(
        URLRouter([
            path("socket/", SocketCostumer),
        ]),
    ),

})

json_token_auth.py

from http import cookies

from channels.auth import AuthMiddlewareStack
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication


class JsonWebTokenAuthenticationFromScope(BaseJSONWebTokenAuthentication):
    """
    Extracts the JWT from a channel scope (instead of an http request)
    """

    def get_jwt_value(self, scope):
        try:
            cookie = next(x for x in scope['headers'] if x[0].decode('utf-8') == 'cookie')[1].decode('utf-8')
            return cookies.SimpleCookie(cookie)['JWT'].value
        except:
            return None


class JsonTokenAuthMiddleware(BaseJSONWebTokenAuthentication):
    """
    Token authorization middleware for Django Channels 2
    """

    def __init__(self, inner):
        self.inner = inner

    def __call__(self, scope):

        try:
            # Close old database connections to prevent usage of timed out connections
            close_old_connections()

            user, jwt_value = JsonWebTokenAuthenticationFromScope().authenticate(scope)
            scope['user'] = user
        except:
            scope['user'] = AnonymousUser()

        return self.inner(scope)


def JsonTokenAuthMiddlewareStack(inner):
    return JsonTokenAuthMiddleware(AuthMiddlewareStack(inner))

【讨论】:

    猜你喜欢
    • 2015-12-25
    • 2018-05-07
    • 2018-11-20
    • 2017-07-27
    • 2018-07-01
    • 2017-08-10
    • 1970-01-01
    • 2015-10-14
    • 2020-08-27
    相关资源
    最近更新 更多