diff --git a/backend/IM_API/Dtos/ClearConversationsDto.cs b/backend/IM_API/Dtos/ClearConversationsDto.cs new file mode 100644 index 0000000..0d8c5c9 --- /dev/null +++ b/backend/IM_API/Dtos/ClearConversationsDto.cs @@ -0,0 +1,26 @@ +namespace IM_API.Dtos +{ + public class ClearConversationsDto + { + public int UserId { get; set; } + /// + /// 聊天类型 + /// + public ChatType ChatType { get; set; } + /// + /// 目标ID,聊天类型为群则为群id,私聊为用户id + /// + public int TargetId { get; set; } + } + public enum ChatType + { + /// + /// 私聊 + /// + single = 0, + /// + /// 私聊 + /// + group = 1 + } +} diff --git a/backend/IM_API/Dtos/HandleFriendRequestDto.cs b/backend/IM_API/Dtos/HandleFriendRequestDto.cs new file mode 100644 index 0000000..637871d --- /dev/null +++ b/backend/IM_API/Dtos/HandleFriendRequestDto.cs @@ -0,0 +1,25 @@ +namespace IM_API.Dtos +{ + public class HandleFriendRequestDto + { + /// + /// 好友请求Id + /// + public int RequestId { get; set; } + /// + /// 处理操作 + /// + public HandleFriendRequestAction Action { get; set; } + } + public enum HandleFriendRequestAction + { + /// + /// 同意 + /// + Accept = 0, + /// + /// 拒绝 + /// + Reject = 1 + } +} diff --git a/backend/IM_API/IM_API.csproj b/backend/IM_API/IM_API.csproj index 1f6fe1f..dfcdc88 100644 --- a/backend/IM_API/IM_API.csproj +++ b/backend/IM_API/IM_API.csproj @@ -10,6 +10,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -19,7 +20,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/backend/IM_API/Interface/Services/IConversationService.cs b/backend/IM_API/Interface/Services/IConversationService.cs new file mode 100644 index 0000000..9c4bf63 --- /dev/null +++ b/backend/IM_API/Interface/Services/IConversationService.cs @@ -0,0 +1,21 @@ +using IM_API.Dtos; +using IM_API.Models; + +namespace IM_API.Interface.Services +{ + public interface IConversationService + { + /// + /// 清除消息会话 + /// + /// + /// + Task ClearConversationsAsync(ClearConversationsDto clearConversationsDto); + /// + /// 获取用户当前消息会话 + /// + /// 用户id + /// + Task GetConversationsAsync(int userId); + } +} diff --git a/backend/IM_API/Interface/Services/IFriendSerivce.cs b/backend/IM_API/Interface/Services/IFriendSerivce.cs index 2b5fc3a..4f0bfa2 100644 --- a/backend/IM_API/Interface/Services/IFriendSerivce.cs +++ b/backend/IM_API/Interface/Services/IFriendSerivce.cs @@ -28,5 +28,37 @@ namespace IM_API.Interface.Services /// /// Task GetFriendRequestListAsync(int userId,bool isReceived,int page,int limit); + /// + /// 处理好友请求 + /// + /// + /// + Task HandleFriendRequestAsync(HandleFriendRequestDto requestDto); + /// + /// 通过用户Id删除好友关系 + /// + /// 操作用户Id + /// 被删除用户ID + /// + Task DeleteFriendByUserIdAsync(int userId,int toUserId); + /// + /// 通过好友关系Id删除好友关系 + /// + /// 好友关系id + /// + Task DeleteFriendAsync(int friendId); + /// + /// 通过用户Id拉黑好友关系 + /// + /// 操作用户Id + /// 被拉黑用户ID + /// + Task BlockFriendByUserIdAsync(int userId, int toUserId); + /// + /// 通过好友关系Id拉黑好友关系 + /// + /// 好友关系id + /// + Task BlockeFriendAsync(int friendId); } } diff --git a/backend/IM_API/Interface/Services/IJWTService.cs b/backend/IM_API/Interface/Services/IJWTService.cs new file mode 100644 index 0000000..202d444 --- /dev/null +++ b/backend/IM_API/Interface/Services/IJWTService.cs @@ -0,0 +1,21 @@ +using System.Security.Claims; + +namespace IM_API.Interface.Services +{ + public interface IJWTService + { + /// + /// 生成用户凭证 + /// + /// 负载 + /// 过期时间 + /// + string GenerateAccessToken(IEnumerable claims, DateTime expiresAt); + /// + /// 创建用户凭证 + /// + /// + /// + (string token, DateTime expiresAt) CreateAccessTokenForUser(int userId,string username,string role); + } +} diff --git a/backend/IM_API/Interface/Services/IRefreshTokenService.cs b/backend/IM_API/Interface/Services/IRefreshTokenService.cs new file mode 100644 index 0000000..3133fc9 --- /dev/null +++ b/backend/IM_API/Interface/Services/IRefreshTokenService.cs @@ -0,0 +1,27 @@ +namespace IM_API.Interface.Services +{ + public interface IRefreshTokenService + { + /// + /// 创建刷新令牌 + /// + /// + /// + /// + Task CreateRefreshTokenAsync(int userId, CancellationToken ct = default); + /// + /// 验证刷新令牌 + /// + /// 刷新令牌 + /// + /// + Task<(bool ok, int userId)> ValidateRefreshTokenAsync(string token, CancellationToken ct = default); + /// + /// 删除更新令牌 + /// + /// 刷新令牌 + /// + /// + Task RevokeRefreshTokenAsync(string token, CancellationToken ct = default); + } +} diff --git a/backend/IM_API/Program.cs b/backend/IM_API/Program.cs index 2ac6eae..52e576e 100644 --- a/backend/IM_API/Program.cs +++ b/backend/IM_API/Program.cs @@ -1,7 +1,11 @@ using IM_API.Configs; using IM_API.Models; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using StackExchange.Redis; +using System.Text; namespace IM_API { @@ -16,14 +20,74 @@ namespace IM_API .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build(); - string conStr = builder.Configuration.GetConnectionString("DefaultConnection")!; - + string conStr = configuration.GetConnectionString("DefaultConnection")!; + string redisConStr = configuration.GetConnectionString("Redis"); + //עݿ builder.Services.AddDbContext(options => { options.UseMySql(conStr,ServerVersion.AutoDetect(conStr)); }); + //עredis + var redis = ConnectionMultiplexer.Connect(redisConStr); + builder.Services.AddSingleton(redis); builder.Services.AddAllService(configuration); + + builder.Services.AddSignalR(); + //Դ + builder.Services.AddCors(options => + { + options.AddDefaultPolicy(policy => + { + policy.AllowAnyHeader() + .AllowAnyMethod() + .AllowAnyHeader() + .AllowAnyOrigin() + .AllowAnyOrigin(); + }); + }); + //ƾ֤ + builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + //httpsDZ + options.RequireHttpsMetadata = false; + //token + options.SaveToken = true; + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + //֤ǩ + ValidateIssuer = true, + ValidIssuer = configuration["Jwt:Issuer"], + //֤ + ValidateAudience = true, + ValidAudience = configuration["Jwt:Audience"], + //֤ǩԿ + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"])), + //ʱƫ + ValidateLifetime = true, + ClockSkew = TimeSpan.FromSeconds(30) + + + }; + //websocket tokenƾ֤ + options.Events = new JwtBearerEvents { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + if (!string.IsNullOrEmpty(accessToken)) + { + context.Token = accessToken; + } + return Task.CompletedTask; + } + }; + }); builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); @@ -41,6 +105,7 @@ namespace IM_API app.UseHttpsRedirection(); app.UseAuthorization(); + app.UseAuthentication(); app.MapControllers(); diff --git a/backend/IM_API/Services/FriendService.cs b/backend/IM_API/Services/FriendService.cs index 82cb9a1..0ef256c 100644 --- a/backend/IM_API/Services/FriendService.cs +++ b/backend/IM_API/Services/FriendService.cs @@ -1,6 +1,10 @@ -namespace IM_API.Services +using IM_API.Models; +using Microsoft.EntityFrameworkCore; + +namespace IM_API.Services { public class FriendService { + } } diff --git a/backend/IM_API/Services/JWTService.cs b/backend/IM_API/Services/JWTService.cs new file mode 100644 index 0000000..f2be501 --- /dev/null +++ b/backend/IM_API/Services/JWTService.cs @@ -0,0 +1,55 @@ +using IM_API.Interface.Services; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace IM_API.Services +{ + public class JWTService : IJWTService + { + private readonly IConfiguration _config; + private readonly string _key; + private readonly string _issuer; + private readonly string _audience; + private readonly int _accessMinutes; + + public JWTService(IConfiguration config) + { + _config = config; + _key = _config["Jwt:Key"]!; + _issuer = _config["Jwt:Issuer"]!; + _audience = _config["Jwt:Audience"]!; + _accessMinutes = int.Parse(_config["Jwt:AccessTokenMinutes"] ?? "15"); + } + + public string GenerateAccessToken(IEnumerable claims, DateTime expiresAt) + { + var keyBytes = Encoding.UTF8.GetBytes(_key); + var creds = new SigningCredentials(new SymmetricSecurityKey(keyBytes), SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: _issuer, + audience: _audience, + claims: claims, + expires: expiresAt, + signingCredentials: creds + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + public (string token, DateTime expiresAt) CreateAccessTokenForUser(int userId, string username, string role) + { + var expiresAt = DateTime.UtcNow.AddMinutes(_accessMinutes); + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()), + new Claim(ClaimTypes.Name, username), + new Claim(ClaimTypes.Role, role) + }; + var token = GenerateAccessToken(claims, expiresAt); + return (token, expiresAt); + } + } +} diff --git a/backend/IM_API/Services/RedisRefreshTokenService.cs b/backend/IM_API/Services/RedisRefreshTokenService.cs new file mode 100644 index 0000000..260041d --- /dev/null +++ b/backend/IM_API/Services/RedisRefreshTokenService.cs @@ -0,0 +1,66 @@ +using IM_API.Interface.Services; +using Microsoft.AspNetCore.Connections; +using Newtonsoft.Json; +using StackExchange.Redis; +using System.Numerics; +using System.Security.Cryptography; +using System.Text.Json; + +namespace IM_API.Services +{ + public class RedisRefreshTokenService : IRefreshTokenService + { + private readonly ILogger _logger; + //redis数据库 + private readonly IDatabase _db; + private IConfiguration configuration; + //过期时长 + private readonly TimeSpan _refreshTTL; + public RedisRefreshTokenService(ILogger logger, IConnectionMultiplexer multiplexer, IConfiguration configuration) + { + _logger = logger; + _db = multiplexer.GetDatabase(); + this.configuration = configuration; + //设置refresh过期时间 + var days = int.Parse(this.configuration["Jwt:RefreshTokenDays"] ?? "30"); + _refreshTTL = TimeSpan.FromDays(days); + } + + private static string GenerateTokenStr() + { + var bytes = RandomNumberGenerator.GetBytes(32); + return Convert.ToBase64String(bytes); + } + + public async Task CreateRefreshTokenAsync(int userId, CancellationToken ct = default) + { + string token = GenerateTokenStr(); + var payload = new { UserId = userId,CreateAt = DateTime.Now}; + string json = JsonConvert.SerializeObject(payload); + //token写入redis + await _db.StringSetAsync(token,json,_refreshTTL); + return token; + } + + public async Task RevokeRefreshTokenAsync(string token, CancellationToken ct = default) + { + await _db.KeyDeleteAsync(token); + } + + public async Task<(bool ok, int userId)> ValidateRefreshTokenAsync(string token, CancellationToken ct = default) + { + var json = await _db.StringGetAsync(token); + if (json.IsNullOrEmpty) return (false,-1); + try + { + var doc = JsonConvert.DeserializeObject(json); + var userId = doc.GetProperty("UserId").GetInt32(); + return (true,userId); + } + catch + { + return (false,-1); + } + } + } +} diff --git a/backend/IM_API/appsettings.json b/backend/IM_API/appsettings.json index d6e1b90..0b5706a 100644 --- a/backend/IM_API/appsettings.json +++ b/backend/IM_API/appsettings.json @@ -6,7 +6,15 @@ } }, "AllowedHosts": "*", + "Jwt": { + "Key": "change_this_super_secret_key_in_prod", + "Issuer": "IMDemo", + "Audience": "IMClients", + "AccessTokenMinutes": 15, + "RefreshTokenDays": 30 + }, "ConnectionStrings": { - "DefaultConnection": "Server=frp-era.com;Port=26582;Database=IM;User=product;Password=12345678;" + "DefaultConnection": "Server=frp-era.com;Port=26582;Database=IM;User=product;Password=12345678;", + "Redis": "192.168.5.100:6379" } } diff --git a/frontend/web/src/App.vue b/frontend/web/src/App.vue index 59f2b39..4550d52 100644 --- a/frontend/web/src/App.vue +++ b/frontend/web/src/App.vue @@ -3,10 +3,8 @@ diff --git a/frontend/web/src/components/Window.vue b/frontend/web/src/components/Window.vue index 874c1aa..f8678e5 100644 --- a/frontend/web/src/components/Window.vue +++ b/frontend/web/src/components/Window.vue @@ -23,12 +23,25 @@ @@ -42,9 +55,15 @@ \ No newline at end of file + diff --git a/frontend/web/src/views/auth/Login.vue b/frontend/web/src/views/auth/Login.vue index f5667df..b2c7d98 100644 --- a/frontend/web/src/views/auth/Login.vue +++ b/frontend/web/src/views/auth/Login.vue @@ -6,10 +6,22 @@
即时通讯
@@ -34,19 +46,55 @@