SignalR服务器端消息推送 — SignalR身份认证

SignalR支持验证和授权机制,我们可以用Cookie、JWT等方式进行身份信息的传递。

由于JWT更符合现代项目的要求,因此这里讲解SignalR与JWT验证方式的使用。

第1步:

先在配置系统中配置一个名字为JWT的节点,然后在JWT节点下创建SigningKey、ExpireSeconds两个配置项。

再创建一个类JWTOptions,类中包含对应的SigningKey、ExpireSeconds两个属性。

第2步:

通过NuGet安装Microsoft.AspNetCore.Authentication.JwtBearer.

第3步:

编写代码对JWT进行配置,把如下代码添加到Program.cs的builder.Build之前。

C#
builder.Services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT"));

//添加JWT认证
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        var jwtOptions = builder.Services.BuildServiceProvider().GetRequiredService<IOptions<JWTOptions>>().Value;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = false,
            ValidateAudience = false,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)),
            ClockSkew = TimeSpan.Zero
        };

        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;
                if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/Hubs/ChatRoomHub"))
                {
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };


    });

在ASP.NET Core Web中,我们把JWT放到名字为Authorization的报文头中。

但是WebSocket不支持Authorization报文头,而且WebSocket中也不能自定义请求报文头。

我们可以把JWT放到请求的URL中,然后在服务器端检测到请求的URL中有JWT,并且请求路径是针对集线器的,我们就把URL请求中的JWT取出来赋值给context.Token,接下来ASP.NET Core就能识别、解析这个JWT了。

第4步:

在Program.cs的app.UseAuthorization之前添加app.UseAuthentication。

第5步:

在控制器类Test1Controller中增加登录并且创建JWT的操作方法Login,其方法体主要部分如下:

C#
   [HttpPost("login")]
   public async Task<IActionResult> Login([FromBody] User user)
   {
       if (user.Name == "admin" && user.Password == "123456")
       {
           var claims = new List<Claim>
           {
               new Claim(ClaimTypes.Name, user.Name),
               new Claim(ClaimTypes.Role, "admin")
           };
           var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWTOptions.SigningKey));
           var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
           var token = new JwtSecurityToken(
               issuer: _jwtOptions.Issuer,
               audience: _jwtOptions.Audience,
               claims: claims,
               expires: DateTime.Now.AddMinutes(30),
               signingCredentials: creds
           );
           return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
       }
       return BadRequest("用户名或密码错误");
   }

这里为了简化问题,使用了硬编码的用户密码,实现项目中,请把这部分用标识框架代替。

第6步,在需要登录才能访问的集线器类上或者方法上添加【Authorize】,如以下代码:

C#
 [Authorize]
 public class ChatRoomHub:Hub
 {


     public Task SendPublicMessage(string message)
     {
         //获取唯一标识
         string connId = this.Context.ConnectionId;

         string receivePublicMessage = $"{connId} {DateTime.Now}{message}";

         //发送消息
         return Clients.All.SendAsync("ReceivePublicMessage", receivePublicMessage);

     }

 }

如果【Authorize】只添加到ChatRoomHub类的方法上,而不是ChatRoomHub类上的话,那么连接到这个集线器的过程是不需要验证的,这样就造成了任意的客户端都可以连接到这个集线器上监听消息,它们只是不能向服务器发送”SendPublicMessage”消息而已,

大部分项目应该是不允许非验证用户连接集线器的,因此建议一定要把【Authorize】标注到Hub类上。

标注到Hub类的方法上的【Authorize】应该用于更详细的权限控制,比如集线器中的某些方法只有管理员才能调用。

第7步:

修改前端代码:首先,我们在页面中增加包含用户名、密码和【登录】按钮的界面元素,然后在页面的state中增加一个对用户名、密码进行绑定的属性,以及一个保存登录JWT的属性。

JavaScript
  const state = reactive({accessToken:"",userMessage:"",messages:[],loginData:{userName:"",password:""}})

因为这里需要完成登录验证后才能用获得的JWT去连接ChatRoomHub,所以我们把连接ChatRoomHub的代码从onMount中移到startConn函数中,如以下代码:

JavaScript

const startConnection = async function () {

   const transport = singnalR.HttpTransportType.WebSockets;
   const options = {skipNegotiation:true,transport};

   options.accessTokenFactory = ()=> state.accessToken;

   connection = new singnalR.HubConnectionBuilder()
   .withUrl('https://localhost:7115/Hubs/ChatRoomHub',options)
   .withAutomaticReconnect().build();

   await connection.start();

   connection.on('ReceivePublicMessage',msg=>{
    
       state.messages.push(msg);
    

   });

   alert(connection.state);

}

重点代码在第7行,这里通过options的accessTokenFactory回调函数把JWT传递给服务器端。

最后编写【登录】按钮的响应函数,如以下代码所示:

JavaScript
const loginClick = async function () {

  const response = await fetch('https://localhost:7115/api/Test1/Login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(state.loginData),
  });

  const data = await response.json();

  state.accessToken = data.accessToken;

  await startConnection();

}

在loginClick函数中,我们先通过axios向登录接口发送登录请求,然后把获得的JWT赋值给state.accessToken,最后调用startConnection函数创建连接。

运行上面的服务器端和前端项目代码,然后在浏览器端访问页面,登录后,我们就可以聊天了。

订阅评论
提醒
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
滚动至顶部