增强token的安全性 防止盗用
This commit is contained in:
parent
d314597e8d
commit
94642457a8
6 changed files with 84 additions and 35 deletions
|
@ -3,6 +3,7 @@ using iFileProxy.Helpers;
|
||||||
using iFileProxy.Models;
|
using iFileProxy.Models;
|
||||||
using iFileProxy.Services;
|
using iFileProxy.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace iFileProxy.Controllers
|
namespace iFileProxy.Controllers
|
||||||
{
|
{
|
||||||
|
@ -50,7 +51,16 @@ namespace iFileProxy.Controllers
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||||
var (success, token, message) = await _authService.LoginAsync(request.Username, request.Password, ip);
|
var userAgent = HttpContext.Request.Headers["User-Agent"].FirstOrDefault() ?? "unknown";
|
||||||
|
var fingerprint = HttpContext.Request.Headers["X-Device-Fingerprint"].FirstOrDefault() ?? "unknown";
|
||||||
|
|
||||||
|
var (success, token, message) = await _authService.LoginAsync(
|
||||||
|
request.Username,
|
||||||
|
request.Password,
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
fingerprint
|
||||||
|
);
|
||||||
|
|
||||||
return Ok(new CommonRsp
|
return Ok(new CommonRsp
|
||||||
{
|
{
|
||||||
|
|
|
@ -19,40 +19,65 @@ namespace iFileProxy.Middleware
|
||||||
public async Task InvokeAsync(HttpContext context)
|
public async Task InvokeAsync(HttpContext context)
|
||||||
{
|
{
|
||||||
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
|
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
|
||||||
|
var fingerprint = context.Request.Headers["X-Device-Fingerprint"].FirstOrDefault();
|
||||||
|
var userAgent = context.Request.Headers["User-Agent"].FirstOrDefault();
|
||||||
|
var ip = context.Connection.RemoteIpAddress?.ToString();
|
||||||
|
|
||||||
if (token != null)
|
if (token != null)
|
||||||
await AttachUserToContext(context, token);
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Key"]);
|
||||||
|
|
||||||
|
// 验证token
|
||||||
|
tokenHandler.ValidateToken(token, new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(key),
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidIssuer = _configuration["Jwt:Issuer"],
|
||||||
|
ValidAudience = _configuration["Jwt:Audience"],
|
||||||
|
ClockSkew = TimeSpan.Zero
|
||||||
|
}, out SecurityToken validatedToken);
|
||||||
|
|
||||||
|
var jwtToken = (JwtSecurityToken)validatedToken;
|
||||||
|
|
||||||
|
// 验证客户端信息
|
||||||
|
var tokenFingerprint = jwtToken.Claims.First(x => x.Type == "fingerprint").Value;
|
||||||
|
var tokenUserAgent = jwtToken.Claims.First(x => x.Type == "userAgent").Value;
|
||||||
|
var tokenIp = jwtToken.Claims.First(x => x.Type == "ip").Value;
|
||||||
|
|
||||||
|
// 如果客户端信息不匹配,则拒绝访问
|
||||||
|
if (fingerprint != tokenFingerprint ||
|
||||||
|
userAgent != tokenUserAgent ||
|
||||||
|
(ip != tokenIp && !IsLocalNetwork(ip, tokenIp))) // 允许本地网络IP变化
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 401;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = jwtToken.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
|
||||||
|
context.Items["User"] = userId;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// token验证失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await _next(context);
|
await _next(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AttachUserToContext(HttpContext context, string token)
|
private bool IsLocalNetwork(string? ip1, string? ip2)
|
||||||
{
|
{
|
||||||
try
|
if (string.IsNullOrEmpty(ip1) || string.IsNullOrEmpty(ip2)) return false;
|
||||||
{
|
|
||||||
var tokenHandler = new JwtSecurityTokenHandler();
|
|
||||||
var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Key"]);
|
|
||||||
tokenHandler.ValidateToken(token, new TokenValidationParameters
|
|
||||||
{
|
|
||||||
ValidateIssuerSigningKey = true,
|
|
||||||
IssuerSigningKey = new SymmetricSecurityKey(key),
|
|
||||||
ValidateIssuer = true,
|
|
||||||
ValidateAudience = true,
|
|
||||||
ValidIssuer = _configuration["Jwt:Issuer"],
|
|
||||||
ValidAudience = _configuration["Jwt:Audience"],
|
|
||||||
ClockSkew = TimeSpan.Zero
|
|
||||||
}, out SecurityToken validatedToken);
|
|
||||||
|
|
||||||
var jwtToken = (JwtSecurityToken)validatedToken;
|
// 检查是否都是内网IP
|
||||||
var userId = jwtToken.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
|
return (ip1.StartsWith("192.168.") && ip2.StartsWith("192.168.")) ||
|
||||||
|
(ip1.StartsWith("10.") && ip2.StartsWith("10.")) ||
|
||||||
// 将用户信息附加到上下文中
|
(ip1.StartsWith("172.") && ip2.StartsWith("172."));
|
||||||
context.Items["User"] = userId;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// 令牌验证失败,不附加用户信息
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
6
src/Models/ClientInfo.cs
Normal file
6
src/Models/ClientInfo.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
public class ClientInfo
|
||||||
|
{
|
||||||
|
public string IP { get; set; } = string.Empty;
|
||||||
|
public string UserAgent { get; set; } = string.Empty;
|
||||||
|
public string Fingerprint { get; set; } = string.Empty; // 浏览器指纹
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ namespace iFileProxy
|
||||||
builder =>
|
builder =>
|
||||||
{
|
{
|
||||||
builder
|
builder
|
||||||
.WithOrigins("http://localhost:3000", "http://admin.gitdl.cn", "https://admin.gitdl.cn","http://47.243.56.137:50050")
|
.WithOrigins("http://localhost:3000", "http://admin.gitdl.cn", "https://admin.gitdl.cn","http://47.243.56.137:50050", "http://localhost:4173")
|
||||||
.AllowAnyMethod()
|
.AllowAnyMethod()
|
||||||
.AllowAnyHeader()
|
.AllowAnyHeader()
|
||||||
.AllowCredentials();
|
.AllowCredentials();
|
||||||
|
|
|
@ -74,7 +74,7 @@
|
||||||
loggerConfiguration.WriteTo.Sink(
|
loggerConfiguration.WriteTo.Sink(
|
||||||
new DatabaseLogSink(
|
new DatabaseLogSink(
|
||||||
_dbGateService,
|
_dbGateService,
|
||||||
LogEventLevel.Error
|
LogEventLevel.Warning
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,7 @@ namespace iFileProxy.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(bool success, string token, string message)> LoginAsync(string account, string password, string ip)
|
public async Task<(bool success, string token, string message)> LoginAsync(string account, string password, string ip, string userAgent, string fingerprint)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -102,8 +102,13 @@ namespace iFileProxy.Services
|
||||||
};
|
};
|
||||||
await _dbGateService.CreateUserEventAsync(userEvent);
|
await _dbGateService.CreateUserEventAsync(userEvent);
|
||||||
|
|
||||||
// 生成JWT令牌
|
// 生成包含客户端信息的token
|
||||||
var token = GenerateJwtToken(user);
|
var token = GenerateJwtToken(user, new ClientInfo
|
||||||
|
{
|
||||||
|
IP = ip,
|
||||||
|
UserAgent = userAgent,
|
||||||
|
Fingerprint = fingerprint
|
||||||
|
});
|
||||||
|
|
||||||
return (true, token, "登录成功");
|
return (true, token, "登录成功");
|
||||||
}
|
}
|
||||||
|
@ -114,13 +119,16 @@ namespace iFileProxy.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GenerateJwtToken(User user)
|
private string GenerateJwtToken(User user, ClientInfo clientInfo)
|
||||||
{
|
{
|
||||||
var claims = new[]
|
var claims = new[]
|
||||||
{
|
{
|
||||||
new Claim(ClaimTypes.NameIdentifier, user.UserId),
|
new Claim(ClaimTypes.NameIdentifier, user.UserId),
|
||||||
new Claim(ClaimTypes.Name, user.Username),
|
new Claim(ClaimTypes.Name, user.Username),
|
||||||
new Claim(ClaimTypes.Role, user.Mask.ToString())
|
new Claim(ClaimTypes.Role, user.Mask.ToString()),
|
||||||
|
new Claim("ip", clientInfo.IP),
|
||||||
|
new Claim("userAgent", clientInfo.UserAgent),
|
||||||
|
new Claim("fingerprint", clientInfo.Fingerprint)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 确保密钥至少32字节长
|
// 确保密钥至少32字节长
|
||||||
|
|
Loading…
Reference in a new issue