Socket.IO身份验证

134

我正在尝试在Node.js中使用Socket.IO,并试图让服务器为每个Socket.IO客户端提供一个标识。由于socket代码超出了http服务器代码的范围,因此无法轻松访问发送的请求信息,因此我假设它需要在连接期间发送。以下是解决方法:

1)向服务器获取关于谁正在通过Socket.IO连接的信息

2)验证他们所说的身份(如果使用Express,是否更容易)

7个回答

107

使用connect-redis,并将redis作为所有已认证用户的会话存储。确保在认证时向客户端发送密钥(通常为req.sessionID)。请让客户端将此密钥存储在cookie中。

在socket连接(或稍后的任何时间)上从cookie中获取此密钥并将其发送回服务器。使用此密钥在redis中获取会话信息。 (GET key)

例如:

服务器端(使用redis作为会话存储):

req.session.regenerate...
res.send({rediskey: req.sessionID});

客户端:

//store the key in a cookie
SetCookie('rediskey', <%= rediskey %>); //http://msdn.microsoft.com/en-us/library/ms533693(v=vs.85).aspx

//then when socket is connected, fetch the rediskey from the document.cookie and send it back to server
var socket = new io.Socket();

socket.on('connect', function() {
  var rediskey = GetCookie('rediskey'); //http://msdn.microsoft.com/en-us/library/ms533693(v=vs.85).aspx
  socket.send({rediskey: rediskey});
});

服务器端:

//in io.on('connection')
io.on('connection', function(client) {
  client.on('message', function(message) {

    if(message.rediskey) {
      //fetch session info from redis
      redisclient.get(message.rediskey, function(e, c) {
        client.user_logged_in = c.username;
      });
    }

  });
});

3
有一个关于此事的新有趣链接 => http://www.danielbaulig.de/socket-ioexpress/ - Alfred
1
哎呀!那个链接真的很好。这个已经过时了(使用Socket.IO 0.6.3)!本质上是相同的概念。获取cookie,检查会话存储并进行身份验证 :) - Shripad Krishna
@NightWolf,它应该可以工作,因为你是在JavaScript中获取cookie而不是在Flash(ActionScript)中。GetCookie是JavaScript函数。 - Shripad Krishna
1
@Alfred,那个链接现在好像已经失效了:( - Pro Q
@Alfred的链接于2018-02-01再次有效。 - Tom

33

我也喜欢pusherapp处理私有频道的方式。enter image description here

Pusher会生成唯一的socket id并将其发送到浏览器。之后,通过AJAX请求将该id发送给您的应用程序(1),以便让用户能够在您现有的身份验证系统中访问频道。如果成功,您的应用程序会向浏览器返回一个授权字符串,该字符串由Pusher密钥签名。然后通过WebSocket将该字符串发送回Pusher,完成授权过程(2)。

因为每个socket在socket.io中都有唯一的socket_id。

socket.on('connect', function() {
        console.log(socket.transport.sessionid);
});

他们使用签名授权字符串来授权用户。

我还没有将这个概念应用到socket.io,但我认为它可能会非常有趣。


3
太好了!但是如果您的应用服务器和Websocket服务器没有分开,使用cookie可能会更容易。但通常情况下,您希望将它们分开(如果分开,扩展套接字服务器将更容易),所以这很好 :) - Shripad Krishna
1
@Shripad,你说得完全正确,我也非常喜欢你的实现方式 :P - Alfred

33

我知道这篇文章有点老了,但是对于未来的读者来说,除了解析cookie和从存储中检索会话的方法(例如passport.socketio),您还可以考虑基于令牌的方法。

在此示例中,我使用了相当标准的JSON Web Tokens。您需要向客户端页面提供令牌,在此示例中想象一个返回JWT的身份验证端点:

var jwt = require('jsonwebtoken');
// other requires

app.post('/login', function (req, res) {

  // TODO: validate the actual user user
  var profile = {
    first_name: 'John',
    last_name: 'Doe',
    email: 'john@doe.com',
    id: 123
  };

  // we are sending the profile in the token
  var token = jwt.sign(profile, jwtSecret, { expiresInMinutes: 60*5 });

  res.json({token: token});
});

现在,您的socket.io服务器可以按照以下方式进行配置:

var socketioJwt = require('socketio-jwt');

var sio = socketIo.listen(server);

sio.set('authorization', socketioJwt.authorize({
  secret: jwtSecret,
  handshake: true
}));

sio.sockets
  .on('connection', function (socket) {
     console.log(socket.handshake.decoded_token.email, 'has joined');
     //socket.on('event');
  });

socket.io-jwt中间件期望在查询字符串中接收令牌,因此从客户端连接时只需附加它:

var socket = io.connect('', {
  query: 'token=' + token
});

我在这里写了一个更详细的关于此方法及cookies的解释,点击这里查看。


嗨!快问,如果客户端无法解码令牌,为什么要发送带有令牌的配置文件? - Carpetfizz
可以。JWT 只是 base64 编码和数字签名。客户端可以解码,但在此示例中无法验证签名。 - José F. Romaniello
@JoséF.Romaniello,你的文章已经下架了,不过我很喜欢你的方法。 - captDaylight

