Merge pull request !3 from Argo-QQ/dev-#IS60Z
This commit is contained in:
commit
e842592481
|
@ -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" />
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using Bootstrap.Security;
|
||||
using Longbow.Cache;
|
||||
using Longbow.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bootstrap.DataAccess
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue