增加功能:在线用户增加客户端浏览器与操作系统信息,1分钟后清理会话信息 #IS60Z

This commit is contained in:
Argo-MacBookPro 2019-03-02 15:15:47 +08:00
parent 116150e60d
commit 8c9c5b5d6c
16 changed files with 221 additions and 41 deletions

View File

@ -15,7 +15,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Bootstrap.Security.Mvc" Version="2.2.3" /> <PackageReference Include="Bootstrap.Security.Mvc" Version="2.2.3" />
<PackageReference Include="Longbow.Logging" Version="2.2.5" /> <PackageReference Include="Longbow.Logging" Version="2.2.5" />
<PackageReference Include="Longbow.Web" Version="2.2.4" />
<PackageReference Include="Microsoft.AspNetCore.App" /> <PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="3.1.2" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="3.1.2" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.2" PrivateAssets="All" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.2" PrivateAssets="All" />

View File

@ -19,11 +19,11 @@ namespace Bootstrap.Admin.Controllers.Api
[HttpPost()] [HttpPost()]
public IEnumerable<OnlineUser> Post([FromServices]IOnlineUsers onlineUSers) public IEnumerable<OnlineUser> Post([FromServices]IOnlineUsers onlineUSers)
{ {
return onlineUSers.OnlineUsers; return onlineUSers.OnlineUsers.OrderByDescending(u => u.LastAccessTime);
} }
/// <summary> /// <summary>
/// 获取指定IP地址的在线用户请求地址明细数据 /// 获取指定会话的在线用户请求地址明细数据
/// </summary> /// </summary>
/// <param name="id"></param> /// <param name="id"></param>
/// <param name="onlineUSers"></param> /// <param name="onlineUSers"></param>
@ -31,7 +31,7 @@ namespace Bootstrap.Admin.Controllers.Api
[HttpGet("{id}")] [HttpGet("{id}")]
public IEnumerable<KeyValuePair<DateTime, string>> Get(string id, [FromServices]IOnlineUsers onlineUSers) public IEnumerable<KeyValuePair<DateTime, string>> Get(string id, [FromServices]IOnlineUsers onlineUSers)
{ {
var user = onlineUSers.OnlineUsers.FirstOrDefault(u => u.Ip == id); var user = onlineUSers.OnlineUsers.FirstOrDefault(u => u.ConnectionId == id);
return user?.RequestUrls ?? new KeyValuePair<DateTime, string>[0]; return user?.RequestUrls ?? new KeyValuePair<DateTime, string>[0];
} }
} }

View File

