我有一个 ASP.NET Core 6.0 Web API,它接收来自 Angular Web UI 应用程序的请求。目前我已经按照以下方式实现了 JWT 令牌授权:[https://www.c-sharpcorner.com/article/jwt-token-authentication-and-authorizations-in-net-core-6-0-web-api/ ][1].
我想要实现的目标是:用户应该仅登录单个设备。如果同一用户尝试从第二个设备登录,则应从第一个设备注销,然后登录到第二个设备。来自第一台设备的所有 API 请求都应未经授权。
要实现这一点:
根据我的理解,第2点可以使用Web API中的自定义授权过滤器来植入。任何人都可以建议流程是否正确,如果有这种实现的示例,那就太好了。
任何人都可以建议流程是否正确,如果有的话那就太好了 这种实现的示例
好吧,如果您能够分享一些现有的代码片段,那么您目前的情况会更好。
然而,这个过程有点争议,因为它可以通过多种方式实现。
我不太确定您的身份验证是什么样的。但我试图分享如何使用会话和 asp.net core Identity 来实现它。
正如我已经说过的,正如有人可能会说的那样,这个过程有点争论,他们可以使用用户声明或使用数据库交互来做到这一点。
即使可能存在争论,我将如何定义设备的唯一性,所以我使用了一个类,在其中考虑用户浏览器信息、MAC 地址、UserId 或 IP。
使用上述两者中的任何一个,我认为我们可以定义设备身份。
好吧,让我们看看如何实现:
我是如何规划算法的:
首先,我发送用户电子邮件和登录密码,我在其中获取用户浏览器信息、用户 MAC 和 IP 地址。
我们来看看:
private async Task<DeviceInfo> GetCurrentDeviceIdentifier(HttpContext httpContext)
{
var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
using (var client = new HttpClient())
{
var response = await client.GetStringAsync("https://api64.ipify.org?format=json");
var ipAddress = JObject.Parse(response)["ip"].ToString();
var combinedIdentifier = $"{ipAddress}_{userAgent}";
//var hashedIdentifier = ComputeHash(combinedIdentifier);
var _deviceInfo = new DeviceInfo();
_deviceInfo.BrowserInfo = userAgent;
_deviceInfo.MAC = GetMacAddress();
return _deviceInfo;
}
}
注意:为了更好的安全性,我们可以将设备信息放入哈希中。但我要跳过演示。
检查用户登录:
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
string? userId = _userManager.GetUserId(User);
DeviceInfo deviceIdentifier = await GetCurrentDeviceIdentifier(HttpContext);
deviceIdentifier.UserId = userId!;
var signInStatus = await GetDeviceIdInSession(deviceIdentifier);
if (signInStatus == "Authorized")
{
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}
else
{
ModelState.AddModelError(string.Empty, $"Invalid login attempt. User already login in device {deviceIdentifier.MAC} browser info: {deviceIdentifier.BrowserInfo} ");
return Page();
}
在会话中保留设备信息:
private async Task<bool> SetDeviceInfoInSession(DeviceInfo deviceInfo)
{
var userDevice = HttpContext.Session.GetComplexObjectSession<DeviceInfo>("DeviceInfo");
if (userDevice == null)
{
HttpContext.Session.SetComplexObjectSession("DeviceInfo", deviceInfo);
return true;
}
else
{
return false;
}
}
private async Task<string> GetDeviceIdInSession(DeviceInfo deviceInfo)
{
var loginUserDevice = HttpContext.Session.GetComplexObjectSession<DeviceInfo>("DeviceInfo");
string authStatus = "";
if (loginUserDevice == null)
{
await SetDeviceInfoInSession(deviceInfo);
authStatus = "Authorized";
}
else
{
if (deviceInfo.MAC == loginUserDevice!.MAC || deviceInfo.BrowserInfo == loginUserDevice!.BrowserInfo)
{
authStatus = "Unauthorized";
}
else
{
authStatus = "Authorized";
}
}
return authStatus;
}
我用过的课程:
public class DeviceInfo
{
public string UserId { get; set; }
public string? MAC { get; set; }
public string? BrowserInfo { get; set; }
}
注意:如果需要任何其他信息,您可以修改课程
会话处理程序方法:
public static class SessionExtension
{
//setting session
public static void SetComplexObjectSession(this ISession session, string key, object value)
{
session.SetString(key, JsonConvert.SerializeObject(value));
}
//getting session
public static T? GetComplexObjectSession<T>(this ISession session, string key)
{
var value = session.GetString(key);
return value == null ? default(T) : JsonConvert.DeserializeObject<T>(value);
}
}
程序.cs:
builder.Services.AddSession(options =>
{
options.Cookie.Name = "DeviceInfo";
options.IdleTimeout = TimeSpan.FromMinutes(3);
options.Cookie.IsEssential = true;
});
app.UseRouting();
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();
输出:
注意: 您还可以使用用户声明或基于策略的身份验证来做到这一点。另一件事是,它的讨论有点长,所以在一个问题中很难在没有相关代码片段的情况下解释所有内容。我建议您发布新问题以及您的具体问题和相关代码片段。希望你明白我的意思。