We want to use django-channels for our websockets but we need to authenticate as well. We have a rest api running with django-rest-framework and there we use tokens to authenticate a user, but the same functionality does not seem to be built into django-channels.
问题:
回答1:
For Django-Channels 2 you can write custom authentication middleware https://gist.github.com/rluts/22e05ed8f53f97bdd02eafdf38f3d60a
token_auth.py:
from channels.auth import AuthMiddlewareStack
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
headers = dict(scope['headers'])
if b'authorization' in headers:
try:
token_name, token_key = headers[b'authorization'].decode().split()
if token_name == 'Token':
token = Token.objects.get(key=token_key)
scope['user'] = token.user
except Token.DoesNotExist:
scope['user'] = AnonymousUser()
return self.inner(scope)
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
routing.py:
from django.urls import path
from channels.http import AsgiHandler
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from yourapp.consumers import SocketCostumer
from yourapp.token_auth import TokenAuthMiddlewareStack
application = ProtocolTypeRouter({
"websocket": TokenAuthMiddlewareStack(
URLRouter([
path("socket/", SocketCostumer),
]),
),
})
回答2:
This answer is valid for channels 1.
You can find all information in this github issue: https://github.com/django/channels/issues/510#issuecomment-288677354
I will summarise the discussion here.
copy this mixin into your project: https://gist.github.com/leonardoo/9574251b3c7eefccd84fc38905110ce4
apply the decorator to
ws_connect
the token is received in the app via an earlier authentication request to the /auth-token
view in django-rest-framework. We use a querystring to send the token back to django-channels. If you're not using django-rest-framework you can consume the querystring in your own way. Read the mixin for how to get to it.
- After using the mixin, and the correct token is used with the upgrade / connect request, the message will have a user like in the example below.
As you can see, we have
has_permission()
implemented on theUser
model, so it can just check its instance. If there is no token or the token is invalid, there will be no user on the message.
# get_group, get_group_category and get_id are specific to the way we named # things in our implementation but I've included them for completeness. # We use the URL `wss://www.website.com/ws/app_1234?token=3a5s4er34srd32` def get_group(message): return message.content['path'].strip('/').replace('ws/', '', 1) def get_group_category(group): partition = group.rpartition('_') if partition[0]: return partition[0] else: return group def get_id(group): return group.rpartition('_')[2] def accept_connection(message, group): message.reply_channel.send({'accept': True}) Group(group).add(message.reply_channel) # here in connect_app we access the user on message # that has been set by @rest_token_user def connect_app(message, group): if message.user.has_permission(pk=get_id(group)): accept_connection(message, group) @rest_token_user def ws_connect(message): group = get_group(message) # returns 'app_1234' category = get_group_category(group) # returns 'app' if category == 'app': connect_app(message, group) # sends the message contents to everyone in the same group def ws_message(message): Group(get_group(message)).send({'text': message.content['text']}) # removes this connection from its group. In this setup a # connection wil only ever have one group. def ws_disconnect(message): Group(get_group(message)).discard(message.reply_channel)
thanks to github user leonardoo for sharing his mixin.
回答3:
The following Django-Channels 2 middleware authenticates JWTs generated by djangorestframework-jwt .
The token can be set via the djangorestframework-jwt http APIs, and it will also be sent for WebSocket connections if JWT_AUTH_COOKIE
is defined.
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))
回答4:
I believe sending token in query string can expose token even inside HTTPS protocols. To come around such issue I have used the following steps:
Create a token based REST API endpoint which creates temporary session and respond back with this
session_key
(This session is set to expire in 2 minutes)login(request,request.user)#Create session with this user request.session.set_expiry(2*60)#Make this session expire in 2Mins return Response({'session_key':request.session.session_key})
Use this
session_key
in query parameter in channels parameter
I understand there is one extra API call but I believe it's much more secure than sending token in URL string.
Edit: This is just another approach to this problem, as discussed in comments, get parameters are exposed only in urls of http protocols, which should be avoided in anyhow.
回答5:
Regarding Channels 1.x
As already pointed out here the mixin by leonardoo is the easiest way: https://gist.github.com/leonardoo/9574251b3c7eefccd84fc38905110ce4
I think, however, it is somewhat confusing to figure out what the mixin is doing and what not, so I will try to make that clear:
When looking for a way to access message.user using the native django channels decorators you would have to implement it like this:
@channel_session_user_from_http
def ws_connect(message):
print(message.user)
pass
@channel_session_user
def ws_receive(message):
print(message.user)
pass
@channel_session_user
def ws_disconnect(message):
print(message.user)
pass
Channels does that by authenticating the user, creating a http_session and then converting the http_session in a channel_session, which uses the reply channel instead of cookies to identify the client. All this is done in channel_session_user_from_http. Have a look at the channels source code for more detail: https://github.com/django/channels/blob/1.x/channels/sessions.py
leonardoo's decorator rest_token_user does, however, not create a channel session it simply stores the user in the message object in ws_connect. As the token is not sent again in ws_receive and the message object is not available either, in order to get the user in ws_receive and ws_disconnect as well, you would have to store it in the session yourself. This would be a easy way to do this:
@rest_token_user #Set message.user
@channel_session #Create a channel session
def ws_connect(message):
message.channel_session['userId'] = message.user.id
message.channel_session.save()
pass
@channel_session
def ws_receive(message):
message.user = User.objects.get(id = message.channel_session['userId'])
pass
@channel_session
def ws_disconnect(message):
message.user = User.objects.get(id = message.channel_session['userId'])
pass