5
以下是我的尝试,让以下内容正常工作:
  • express: 4.14
  • socket.io: 1.5
  • passport(使用sessions):0.3
  • redis:2.6(非常快的数据结构,可处理会话;但您也可以使用其他数据库如MongoDB。然而,我鼓励您将其用于会话数据+ MongoDB用于存储其他持久性数据,例如用户)

由于您可能还想添加一些API请求,因此我们还将使用http包在同一个端口上工作,既能够支持HTTP,又能够支持Web socket。


server.js

以下摘录仅包括设置先前技术所需的所有内容。您可以在此处查看我在其中一个项目中使用的完整的server.js版本here

import http from 'http';
import express from 'express';
import passport from 'passport';
import { createClient as createRedisClient } from 'redis';
import connectRedis from 'connect-redis';
import Socketio from 'socket.io';

// Your own socket handler file, it's optional. Explained below.
import socketConnectionHandler from './sockets'; 

// Configuration about your Redis session data structure.
const redisClient = createRedisClient();
const RedisStore = connectRedis(Session);
const dbSession = new RedisStore({
  client: redisClient,
  host: 'localhost',
  port: 27017,
  prefix: 'stackoverflow_',
  disableTTL: true
});

// Let's configure Express to use our Redis storage to handle
// sessions as well. You'll probably want Express to handle your 
// sessions as well and share the same storage as your socket.io 
// does (i.e. for handling AJAX logins).
const session = Session({
  resave: true,
  saveUninitialized: true,
  key: 'SID', // this will be used for the session cookie identifier
  secret: 'secret key',
  store: dbSession
});
app.use(session);

// Let's initialize passport by using their middlewares, which do 
//everything pretty much automatically. (you have to configure login
// / register strategies on your own though (see reference 1)
app.use(passport.initialize());
app.use(passport.session());

// Socket.IO
const io = Socketio(server);
io.use((socket, next) => {
  session(socket.handshake, {}, next);
});
io.on('connection', socketConnectionHandler); 
// socket.io is ready; remember that ^this^ variable is just the 
// name that we gave to our own socket.io handler file (explained 
// just after this).

// Start server. This will start both socket.io and our optional 
// AJAX API in the given port.
const port = 3000; // Move this onto an environment variable, 
                   // it'll look more professional.
server.listen(port);
console.info(`  API listening on port ${port}`);
console.info(` Socket listening on port ${port}`);

sockets/index.js

我们的socketConnectionHandler,我不喜欢把所有东西都放在server.js里面(尽管你完全可以这么做),特别是因为这个文件很快就可能包含相当多的代码。

export default function connectionHandler(socket) {
  const userId = socket.handshake.session.passport &&
                 socket.handshake.session.passport.user; 
  // If the user is not logged in, you might find ^this^ 
  // socket.handshake.session.passport variable undefined.

  // Give the user a warm welcome.
  console.info(`⚡︎ New connection: ${userId}`);
  socket.emit('Grettings', `Grettings ${userId}`);

  // Handle disconnection.
  socket.on('disconnect', () => {
    if (process.env.NODE_ENV !== 'production') {
      console.info(`⚡︎ Disconnection: ${userId}`);
    }
  });
}

额外材料(客户端):

以下是JavaScript socket.io客户端的基本版本:

import io from 'socket.io-client';

const socketPath = '/socket.io'; // <- Default path.
                                 // But you could configure your server
                                // to something like /api/socket.io

const socket = io.connect('localhost:3000', { path: socketPath });
socket.on('connect', () => {
  console.info('Connected');
  socket.on('Grettings', (data) => {
    console.info(`Server gretting: ${data}`);
  });
});
socket.on('connect_error', (error) => {
  console.error(`Connection error: ${error}`);
});

参考资料:

我无法在代码中引用,所以我将它移动到这里。

1:如何设置您的Passport策略:https://scotch.io/tutorials/easy-node-authentication-setup-and-local#handling-signupregistration


2
这篇文章(http://simplapi.wordpress.com/2012/04/13/php-and-node-js-session-share-redi/)展示了如何:
  • 使用Predis将HTTP服务器的会话存储在Redis中
  • 通过发送到cookie中的会话ID,在node.js中从Redis获取这些会话

使用此代码,您还可以在socket.io中获取它们。

var io = require('socket.io').listen(8081);
var cookie = require('cookie');
var redis = require('redis'), client = redis.createClient();
io.sockets.on('connection', function (socket) {
    var cookies = cookie.parse(socket.handshake.headers['cookie']);
    console.log(cookies.PHPSESSID);
    client.get('sessions/' + cookies.PHPSESSID, function(err, reply) {
        console.log(JSON.parse(reply));
    });
});

2

使用会话和Redis进行客户端/服务器端通信

服务器端

io.use(function(socket, next) {
    // get here session id 
    console.log(socket.handshake.headers.cookie); and match from redis session data
    next();
});

看起来,如果您只是插入用于验证Node.js端点的相同代码(但您将不得不调整任何处理请求对象的部分),您可以重复使用您的令牌用于您的路由。 - Nick Pineda

-6

这应该可以做到

//server side

io.sockets.on('connection', function (con) {
  console.log(con.id)
})

//client side

var io = io.connect('http://...')

console.log(io.sessionid)

1
在我的情况下,io.socket.sessionid - ZiTAL
9
这甚至不能算是一个回答的尝试。这不是身份验证,只是建立连接。 - user1596138

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接