@ -1,6 +1,10 @@
using System; using Longbow.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
namespace Bootstrap.Admin namespace Bootstrap.Admin
{ {
@ -9,7 +13,17 @@ namespace Bootstrap.Admin
/// </summary> /// </summary>
internal class DefaultOnlineUsers : IOnlineUsers internal class DefaultOnlineUsers : IOnlineUsers
{ {
private ConcurrentDictionary<string, OnlineUser> _onlineUsers = new ConcurrentDictionary<string, OnlineUser>(); private ConcurrentDictionary<string, OnlineUserCache> _onlineUsers = new ConcurrentDictionary<string, OnlineUserCache>();
private HttpClient _client;
private IEnumerable<string> _local = new string[] { "::1", "127.0.0.1" };
/// <summary>
///
/// </summary>
/// <param name="factory"></param>
public DefaultOnlineUsers(IHttpClientFactory factory)
{
_client = factory.CreateClient(OnlineUsersServicesCollectionExtensions.IPSvrHttpClientName);
}
/// <summary> /// <summary>
/// ///
@ -17,7 +31,7 @@ namespace Bootstrap.Admin
/// <returns></returns> /// <returns></returns>
public IEnumerable<OnlineUser> OnlineUsers public IEnumerable<OnlineUser> OnlineUsers
{ {
get { return _onlineUsers.Values; } get { return _onlineUsers.Values.Select(v => v.User); }
} }
/// <summary> /// <summary>
@ -27,6 +41,45 @@ namespace Bootstrap.Admin
/// <param name="addValueFactory"></param> /// <param name="addValueFactory"></param>
/// <param name="updateValueFactory"></param> /// <param name="updateValueFactory"></param>
/// <returns></returns> /// <returns></returns>
public OnlineUser AddOrUpdate(string key, Func<string, OnlineUser> addValueFactory, Func<string, OnlineUser, OnlineUser> updateValueFactory) => _onlineUsers.AddOrUpdate(key, addValueFactory, updateValueFactory); public OnlineUserCache AddOrUpdate(string key, Func<string, OnlineUserCache> addValueFactory, Func<string, OnlineUserCache, OnlineUserCache> updateValueFactory) => _onlineUsers.AddOrUpdate(key, addValueFactory, updateValueFactory);
/// <summary>
///
/// </summary>
/// <param name="key"></param>
/// <param name="onlineUserCache"></param>
/// <returns></returns>
public bool TryRemove(string key, out OnlineUserCache onlineUserCache) => _onlineUsers.TryRemove(key, out onlineUserCache);
/// <summary>
///
/// </summary>
/// <param name="ip"></param>
/// <returns></returns>
public string RetrieveLocaleByIp(string ip = null)
{
if (ip.IsNullOrEmpty() || _local.Any(p => p == ip)) return "本地连接";
var url = ConfigurationManager.AppSettings["IPSvrUrl"];
var task = _client.GetAsJsonAsync<IPLocator>($"{url}{ip}");
task.Wait();
return task.Result.status == "0" ? string.Join(" ", task.Result.address.SpanSplit("|").Skip(1).Take(2)) : "XX XX";
}
/// <summary>
///
/// </summary>
private class IPLocator
{
/// <summary>
/// 详细地址信息
/// </summary>
public string address { get; set; }
/// <summary>
/// 结果状态返回码
/// </summary>
public string status { get; set; }
}
} }
} }

View File

@ -20,6 +20,21 @@ namespace Bootstrap.Admin
/// <param name="addValueFactory"></param> /// <param name="addValueFactory"></param>
/// <param name="updateValueFactory"></param> /// <param name="updateValueFactory"></param>
/// <returns></returns> /// <returns></returns>
OnlineUser AddOrUpdate(string key, Func<string, OnlineUser> addValueFactory, Func<string, OnlineUser, OnlineUser> updateValueFactory); OnlineUserCache AddOrUpdate(string key, Func<string, OnlineUserCache> addValueFactory, Func<string, OnlineUserCache, OnlineUserCache> updateValueFactory);
/// <summary>
///
/// </summary>
/// <param name="key"></param>
/// <param name="onlineUserCache"></param>
/// <returns></returns>
bool TryRemove(string key, out OnlineUserCache onlineUserCache);
/// <summary>
///
/// </summary>
/// <param name="ip"></param>
/// <returns></returns>
string RetrieveLocaleByIp(string ip = null);
} }
} }

View File

