diff --git a/README.zh-CN.md b/README.zh-CN.md index 92d3243..367b1d3 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -33,7 +33,7 @@ ### 简介 -MiniAuth 一个轻量 ASP.NET Core Identity Web 后台管理插件 +MiniAuth 一个轻量 ASP.NET Core Identity Web 后台管理中间插件 「一行代码」为「新、旧项目」 添加 Identity 系统跟用户、权限管理后台 Web UI @@ -55,21 +55,19 @@ MiniAuth 一个轻量 ASP.NET Core Identity Web 后台管理插件 ### 特点 -- 兼容 : Based on JWT, Cookie, Session 只要符合 .NET identity 规格都支持。 -- 简单 : 拔插设计,API、MVC、Razor Page 等,都能开箱即用 +- 兼容 : 支持 .NET identity Based on JWT, Cookie, Session 等 +- 简单 : 拔插设计,API、MVC、Razor Page 等开箱即用 +- 支持多数据库 : 支持符合 Oracle, SQL Server, MySQL etc. +- 渐进、非侵入式 : 不影响现有数据库、项目结构 - 多平台 : 支持 Linux, macOS 环境 -- 支持多数据库 : 符合 Identity EF Core 规格的数据库都支持 -- 渐进、非侵入式 : 预设不会影响现有数据库结构,如有类似组织、部门需求在渐进添加 ### 安装 从 [NuGet](https://www.nuget.org/packages/MiniAuth) 安装套件 -``` +```cmd dotnet add package MiniAuth -// or -NuGet\Install-Package MiniAuth ``` @@ -84,7 +82,7 @@ NuGet\Install-Package MiniAuth { var builder = WebApplication.CreateBuilder(args); - builder.Services.AddMiniAuth(); + builder.Services.AddMiniAuth(); // <= ❗❗❗ var app = builder.Build(); app.Run(); diff --git a/src/MiniAuth.IdentityAuth/MiniAuth.IdentityAuth.csproj b/src/MiniAuth.IdentityAuth/MiniAuth.IdentityAuth.csproj index 0ca70e6..f9fd0a9 100644 --- a/src/MiniAuth.IdentityAuth/MiniAuth.IdentityAuth.csproj +++ b/src/MiniAuth.IdentityAuth/MiniAuth.IdentityAuth.csproj @@ -35,17 +35,17 @@ - + - + - + diff --git a/src/MiniAuth.IdentityAuth/MiniAuthIdentityEndpoints.cs b/src/MiniAuth.IdentityAuth/MiniAuthIdentityEndpoints.cs index 8a5608c..dd1b2bf 100644 --- a/src/MiniAuth.IdentityAuth/MiniAuthIdentityEndpoints.cs +++ b/src/MiniAuth.IdentityAuth/MiniAuthIdentityEndpoints.cs @@ -3,17 +3,21 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; using MiniAuth; using MiniAuth.IdentityAuth.Helpers; using MiniAuth.IdentityAuth.Models; using System; using System.Collections.Concurrent; +using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Linq; using System.Security.Claims; @@ -38,7 +42,7 @@ TDbContext _dbContext { await OkResult(context, _endpointCache.Values.OrderByDescending(o => o.Id).ToJson()); }) - .RequireAuthorization(new AuthorizeAttribute() { Roles= "miniauth-admin" }); + .RequireAuthorization(new AuthorizeAttribute() { Roles = "miniauth-admin" }); endpoints.MapGet("/miniauth/logout", async (HttpContext context , SignInManager signInManager , IOptions identityOptionsAccessor @@ -55,6 +59,7 @@ TDbContext _dbContext endpoints.MapPost("/miniauth/login", async (HttpContext context , TDbContext _dbContext , SignInManager signInManager + , UserManager _userManager ) => { JsonDocument bodyJson = await GetBodyJson(context); @@ -62,19 +67,69 @@ TDbContext _dbContext var userName = root.GetProperty("username"); var password = root.GetProperty("password"); var remember = root.GetProperty("remember"); - var result = await signInManager.PasswordSignInAsync(userName, password, remember, lockoutOnFailure: false); - if (result.Succeeded) + + if (MiniAuth.MiniAuthOptions.AuthenticationType == MiniAuthOptions.AuthType.BearerJwt) { - var newToken = Guid.NewGuid().ToString(); - //context.Response.Cookies.Append("X-MiniAuth-Token", newToken); - await OkResult(context, $"{{\"X-MiniAuth-Token\":\"{newToken}\"}}"); - //await OkResult(context, "".ToJson(code: 200, message: "")); + var user = await _dbContext.Users.FirstOrDefaultAsync(f => f.UserName == userName); + if (!(user != null && await _userManager.CheckPasswordAsync((TIdentityUser)user, password))) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + return; + } + var claims = new List + { + new Claim(ClaimTypes.Name, user.UserName), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + var userRoles = _dbContext.UserRoles.Where(w => w.UserId == user.Id).Select(s => s.RoleId).ToArray(); + foreach (var userRole in userRoles) + { + claims.Add(new Claim(ClaimTypes.Role, userRole)); + } + var jwtToken = new JwtSecurityTokenHandler().WriteToken(CreateToken(claims, MiniAuthOptions.TokenExpiresIn)); + var result = new + { + tokenType = "Bearer", + accessToken = jwtToken, + expiresIn = MiniAuthOptions.TokenExpiresIn, + //refreshToken = refreshToken + }; + /* +e.g. +{ + "ok": true, + "code": 200, + "message": null, + "data": { + "tokenType": "Bearer", + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTgxMTkzMzh9.I-tm9436GEXyETgUSzL7KeX5RvyN8X_4rLAKLDMZnZk", + "expiresIn": 900 + } +} + */ + + await OkResult(context, result.ToJson()); return; } else { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; - return; + var result = await signInManager.PasswordSignInAsync(userName, password, remember, lockoutOnFailure: false); + if (result.Succeeded) + { + var newToken = Guid.NewGuid().ToString(); + var jsonResult = new + { + token = newToken, + expiration = null as DateTime? + }; + await OkResult(context, jsonResult.ToJson()); + return; + } + else + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + return; + } } }); } @@ -96,7 +151,7 @@ TDbContext _dbContext }; })); await OkResult(context, roles.ToJson()); - }).RequireAuthorization(new AuthorizeAttribute() { Roles= "miniauth-admin" }); + }).RequireAuthorization(new AuthorizeAttribute() { Roles = "miniauth-admin" }); endpoints.MapPost("/miniauth/api/saveRole", async (HttpContext context , TDbContext _dbContext @@ -152,7 +207,7 @@ TDbContext _dbContext await _dbContext.SaveChangesAsync(); await OkResult(context, "".ToJson(code: 200, message: "")); - }).RequireAuthorization(new AuthorizeAttribute() { Roles= "miniauth-admin" }); + }).RequireAuthorization(new AuthorizeAttribute() { Roles = "miniauth-admin" }); endpoints.MapPost("/miniauth/api/deleteRole", async (HttpContext context @@ -169,7 +224,7 @@ TDbContext _dbContext await _dbContext.SaveChangesAsync(); } await OkResult(context, "".ToJson(code: 200, message: "")); - }).RequireAuthorization(new AuthorizeAttribute() { Roles= "miniauth-admin" }); + }).RequireAuthorization(new AuthorizeAttribute() { Roles = "miniauth-admin" }); endpoints.MapPost("/miniauth/api/getUsers", async (HttpContext context @@ -215,7 +270,7 @@ TDbContext _dbContext }); var totalItems = _dbContext.Users.Count(); await OkResult(context, new { users = userVo, totalItems }.ToJson()); - }).RequireAuthorization(new AuthorizeAttribute() { Roles= "miniauth-admin" }); + }).RequireAuthorization(new AuthorizeAttribute() { Roles = "miniauth-admin" }); endpoints.MapPost("/miniauth/api/deleteUser", async (HttpContext context , TDbContext _dbContext @@ -231,7 +286,7 @@ TDbContext _dbContext await _dbContext.SaveChangesAsync(); } await OkResult(context, "".ToJson(code: 200, message: "")); - }).RequireAuthorization(new AuthorizeAttribute() { Roles= "miniauth-admin" }); + }).RequireAuthorization(new AuthorizeAttribute() { Roles = "miniauth-admin" }); endpoints.MapPost("/miniauth/api/saveUser", async (HttpContext context , TDbContext _dbContext @@ -356,7 +411,7 @@ TDbContext _dbContext await _dbContext.SaveChangesAsync(); await OkResult(context, "".ToJson(code: 200, message: "")); } - }).RequireAuthorization(new AuthorizeAttribute() { Roles= "miniauth-admin" }); + }).RequireAuthorization(new AuthorizeAttribute() { Roles = "miniauth-admin" }); endpoints.MapPost("/miniauth/api/resetPassword", async (HttpContext context , TDbContext _dbContext @@ -393,7 +448,7 @@ TDbContext _dbContext { await OkResult(context, "".ToJson(code: 404, message: "User not found")); } - }).RequireAuthorization(new AuthorizeAttribute() { Roles= "miniauth-admin" }); + }).RequireAuthorization(new AuthorizeAttribute() { Roles = "miniauth-admin" }); endpoints.MapGet("/miniauth/api/getUserInfo", async (HttpContext context , TDbContext _dbContext @@ -437,7 +492,17 @@ TDbContext _dbContext } } } + private JwtSecurityToken CreateToken(List claims,int expires) + { + var secretkey = MiniAuthOptions.IssuerSigningKey; + var credentials = new SigningCredentials(secretkey, SecurityAlgorithms.HmacSha256); + var token = new JwtSecurityToken( + expires: DateTime.Now.AddSeconds(expires), + signingCredentials: credentials + ); + return token; + } private static string GetNewPassword() { return $"{Guid.NewGuid().ToString().Substring(0, 10).ToUpper()}@{Guid.NewGuid().ToString().Substring(0, 5)}"; @@ -456,6 +521,7 @@ private static async Task OkResult(HttpContext context, string result, string co context.Response.StatusCode = StatusCodes.Status200OK; context.Response.ContentType = contentType; context.Response.ContentLength = result != null ? Encoding.UTF8.GetByteCount(result) : 0; + await context.Response.WriteAsync(result).ConfigureAwait(false); } } diff --git a/src/MiniAuth.IdentityAuth/MiniAuthIdentityServiceExtensions.cs b/src/MiniAuth.IdentityAuth/MiniAuthIdentityServiceExtensions.cs index b268d24..2d26dc5 100644 --- a/src/MiniAuth.IdentityAuth/MiniAuthIdentityServiceExtensions.cs +++ b/src/MiniAuth.IdentityAuth/MiniAuthIdentityServiceExtensions.cs @@ -1,11 +1,14 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -71,28 +74,36 @@ public static IServiceCollection AddMiniAuth(); } - if (MiniAuthOptions.AuthenticationType == MiniAuthOptions.AuthType.Jwt) + if (MiniAuthOptions.AuthenticationType == MiniAuthOptions.AuthType.BearerJwt) { - throw new NotImplementedException("Jwt is not implemented yet"); - //services - // .AddAuthentication() - // .AddCookie() - // .AddJwtBearer(options => - // { - // options.IncludeErrorDetails = true; - // options.TokenValidationParameters = new TokenValidationParameters - // { - // ValidateIssuer = true, - // ValidateAudience = true, - // ValidateLifetime = true, - // ValidateIssuerSigningKey = true, - // ValidIssuer = "practical aspnetcore", - // ValidAudience = "https://localhost:5001/", - // IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("this is custom key for practical aspnetcore sample")) - // }; - // }); - //.AddDefaultTokenProviders() - //.AddEntityFrameworkStores(); + + services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + + }) + .AddJwtBearer(options => + { + options.IncludeErrorDetails = true; + + options.TokenValidationParameters = new TokenValidationParameters + { + NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", + RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", + ValidateIssuer = true, + ValidIssuer = "User", + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = false, + IssuerSigningKey =MiniAuth.MiniAuthOptions.IssuerSigningKey + }; + }); } } else diff --git a/src/MiniAuth.IdentityAuth/MiniAuthOptions.cs b/src/MiniAuth.IdentityAuth/MiniAuthOptions.cs index ccfbaaa..5fc3e36 100644 --- a/src/MiniAuth.IdentityAuth/MiniAuthOptions.cs +++ b/src/MiniAuth.IdentityAuth/MiniAuthOptions.cs @@ -1,4 +1,8 @@ -namespace MiniAuth +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using System.Text; + +namespace MiniAuth { public class MiniAuthOptions { @@ -8,8 +12,13 @@ public class MiniAuthOptions public enum AuthType { Cookie, - Jwt + BearerJwt } public static AuthType AuthenticationType = AuthType.Cookie; + public static SecurityKey IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("this is miniauth key for demo")); + /// + /// Seconds + /// + public static int TokenExpiresIn = 15*60; } } diff --git a/tests/TestAspNetCoreApiAot/TestAspNetCoreApiAot.sln b/tests/TestAspNetCoreApiAot/TestAspNetCoreApiAot.sln new file mode 100644 index 0000000..168c0d3 --- /dev/null +++ b/tests/TestAspNetCoreApiAot/TestAspNetCoreApiAot.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34330.188 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestAspNetCoreApiAot", "TestAspNetCoreApiAot\TestAspNetCoreApiAot.csproj", "{597416A4-A732-48F6-8140-1FAD0FA7D520}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {597416A4-A732-48F6-8140-1FAD0FA7D520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {597416A4-A732-48F6-8140-1FAD0FA7D520}.Debug|Any CPU.Build.0 = Debug|Any CPU + {597416A4-A732-48F6-8140-1FAD0FA7D520}.Release|Any CPU.ActiveCfg = Release|Any CPU + {597416A4-A732-48F6-8140-1FAD0FA7D520}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FEC23E2F-E0CB-43E0-9757-B84473B7A3E4} + EndGlobalSection +EndGlobal diff --git a/tests/TestBearer/TestBearer.sln b/tests/TestBearer/TestBearer.sln new file mode 100644 index 0000000..c62101c --- /dev/null +++ b/tests/TestBearer/TestBearer.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.34928.147 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestBearer", "TestBearer\TestBearer.csproj", "{A6C7F301-9148-4F47-8AA0-0E64B2CEB745}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniAuth.IdentityAuth", "..\..\src\MiniAuth.IdentityAuth\MiniAuth.IdentityAuth.csproj", "{2DF57D81-261F-470E-9ECE-2C3016371304}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A6C7F301-9148-4F47-8AA0-0E64B2CEB745}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6C7F301-9148-4F47-8AA0-0E64B2CEB745}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6C7F301-9148-4F47-8AA0-0E64B2CEB745}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6C7F301-9148-4F47-8AA0-0E64B2CEB745}.Release|Any CPU.Build.0 = Release|Any CPU + {2DF57D81-261F-470E-9ECE-2C3016371304}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DF57D81-261F-470E-9ECE-2C3016371304}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DF57D81-261F-470E-9ECE-2C3016371304}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DF57D81-261F-470E-9ECE-2C3016371304}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {671C9226-4331-4853-9517-F58398DEF2A3} + EndGlobalSection +EndGlobal diff --git a/tests/TestBearer/TestBearer/Program.cs b/tests/TestBearer/TestBearer/Program.cs new file mode 100644 index 0000000..5fc8d67 --- /dev/null +++ b/tests/TestBearer/TestBearer/Program.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using MiniAuth; + +namespace TestBearer +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + MiniAuthOptions.AuthenticationType = MiniAuthOptions.AuthType.BearerJwt; + builder.Services.AddMiniAuth(); + + var app = builder.Build(); + + app.MapGet("/", () => "Hello World!") + .RequireAuthorization(); + ; + + app.Run(); + } + } +} diff --git a/tests/TestBearer/TestBearer/Properties/launchSettings.json b/tests/TestBearer/TestBearer/Properties/launchSettings.json new file mode 100644 index 0000000..54dbbc4 --- /dev/null +++ b/tests/TestBearer/TestBearer/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:64331", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5014", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/tests/TestBearer/TestBearer/TestBearer.csproj b/tests/TestBearer/TestBearer/TestBearer.csproj new file mode 100644 index 0000000..67e2fa3 --- /dev/null +++ b/tests/TestBearer/TestBearer/TestBearer.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/tests/TestBearer/TestBearer/appsettings.Development.json b/tests/TestBearer/TestBearer/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/tests/TestBearer/TestBearer/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/tests/TestBearer/TestBearer/appsettings.json b/tests/TestBearer/TestBearer/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/tests/TestBearer/TestBearer/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}