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

 

posted @ 2019-04-30 14:29  刘小吉  阅读(640)  评论(0编辑  收藏  举报