IdentitiServser4 + Mysql实现Authorization Server
Identity Server 4官方文档:https://identityserver4.readthedocs.io/en/latest/
新建2个asp.net core 项目使用空模板
Auth 身份认证服务
Client客户端
Auth项目打开安装相关依赖
IdentityServer4 和 EF 实体框架
Mysql EF Provider
Nlog日志组件
打开Startup.cs 文件配置 管道
配置 identity server
还是Startup.cs,编辑ConfigureServices方法:
添加Nlog日志
services.AddLogging(logBuilder =>
{
logBuilder.AddNLog();
});
日志配置文件(右键属性选择始终复制)
日志配置内容
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <extensions> <add assembly="Nlog.Targets.Splunk"/> </extensions> <targets async="true"> <target name="asyncFile" xsi:type="File" layout="[${longdate}] [${level}] [${logger}] [${message}] ${newline} ${exception:format=tostring}" fileName="${basedir}/log/${shortdate}.txt" archiveFileName="${basedir}/log/archives/log.{#####}.txt" archiveAboveSize="102400000" archiveNumbering="Sequence" concurrentWrites="true" keepFileOpen="false" encoding="utf-8" /> <target name="console" xsi:type="console"/> </targets> <rules> <!--Info,Error,Warn,Debug,Fatal--> <logger name="*" levels="Info,Error,Warn,Debug,Fatal" writeTo="asyncFile" /> <logger name="*" minlevel="Error" writeTo="console" /> </rules> </nlog>
添加IdentityServer服务
services.AddIdentityServer()
添加身份验证方式
services.AddIdentityServer()
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
ResourceOwnerPasswordValidator 类实现接口 IResourceOwnerPasswordValidator
实现验证ValidateAsync异步方法
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { try { //根据context.UserName和context.Password与数据库的数据做校验,判断是否合法 if (context.UserName == "wjk" && context.Password == "123") { context.Result = new GrantValidationResult( subject: context.UserName, authenticationMethod: "custom", claims: GetUserClaims()); } else { //验证失败 context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "invalid custom credential"); } } catch (System.Exception exception) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid username or password"); throw; } }
设置Claims
private static IEnumerable<Claim> GetUserClaims() { return new[] { new Claim("uid", "1"), new Claim(JwtClaimTypes.Name,"wjk"), new Claim(JwtClaimTypes.GivenName, "GivenName"), new Claim(JwtClaimTypes.FamilyName, "yyy"), new Claim(JwtClaimTypes.Email, "977865769@qq.com"), new Claim(JwtClaimTypes.Role,"admin") }; }
设置验证方式
services.AddIdentityServer() .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>() .AddSigningCredential(cert)
使用pfx 证书验证
cert证书读取
var certAbsolutePath = PathResolver.ResolveCertConfigPath(Configuration["AuthOption:X509Certificate2FileName"], Environment); var cert = new X509Certificate2(certAbsolutePath, Configuration["AuthOption:X509Certificate2Password"]);
public static class PathResolver { public static string ResolveCertConfigPath(string configPath, IHostingEnvironment environment) { var start = new[] { "./", ".\\", "~/", "~\\" }; if (start.Any(d => configPath.StartsWith(d))) { foreach (var item in start) { configPath = configPath.TrimStart(item); } return Path.Combine(environment.ContentRootPath, configPath); } return configPath; } public static string TrimStart(this string target, string trimString) { if (string.IsNullOrEmpty(trimString)) return target; string result = target; while (result.StartsWith(trimString)) { result = result.Substring(trimString.Length); } return result; } }
appsettings.json配置文件
"AuthOption": { "X509Certificate2FileName": "./work.pfx", "X509Certificate2Password": "123456" }
证书生成方式:https://www.cnblogs.com/liuxiaoji/p/10790057.html
EF注入
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityServer() .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>() .AddSigningCredential(cert) .AddConfigurationStore(options => { options.ConfigureDbContext = b => b.UseMySql(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); }).AddOperationalStore(options => { options.ConfigureDbContext = b => b.UseMySql(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); options.EnableTokenCleanup = true; });
数据库配置初始化
/// <summary> /// 配置 /// </summary> public class Config { /// <summary> /// 定义身份资源 /// </summary> /// <returns>身份资源</returns> public static IEnumerable<IdentityResource> GetIdentityResources() { return new IdentityResource[] { //身份资源是用户的用户ID new IdentityResources.OpenId() }; } /// <summary> /// 定义API资源 /// </summary> /// <returns>API资源</returns> public static IEnumerable<ApiResource> GetApiResources() { return new[] { new ApiResource("api1", new[] {"uid", JwtClaimTypes.Name}), new ApiResource("api2", new[] { "uid", "name"}) }; } /// <summary> /// 定义客户端 /// </summary> /// <returns>客户端</returns> public static IEnumerable<Client> GetClients() { return new[] { new Client { ClientId = "client", AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials, AllowOfflineAccess = true, ClientSecrets = {new Secret("secret".Sha256())}, AllowedScopes = {OidcConstants.StandardScopes.OfflineAccess, "api1", "api2"} } }; } }
private void InitializeDatabase(IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope()) { /* *切换到程序包管理器控制器,将默认项目设置为Auth,然后输入以下命令 *PersistedGrantDbContext *Add-Migration -Name InitialIdentityServerConfigurationDbMigration -Context PersistedGrantDbContext -OutputDir Migrations/IdentityServer/PersistedGrantDb -Project Auth *ConfigurationDbContext *Add-Migration -Name InitialIdentityServerConfigurationDbMigration -Context ConfigurationDbContext -OutputDir Migrations/IdentityServer/ConfigurationDb -Project Auth *命令将使用ConfigurationDbContext创建一个名为InitialIdentityServerConfigurationDbMigration 的迁移。迁移文件将输出到将默认项目设置为Auth项目的Migrations/IdentityServer/ConfigurationDb文件夹。 */ // 必须执行数据库迁移命令才能生成数据库表 serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate(); var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>(); context.Database.Migrate(); if (!context.Clients.Any()) { foreach (var client in Config.GetClients()) { context.Clients.Add(client.ToEntity()); } context.SaveChanges(); } if (!context.IdentityResources.Any()) { foreach (var resource in Config.GetIdentityResources()) { context.IdentityResources.Add(resource.ToEntity()); } context.SaveChanges(); } if (!context.ApiResources.Any()) { foreach (var resource in Config.GetApiResources()) { context.ApiResources.Add(resource.ToEntity()); } context.SaveChanges(); } } }
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { InitializeDatabase(app); app.UseIdentityServer(); }
EF数据库迁移
var connectionString = Configuration.GetConnectionString("Auth");
"ConnectionStrings": { "Auth": "Server=localhost;database=auth;uid=root;pwd=123456;charset=UTF8;" },
执行命令
切换到程序包管理器控制器,将默认项目设置为Auth,然后输入以下命令 PersistedGrantDbContext Add-Migration -Name InitialIdentityServerConfigurationDbMigration -Context PersistedGrantDbContext -OutputDir Migrations/IdentityServer/PersistedGrantDb -Project Auth ConfigurationDbContext Add-Migration -Name InitialIdentityServerConfigurationDbMigration -Context ConfigurationDbContext -OutputDir Migrations/IdentityServer/ConfigurationDb -Project Auth 命令将使用ConfigurationDbContext创建一个名为InitialIdentityServerConfigurationDbMigration 的迁移。迁移文件将输出到将默认项目设置为Auth项目的Migrations/IdentityServer/ConfigurationDb文件夹。
Client打开添加相关配置
IdentityServer4 和 EF 实体框架
Nlog日志组件
类型转化框架
打开Startup.cs 文件配置 管道
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { //添加日志 loggerFactory.AddNLog(); //添加权限验证 app.UseAuthentication(); //添加MVC框架 app.UseMvc(); }
public void ConfigureServices(IServiceCollection services) { //加入HttpClient services.AddHttpClient(); var config = Configuration.GetSection("JwtBearerOptions").Get<JwtBearerOptions>(); services.Configure<JwtBearerOptions>(Configuration.GetSection("JwtBearerOptions")); services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(option => { option.RequireHttpsMetadata = false; option.Authority = config.Authority; option.RequireHttpsMetadata = config.RequireHttpsMetadata; option.Audience = config.Audience; }); services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_2) .AddJsonOptions(option => { option.UseCamelCasing(true); }); }
配置文件
"JwtBearerOptions": { "Authority": "http://localhost:61491", "RequireHttpsMetadata": false, "Audience": "api1" }
添加AccountController 用于登录 和刷新Toekn
[AllowAnonymous] [Route("api/[controller]")] public class AccountController: ControllerBase { /// <summary> /// http客户端 /// </summary> private readonly HttpClient _client; /// <summary> /// 日志 /// </summary> private readonly ILogger<AccountController> _logger; /// <summary> /// 身份认证配置 /// </summary> private readonly JwtBearerOptions _authOption; public AccountController( IHttpClientFactory httpClientFactory, ILogger<AccountController> logger, IOptions<JwtBearerOptions> authOptions) { _client = httpClientFactory.CreateClient(); _logger = logger; _authOption = authOptions.Value; } [NonAction] private async Task<LoginResult> GenerateTokenAsync(string account, string password, string enterpriseId) { var disco = await _client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest { Address = _authOption.Authority, Policy = { RequireHttps = false } }); if (disco.IsError) { var msg = $"用户{account}登陆到注册中心出错,错误码{disco.StatusCode},详情{disco.Json}"; _logger.LogError(msg); throw new InvalidOperationException(msg); } var parameters = new Dictionary<string, string> { { ClaimsConst.EnterpriseId, enterpriseId } }; // request token var tokenResponse = await _client.RequestPasswordTokenAsync(new PasswordTokenRequest { Address = disco.TokenEndpoint, ClientId = "client", ClientSecret = "secret", UserName = account, Password = password, Parameters = parameters }); if (tokenResponse.IsError) { var msg = $"用户{account}获取token出错,错误码{tokenResponse.HttpStatusCode},详情{tokenResponse.Json}"; _logger.LogError(msg); throw new InvalidOperationException(msg); } return new LoginResult { AccessToken = tokenResponse.AccessToken, RefreshToken = tokenResponse.RefreshToken, TokenType = tokenResponse.TokenType, ExpiresIn = tokenResponse.ExpiresIn }; } /// <summary> /// 登录 /// </summary> /// <param name="login">登录信息</param> /// <returns>结果</returns> [HttpPost("login")] public async Task<ActionResult<LoginResult>> Login([FromBody]Login login) { var result = await GenerateTokenAsync(login.Account, login.Password, "自定义参数"); if (result != null) { return Ok(result); } return Unauthorized(); } /// <summary> /// 用refresh token获取新的access token /// </summary> /// <param name="token">refresh token</param> /// <returns></returns> [HttpGet("refresh/{token}")] public async Task<ActionResult<LoginResult>> Refresh(string token) { var disco = await _client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest { Address = _authOption.Authority, Policy = { RequireHttps = false } }); if (disco.IsError) { _logger.LogError($"获取刷新token出错,错误码{disco.StatusCode},详情{disco.Json}"); return null; } var tokenResponse = await _client.RequestRefreshTokenAsync(new RefreshTokenRequest { Address = disco.TokenEndpoint, ClientId = "client", ClientSecret = "secret", RefreshToken = token, Scope = OidcConstants.StandardScopes.OfflineAccess, }); if (tokenResponse.IsError) { _logger.LogError($"获取刷新token出错,错误码{tokenResponse.HttpStatusCode},详情{tokenResponse.Json}"); return Unauthorized(new { Code = 1, Msg = "刷新token已过期" }); } var tokenResult = tokenResponse.MapTo<LoginResult>(); return Ok(tokenResult); } }
添加 AuthController 用于测试token 身份验证
[Authorize] [Route("api/[controller]")] public class AuthController : ControllerBase { public ActionResult<string> Get() { return Ok("授权成功"); } }
相关实体类
public class ClaimsConst { public const string UserId = "uid"; public const string UserName = "uname"; public const string EnterpriseId = "eid"; public const string DepartmentId = "did"; public const string Subject = "sub"; public const string Expires = "exp"; public const string Issuer = "iss"; public const string Audience = "aud"; public const string IssuedAt = "iat"; public const string ClientId = "client_id"; public const string LoginStrategy = "lgs"; public const string AuthTime = "auth_time"; public const string Idp = "idp"; public const string NotBefore = "nbf"; public const string UserData = "udata"; public const string DeviceCode = "dcd"; }
public class Login { /// <summary> /// 帐号 /// </summary> [Required(ErrorMessage = "帐号不能为空")] [MaxLength(20, ErrorMessage = "帐号长度最长为20位字符")] public string Account { get; set; } /// <summary> /// 密码 /// </summary> [Required(ErrorMessage = "密码不能为空")] [MaxLength(40, ErrorMessage = "密码长度最长为40位字符")] public string Password { get; set; } }
public class LoginResult { /// <summary> /// token /// </summary> public string AccessToken { get; set; } /// <summary> /// 刷新token /// </summary> public string RefreshToken { get; set; } /// <summary> /// token过期时间 /// </summary> public int ExpiresIn { get; set; } /// <summary> /// token类型 /// </summary> public string TokenType { get; set; } }
使用postman 测试
登录
刷新token
访问API
使用正确的Token