@ -12,11 +12,21 @@ namespace Bootstrap.Admin
{ {
private ConcurrentQueue<KeyValuePair<DateTime, string>> _requestUrls = new ConcurrentQueue<KeyValuePair<DateTime, string>>(); private ConcurrentQueue<KeyValuePair<DateTime, string>> _requestUrls = new ConcurrentQueue<KeyValuePair<DateTime, string>>();
/// <summary>
///
/// </summary>
public string ConnectionId { get; set; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public string UserName { get; set; } public string UserName { get; set; }
/// <summary>
///
/// </summary>
public string DisplayName { get; set; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@ -27,6 +37,11 @@ namespace Bootstrap.Admin
/// </summary> /// </summary>
public DateTime LastAccessTime { get; set; } public DateTime LastAccessTime { get; set; }
/// <summary>
///
/// </summary>
public string Location { get; set; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@ -37,6 +52,16 @@ namespace Bootstrap.Admin
/// </summary> /// </summary>
public string Ip { get; set; } public string Ip { get; set; }
/// <summary>
///
/// </summary>
public string Browser { get; set; }
/// <summary>
///
/// </summary>
public string OS { get; set; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>

View File

@ -0,0 +1,63 @@
using System;
using System.Threading;
namespace Bootstrap.Admin
{
/// <summary>
///
/// </summary>
public class OnlineUserCache : IDisposable
{
private Timer dispatcher;
/// <summary>
///
/// </summary>
/// <param name="user"></param>
/// <param name="action"></param>
public OnlineUserCache(OnlineUser user, Action action)
{
User = user;
dispatcher = new Timer(_ => action(), null, TimeSpan.FromMinutes(1), Timeout.InfiniteTimeSpan);
}
/// <summary>
///
/// </summary>
public OnlineUser User { get; set; }
/// <summary>
///
/// </summary>
public void Reset()
{
if (dispatcher != null) dispatcher.Change(TimeSpan.FromMinutes(1), Timeout.InfiniteTimeSpan);
}
#region Impletement IDispose
/// <summary>
///
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (dispatcher != null)
{
dispatcher.Dispose();
dispatcher = null;
}
}
}
/// <summary>
///
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}

View File

@ -1,9 +1,10 @@
using Bootstrap.Admin; using Bootstrap.Admin;
using Bootstrap.DataAccess;
using Longbow.Web;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Builder namespace Microsoft.AspNetCore.Builder
{ {
@ -19,30 +20,33 @@ namespace Microsoft.AspNetCore.Builder
/// <returns></returns> /// <returns></returns>
public static IApplicationBuilder UseOnlineUsers(this IApplicationBuilder builder) => builder.UseWhen(context => context.Filter(), app => app.Use(async (context, next) => public static IApplicationBuilder UseOnlineUsers(this IApplicationBuilder builder) => builder.UseWhen(context => context.Filter(), app => app.Use(async (context, next) =>
{ {
await Task.Run(() => await System.Threading.Tasks.Task.Run(() =>
{ {
var onlineUsers = context.RequestServices.GetService<IOnlineUsers>(); var onlineUserSvr = context.RequestServices.GetRequiredService<IOnlineUsers>();
var clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "::1"; var proxy = new Func<OnlineUserCache, Action, OnlineUserCache>((c, action) =>
onlineUsers.AddOrUpdate(clientIp, key =>
{
var ou = new OnlineUser();
ou.Ip = clientIp;
ou.UserName = context.User.Identity.Name;
ou.FirstAccessTime = DateTime.Now;
ou.LastAccessTime = DateTime.Now;
ou.Method = context.Request.Method;
ou.RequestUrl = context.Request.Path;
ou.AddRequestUrl(context.Request.Path);
return ou;
}, (key, v) =>
{ {
var v = c.User;
v.UserName = context.User.Identity.Name; v.UserName = context.User.Identity.Name;
if (!v.UserName.IsNullOrEmpty()) v.DisplayName = UserHelper.RetrieveUserByUserName(v.UserName).DisplayName;
v.LastAccessTime = DateTime.Now; v.LastAccessTime = DateTime.Now;
v.Method = context.Request.Method; v.Method = context.Request.Method;
v.RequestUrl = context.Request.Path; v.RequestUrl = context.Request.Path;
v.AddRequestUrl(context.Request.Path); v.AddRequestUrl(context.Request.Path);
return v; action?.Invoke();
return c;
}); });
onlineUserSvr.AddOrUpdate(context.Connection.Id ?? "", key =>
{
var agent = new UserAgent(context.Request.Headers["User-Agent"]);
var v = new OnlineUser();
v.ConnectionId = key;
v.Ip = context.Connection.RemoteIpAddress?.ToString();
v.Location = onlineUserSvr.RetrieveLocaleByIp(v.Ip);
v.Browser = $"{agent.Browser.Name} {agent.Browser.Version}";
v.OS = $"{agent.OS.Name} {agent.OS.Version}";
v.FirstAccessTime = DateTime.Now;
return proxy(new OnlineUserCache(v, () => onlineUserSvr.TryRemove(key, out _)), null);
}, (key, v) => proxy(v, () => v.Reset()));
}); });
await next(); await next();
})); }));

View File

@ -8,6 +8,11 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary> /// </summary>
public static class OnlineUsersServicesCollectionExtensions public static class OnlineUsersServicesCollectionExtensions
{ {
/// <summary>
///
/// </summary>
internal const string IPSvrHttpClientName = "IPSvr";
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@ -16,6 +21,10 @@ namespace Microsoft.Extensions.DependencyInjection
public static IServiceCollection AddOnlineUsers(this IServiceCollection services) public static IServiceCollection AddOnlineUsers(this IServiceCollection services)
{ {
services.TryAddSingleton<IOnlineUsers, DefaultOnlineUsers>(); services.TryAddSingleton<IOnlineUsers, DefaultOnlineUsers>();
services.AddHttpClient(IPSvrHttpClientName, client =>
{
client.DefaultRequestHeaders.Connection.Add("keep-alive");
});
return services; return services;
} }
} }

View File

@ -49,6 +49,7 @@
} }
} }
], ],
"IPSvrUrl": "http://api.map.baidu.com/location/ip?ak=6lvVPMDlm2gjLpU0aiqPsHXi2OiwGQRj&ip=",
"SwaggerPathBase": "/BA", "SwaggerPathBase": "/BA",
"AllowOrigins": "http://localhost,http://10.15.63.218", "AllowOrigins": "http://localhost,http://10.15.63.218",
"KeyPath": "..\\keys", "KeyPath": "..\\keys",

View File

@ -14,16 +14,22 @@
return options.pageSize * (options.pageNumber - 1) + index + 1; return options.pageSize * (options.pageNumber - 1) + index + 1;
} }
}, },
{
title: "会话Id", field: "ConnectionId"
},
{ title: "登陆名称", field: "UserName" }, { title: "登陆名称", field: "UserName" },
{ title: "显示名称", field: "DisplayName" }, { title: "显示名称", field: "DisplayName" },
{ title: "登录时间", field: "FirstAccessTime" }, { title: "登录时间", field: "FirstAccessTime" },
{ title: "最近操作时间", field: "LastAccessTime" }, { title: "最近操作时间", field: "LastAccessTime" },
{ title: "请求方式", field: "Method" }, { title: "请求方式", field: "Method" },
{ title: "IP地址", field: "Ip" }, { title: "主机", field: "Ip" },
{ title: "登录地点", field: "Location" },
{ title: "浏览器", field: "Browser" },
{ title: "操作系统", field: "OS" },
{ title: "访问地址", field: "RequestUrl" }, { title: "访问地址", field: "RequestUrl" },
{ {
title: "历史地址", field: "Ip", formatter: function (value, row, index, field) { title: "历史地址", field: "ConnectionId", formatter: function (value, row, index, field) {
return $.format('<button type="button" class="btn btn-info" data-id="{0}" data-toggle="popover" data-trigger="focus" data-html="true" data-title="访问记录">明细</button >', value); return $.format('<button type="button" class="btn btn-info" data-id="{0}" data-toggle="popover" data-trigger="focus" data-html="true" data-title="访问记录">明细</button>', value);
} }
} }
] ]
@ -38,9 +44,11 @@
var content = result.map(function (item) { var content = result.map(function (item) {
return $.format("<tr><td>{0}</td><td>{1}</td></tr>", item.Key, item.Value); return $.format("<tr><td>{0}</td><td>{1}</td></tr>", item.Key, item.Value);
}).join(''); }).join('');
content = $.format('<div class="fixed-table-container"><table class="table table-hover table-sm mb-0"><thead><tr><th class="p-1"><b>访问时间</b></th><th class="p-1">访问地址</th></tr></thead><tbody>{0}</tbody></table></div>', content); content = content === '' ?
$this.lgbPopover({ content: content, placement: $(window).width() < 768 ? 'top' : 'left' }); '已断开' :
$this.popover('show'); $.format('<div class="fixed-table-container"><table class="table table-hover table-sm mb-0"><thead><tr><th class="p-1"><b>访问时间</b></th><th class="p-1">访问地址</th></tr></thead><tbody>{0}</tbody></table></div>', content);
$this.popover({ content: content, placement: $(window).width() < 768 ? 'top' : 'left' });
$this.trigger('focus');
} }
}); });
} }

