添加项目文件。
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.25" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\IM.Commons\IM.Commons.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace IM.Jwt
|
||||
{
|
||||
public interface ITokenService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取令牌
|
||||
/// </summary>
|
||||
/// <param name="claims">令牌负载</param>
|
||||
/// <param name="options"></param>
|
||||
/// <returns></returns>
|
||||
string GetToken(IEnumerable<Claim> claims, JwtOptions options);
|
||||
Task<string> CreateRefreshTokenAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||
Task RevokeRefreshTokenAsync(string refreshToken);
|
||||
Task<(bool ok, Guid userId)> ValidateRefreshTokenAsync(string token, CancellationToken cancellation = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace IM.Jwt
|
||||
{
|
||||
public class JwtOptions
|
||||
{
|
||||
public string Key { get; init; }
|
||||
public string Issuer { get; init; }
|
||||
public string Audience { get; init; }
|
||||
public int AccessTokenMinutes { get; init; }
|
||||
public int RefreshTokenDays { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using IM.Commons;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace IM.Jwt
|
||||
{
|
||||
public class ModuleInit : IModuleInitializer
|
||||
{
|
||||
public void Initialize(IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ITokenService, TokenService>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using IM.Commons;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Newtonsoft.Json;
|
||||
using StackExchange.Redis;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace IM.Jwt
|
||||
{
|
||||
public class TokenService : ITokenService
|
||||
{
|
||||
private readonly IDatabase _redis;
|
||||
private readonly IOptions<JwtOptions> _options;
|
||||
public TokenService(IConnectionMultiplexer multiplexer, IOptions<JwtOptions> options)
|
||||
{
|
||||
_redis = multiplexer.GetDatabase();
|
||||
_options = options;
|
||||
}
|
||||
|
||||
private static string GenerateTokenStr()
|
||||
{
|
||||
var bytes = RandomNumberGenerator.GetBytes(32);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
public async Task<string> CreateRefreshTokenAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string token = GenerateTokenStr();
|
||||
var payload = new { UserId = userId, CreateAt = DateTime.Now };
|
||||
string json = JsonConvert.SerializeObject(payload);
|
||||
//token写入redis
|
||||
await _redis.StringSetAsync(RedisHelper.GetRefreshTokenKey(token), json, TimeSpan.FromDays(_options.Value.RefreshTokenDays));
|
||||
return token;
|
||||
}
|
||||
|
||||
public string GetToken(IEnumerable<Claim> claims, JwtOptions options)
|
||||
{
|
||||
TimeSpan ExpiryDuration = TimeSpan.FromMinutes(options.AccessTokenMinutes);
|
||||
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(options.Key));
|
||||
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
|
||||
var tokenDescriptor = new JwtSecurityToken(options.Issuer, options.Audience, claims,
|
||||
expires: DateTime.Now.Add(ExpiryDuration), signingCredentials: credentials);
|
||||
return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
|
||||
}
|
||||
|
||||
public async Task RevokeRefreshTokenAsync(string refreshToken)
|
||||
{
|
||||
await _redis.KeyDeleteAsync(RedisHelper.GetRefreshTokenKey(refreshToken));
|
||||
}
|
||||
|
||||
public async Task<(bool ok, Guid userId)> ValidateRefreshTokenAsync(string token, CancellationToken cancellation = default)
|
||||
{
|
||||
var json = await _redis.StringGetAsync(RedisHelper.GetRefreshTokenKey(token));
|
||||
if (json.IsNullOrEmpty) return (false, Guid.Empty);
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json.ToString());
|
||||
var userId = doc.RootElement.GetProperty("UserId").GetGuid();
|
||||
return (true, userId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (false, Guid.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using IM.Commons;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
|
||||
namespace IM.Jwt
|
||||
{
|
||||
public static class WebApplicationJwtExtension
|
||||
{
|
||||
public static IServiceCollection AddJwt(this IServiceCollection services, JwtOptions jwtOptions)
|
||||
{
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = jwtOptions.Issuer,
|
||||
|
||||
ValidateAudience = true,
|
||||
ValidAudience = jwtOptions.Audience,
|
||||
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.Zero,
|
||||
|
||||
// 验证签名秘钥(防止 Token 被篡改)
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.Key)),
|
||||
};
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
var accessToken = context.Request.Query["access_token"];
|
||||
var path = context.HttpContext.Request.Path;
|
||||
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hub")) // 假设你的 SignalR 路径是 /hub
|
||||
{
|
||||
context.Token = accessToken;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnAuthenticationFailed = context =>
|
||||
{
|
||||
// 在这里打断点,查看 context.Exception
|
||||
// 常见的有:SecurityTokenExpiredException (过期)
|
||||
// 或 SecurityTokenInvalidSignatureException (密钥不对)
|
||||
Console.WriteLine("验证失败原因: " + context.Exception.Message);
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnChallenge = async context =>
|
||||
{
|
||||
context.HandleResponse();
|
||||
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = StatusCodes.Status200OK;
|
||||
|
||||
var result = Result<object>.Fail(ResultCode.AUTH_FAILED);
|
||||
await context.Response.WriteAsJsonAsync(result);
|
||||
},
|
||||
|
||||
OnForbidden = async context =>
|
||||
{
|
||||
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = StatusCodes.Status200OK;
|
||||
var result = Result<object>.Fail(ResultCode.PERMISSION_DENIED);
|
||||
await context.Response.WriteAsJsonAsync(result);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user