我的Angular 2应用(用typescript编码)有一个简单的认证方案:
- 用户登录:
- 服务器返回JSON Web令牌(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的客户端占用,如果客户端未经过身份验证,则更干净的解决方案将阻止握手。
其他想法:
- 客户端可以发送路径为:
new WebSocket('wss://example.com/channel/<JWT>/')
的JWT- pro:此信息在握手期间可用
- con:该路径似乎不是JWT的"合适"位置。特别是因为中间代理和访问日志将保存路径;在设计HTTP API时,我已经决定不在url 中包含JWT。
- 服务器可以读取客户端的IP + UserAgent,并与JWT发出时HTTP服务器创建的DB记录进行匹配。然后服务器将猜测谁在连接
- pro:此信息可能在握手期间可用(不确定IP)
- con:"猜测"客户端应该与JWT相关联,而客户端从一开始就没有提供JWT,这似乎非常不安全。这将意味着,例如,有人欺骗受害者的UA并使用相同的网络(代理,公共wifi,大学内部网…)将能够冒充受害者。
如何在websockets上验证客户端?假设用户已经通过HTTP登录,并且Angular 2应用有一个JWT令牌。
我确定了以下协议:
1。客户端登录到站点并接收一个认证令牌(JSON Web令牌)
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,在查询参数
中发送票据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){/*...*/}
使用django - restframework-jwt来生成jwt,以及下面的Django-Channels 2中间件。
该令牌可以通过django - restframework-jwt http api设置,如果定义了JWT_AUTH_COOKIE
,它也会被发送到WebSocket连接。
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))
客户端打开websocket,发送用户名和密码查询参数
ws://<username>:<password>@<ip-address><path>
例子:新WebSocket美元("ws://用户:123456 @127.0.0.0/util)