View File

@ -14,7 +14,7 @@
<PackageReference Include="Bootstrap.Security.DataAccess" Version="2.1.0" /> <PackageReference Include="Bootstrap.Security.DataAccess" Version="2.1.0" />
<PackageReference Include="Longbow.Cache" Version="2.2.3" /> <PackageReference Include="Longbow.Cache" Version="2.2.3" />
<PackageReference Include="Longbow.Data" Version="2.2.6" /> <PackageReference Include="Longbow.Data" Version="2.2.6" />
<PackageReference Include="Longbow.Web" Version="2.2.4" /> <PackageReference Include="Longbow.Web" Version="2.2.5" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -12,7 +12,6 @@
<PackageReference Include="Bootstrap.Security.Mvc" Version="2.2.3" /> <PackageReference Include="Bootstrap.Security.Mvc" Version="2.2.3" />
<PackageReference Include="Longbow.Cache" Version="2.2.3" /> <PackageReference Include="Longbow.Cache" Version="2.2.3" />
<PackageReference Include="Longbow.Logging" Version="2.2.5" /> <PackageReference Include="Longbow.Logging" Version="2.2.5" />
<PackageReference Include="Longbow.Web" Version="2.2.4" />
<PackageReference Include="Microsoft.AspNetCore.App" /> <PackageReference Include="Microsoft.AspNetCore.App" />
</ItemGroup> </ItemGroup>

