JWT+Redis实现限制多用户同时登录一个账号、限制账号并发数。

该项目使用ASP.NET Core 3.1 API、Redis和JWT,结合微信小程序,实现限制同一账号多用户同时在线和并发数控制。通过在用户登录时记录账号信息到Redis,并用websocket监听客户端状态,当账号已存在登录态数据时阻止新登录,退出时清除数据。前端使用signal.JS进行websocket通信。

项目场景:

web\小程序。限制账户同时在线、登录。或者限制账户并发数

项目环境:asp.net core 3.1 API+redis+jwt +微信小程序


问题描述:

我们经常可以看到一些软件无法同时登录同一个账号,也就是同时只能有一个用户使用这个账号。或者另一种情形,限制用户并发数。这些都涉及到对在线用户登陆态的管理


解决思路:

  • 后端在用户登录时记录下登录账号、时间、Token值,以Hash类型存入Redis。
  • 登录成功后使用websocket监听客户端是否在线(主要为了解决用户非正常退出或关闭,而导致登陆态无法改变的情况)
  • 用户登录时判断Redis数据库中该账号是否已存在登陆态数据,已存在则无法登录。
  • 用户退出或是程序非正常关闭时,WS连接断开并同时清除redis中的用户登陆态数据

后端解决方案代码:

JWT实现鉴权中心,写成服务注入IOC容器中

using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace canteen_order.Unity
{
public interface IJWTService{
        string GetToken(string UserName);
}
    public class JWTService : IJWTService
    {
        private readonly IConfiguration _configuration;
        public JWTService(IConfiguration configuration){
            _configuration = configuration;
        }
        public string GetToken(string UserName)
        {
          //payload信息
            Claim[] claim = new[] { new Claim("WorkId",UserName), };
            SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["SecurityKey"]));
            SigningCredentials credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
            expires: DateTime.Now.AddMinutes(10),
            signingCredentials:credentials,
            audience:_configuration["Audience"],
            issuer:_configuration["Issuer"],
            claims:claim
            ) ;
            string reutrnToken = new JwtSecurityTokenHandler().WriteToken(token);
            return reutrnToken; //生成Token值
        }
    }
}

Redis写成服务注入IOC容器中

using ServiceStack.Redis;

namespace canteen_order.Services
{
    public interface ICache
    {
        public IRedisClient redisClient { get; }
    }
}
----------------------------------------------------------------------
using canteen_order.Services;
using ServiceStack.Redis;


namespace canteen_order.Unity
{
    public class RedisHelper:ICache
    {

        public IRedisClient _redisClient = null;



        public IRedisClient redisClient => _redisClient ?? new RedisClient("127.0.0.1", 6379);
    }
}

使用signalR进行websocket通信

using canteen_order.Models;
using canteen_order.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace canteen_order.Hubs
{
    public class MenuHub:Hub
    {
       
        public ICache _redisHelper;
        public static ConcurrentDictionary<string, OnlineUser> OnlineClients { get; } = new ConcurrentDictionary<string, OnlineUser>();

        private static readonly object SyncObj = new object();

        public MenuHub(ICache cache)
        {
            _redisHelper = cache;
           
        }

        public override async Task OnConnectedAsync()
        {

            var http = Context.GetHttpContext();
       
           
            var client = new OnlineUser()
            {
              WorkId= http.Request.Query["WorkId"],

            };

            lock (SyncObj)
            {
                OnlineClients[Context.ConnectionId] = client;
            }

            await base.OnConnectedAsync();
          
        }

        public override async Task OnDisconnectedAsync(Exception exception)
        {
            
            await base.OnDisconnectedAsync(exception);
          

            bool isRemoved=false;
            OnlineUser client=null;
            lock (SyncObj)
            {
                _redisHelper.redisClient.RemoveEntryFromHash("OnlineUser", OnlineClients[Context.ConnectionId].WorkId);
               //WS连接断开后,清除登录态数据。
              isRemoved = OnlineClients.TryRemove(Context.ConnectionId, out client);


            }
           
        }

      
        }
    }
}

登录控制器引用上述服务

public async Task<string> Login([FromBody] User user){
            
            var use = await RepositoryWrap.User.Login(user);
            if(use!=null){
                if(_cache.redisClient.HashContainsEntry("OnlineUser",user.WorkId)){//判断该账户是否已登录
                 
                    return "false";
                }
               string token = _jWTService.GetToken(user.WorkId);//重点

                _cache.redisClient.SetEntryInHash("OnlineUser",user.WorkId,JsonConvert.SerializeObject(new OnlineUser { WorkId = user.WorkId, dateTime = DateTime.Now,Token = token,  }));--重点
               
                return token;//登录成功返回前端Token值
            }
            
            return "false";
       }

前端解决方案代码

onLoad: function (options) {
    var that=this

 that.setData({WorkId:wx.getStorageSync('user')})   
    this.hubConnect = new Hub.HubConnection();

    this.hubConnect.start("https://localhost:5001/menuhub", { WorkId:that.data.WorkId });
    this.hubConnect.onOpen = res => {
      console.log("成功开启连接")
    }

    this.hubConnect.on("system", res => {
      wx.showModal({
        title: '系统消息',
        content: res,
      })
    })

    this.hubConnect.on("receive", res => {
      centendata.push({
        content: res.msg,
        time: new Date().toLocaleString(),
        head_owner: res.avatar,
        is_show_right: 0
      });
      this.setData({
        centendata: centendata
      })
    })

  },

小程序因为不支持JQ,所以引用了一个signal.JS的封装包。实现对HUB的操作,需要的可以进群联系我。

最终效果

在这里插入图片描述
在这里插入图片描述
Redis数据库中显示已存在登录态数据

总结

其实实现的方法还有很多譬如 session+redis,不过因为小程序不支持session,自定义请求头后测试发现还存在一些问题就弃用了这个方法,但是web端是可以适用的,因为浏览器支持session、cookie。
本次利用了redisd的Hash类型(键唯一)解决了限制账户同时在线的情况,稍加修改就可以变成限制用户并发数的情况,比如count<3.
本文只是实现了基础,其上还可以继续拓展。因篇幅有限,需要详细解答或者JS包的请进Q群联系:6406277

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值