增加功能:在线用户增加客户端浏览器与操作系统信息,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>
<PackageReference Include="Bootstrap.Security.Mvc" Version="2.2.3" />
<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.Mvc.Versioning" Version="3.1.2" />
<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()]
public IEnumerable<OnlineUser> Post([FromServices]IOnlineUsers onlineUSers)
{
return onlineUSers.OnlineUsers;
return onlineUSers.OnlineUsers.OrderByDescending(u => u.LastAccessTime);
}
/// <summary>
/// 获取指定IP地址的在线用户请求地址明细数据
/// 获取指定会话的在线用户请求地址明细数据
/// </summary>
/// <param name="id"></param>
/// <param name="onlineUSers"></param>
@ -31,7 +31,7 @@ namespace Bootstrap.Admin.Controllers.Api
[HttpGet("{id}")]
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];
}
}

View File

@ -1,6 +1,10 @@
using System;
using Longbow.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
namespace Bootstrap.Admin
{
@ -9,7 +13,17 @@ namespace Bootstrap.Admin
/// </summary>
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>
///
@ -17,7 +31,7 @@ namespace Bootstrap.Admin
/// <returns></returns>
public IEnumerable<OnlineUser> OnlineUsers
{
get { return _onlineUsers.Values; }
get { return _onlineUsers.Values.Select(v => v.User); }
}
/// <summary>
@ -27,6 +41,45 @@ namespace Bootstrap.Admin
/// <param name="addValueFactory"></param>
/// <param name="updateValueFactory"></param>
/// <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="updateValueFactory"></param>
/// <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>>();
/// <summary>
///
/// </summary>
public string ConnectionId { get; set; }
/// <summary>
///
/// </summary>
public string UserName { get; set; }
/// <summary>
///
/// </summary>
public string DisplayName { get; set; }
/// <summary>
///
/// </summary>
@ -27,6 +37,11 @@ namespace Bootstrap.Admin
/// </summary>
public DateTime LastAccessTime { get; set; }
/// <summary>
///
/// </summary>
public string Location { get; set; }
/// <summary>
///
/// </summary>
@ -37,6 +52,16 @@ namespace Bootstrap.Admin
/// </summary>
public string Ip { get; set; }
/// <summary>
///
/// </summary>
public string Browser { get; set; }
/// <summary>
///
/// </summary>
public string OS { get; set; }
/// <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.DataAccess;
using Longbow.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Builder
{
@ -19,30 +20,33 @@ namespace Microsoft.AspNetCore.Builder
/// <returns></returns>
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 clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "::1";
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 onlineUserSvr = context.RequestServices.GetRequiredService<IOnlineUsers>();
var proxy = new Func<OnlineUserCache, Action, OnlineUserCache>((c, action) =>
{
var v = c.User;
v.UserName = context.User.Identity.Name;
if (!v.UserName.IsNullOrEmpty()) v.DisplayName = UserHelper.RetrieveUserByUserName(v.UserName).DisplayName;
v.LastAccessTime = DateTime.Now;
v.Method = context.Request.Method;
v.RequestUrl = 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();
}));

View File

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

View File

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

View File

@ -14,16 +14,22 @@
return options.pageSize * (options.pageNumber - 1) + index + 1;
}
},
{
title: "会话Id", field: "ConnectionId"
},
{ title: "登陆名称", field: "UserName" },
{ title: "显示名称", field: "DisplayName" },
{ title: "登录时间", field: "FirstAccessTime" },
{ title: "最近操作时间", field: "LastAccessTime" },
{ title: "请求方式", field: "Method" },
{ title: "IP地址", field: "Ip" },
{ title: "主机", field: "Ip" },
{ title: "登录地点", field: "Location" },
{ title: "浏览器", field: "Browser" },
{ title: "操作系统", field: "OS" },
{ title: "访问地址", field: "RequestUrl" },
{
title: "历史地址", field: "Ip", 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);
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);
}
}
]
@ -38,9 +44,11 @@
var content = result.map(function (item) {
return $.format("<tr><td>{0}</td><td>{1}</td></tr>", item.Key, item.Value);
}).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);
$this.lgbPopover({ content: content, placement: $(window).width() < 768 ? 'top' : 'left' });
$this.popover('show');
content = 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);
$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="Longbow.Cache" Version="2.2.3" />
<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>
</Project>

View File

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

View File

@ -14,7 +14,7 @@
<PackageReference Include="Bootstrap.Security.DataAccess" Version="2.1.0" />
<PackageReference Include="Longbow.Data" Version="2.2.6" />
<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" Version="2.2.7" />
</ItemGroup>

View File

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

View File

@ -73,7 +73,12 @@ namespace Bootstrap.Admin
/// </summary>
/// <param name="baseAddress"></param>
/// <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();

View File

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