View File

@ -14,7 +14,7 @@
<PackageReference Include="Bootstrap.Security.DataAccess" Version="2.1.0" /> <PackageReference Include="Bootstrap.Security.DataAccess" Version="2.1.0" />
<PackageReference Include="Longbow.Data" Version="2.2.6" /> <PackageReference Include="Longbow.Data" Version="2.2.6" />
<PackageReference Include="Longbow.Security.Cryptography" Version="1.3.0" /> <PackageReference Include="Longbow.Security.Cryptography" Version="1.3.0" />
<PackageReference Include="Longbow.Web" Version="2.2.4" /> <PackageReference Include="Longbow.Web" Version="2.2.5" />
<PackageReference Include="Longbow.Cache" Version="2.2.3" /> <PackageReference Include="Longbow.Cache" Version="2.2.3" />
<PackageReference Include="Longbow" Version="2.2.7" /> <PackageReference Include="Longbow" Version="2.2.7" />
</ItemGroup> </ItemGroup>

View File

@ -1,7 +1,6 @@
using Bootstrap.Security; using Bootstrap.Security;
using Longbow.Cache; using Longbow.Cache;
using Longbow.Data; using Longbow.Data;
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Bootstrap.DataAccess namespace Bootstrap.DataAccess

View File

@ -73,7 +73,12 @@ namespace Bootstrap.Admin
/// </summary> /// </summary>
/// <param name="baseAddress"></param> /// <param name="baseAddress"></param>
/// <returns></returns> /// <returns></returns>
public HttpClient CreateClient(string baseAddress) => CreateDefaultClient(new Uri($"http://localhost/{baseAddress}/"), new RedirectHandler(7), new CookieContainerHandler(_cookie)); public HttpClient CreateClient(string baseAddress)
{
var client = CreateDefaultClient(new Uri($"http://localhost/{baseAddress}/"), new RedirectHandler(7), new CookieContainerHandler(_cookie));
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.1 Safari/605.1.15");
return client;
}
private readonly CookieContainer _cookie = new CookieContainer(); private readonly CookieContainer _cookie = new CookieContainer();

View File

@ -11,15 +11,15 @@ namespace Bootstrap.Admin.Api
[Fact] [Fact]
public async void Post_Ok() public async void Post_Ok()
{ {
var usres = await Client.PostAsJsonAsync<string, IEnumerable<OnlineUser>>(string.Empty); var users = await Client.PostAsJsonAsync<string, IEnumerable<OnlineUser>>(string.Empty);
Assert.Single(usres); Assert.Single(users);
} }
[Fact] [Fact]
public async void Get_Ok() public async void Get_Ok()
{ {
var urls = await Client.GetAsJsonAsync<IEnumerable<KeyValuePair<DateTime, string>>>("::1"); var urls = await Client.GetAsJsonAsync<IEnumerable<KeyValuePair<DateTime, string>>>("UnitTest");
Assert.NotEmpty(urls); Assert.Empty(urls);
} }
} }
} }