在现代Web应用程序中,一般会使用Web, WebApp, NativeApp等多种呈现方式,然后端也由之前的Razor渲染HTML,转变为Stateless的RESTFulAPI,所以,咱们须要一种标准的,通用的,无状态的,与语言无关的认证方式,也就是本文要介绍的JwtBearer认证。html
目录前端
HTTP提供了一套标准的身份验证框架:服务器能够用来针对客户端的请求发送质询(challenge),客户端根据质询提供身份验证凭证。质询与应答的工做流程以下:服务器端向客户端返回401(Unauthorized,未受权)状态码,并在WWW-Authenticate头中添加如何进行验证的信息,其中至少包含有一种质询方式。而后客户端能够在请求中添加Authorization头进行验证,其Value为身份验证的凭证信息。git
在HTTP标准验证方案中,咱们比较熟悉的是"Basic"和"Digest",前者将用户名密码使用BASE64编码后做为验证凭证,后者是Basic的升级版,更加安全,由于Basic是明文传输密码信息,而Digest是加密后传输。在前文介绍的Cookie认证属于Form认证,并不属于HTTP标准验证。github
本文要介绍的Bearer验证也属于HTTP协议标准验证,它随着OAuth协议而开始流行,详细定义见: RFC 6570。web
A security token with the property that any party in possession of the token (a "bearer") can use the token in any way that any other party in possession of it can. Using a bearer token does not require a bearer to prove possession of cryptographic key material (proof-of-possession).算法
Bearer验证中的凭证称为BEARER_TOKEN
,或者是access_token
,它的颁发和验证彻底由咱们本身的应用程序来控制,而不依赖于系统和Web服务器,Bearer验证的标准请求方式以下:json
Authorization: Bearer [BEARER_TOKEN]
那么使用Bearer验证有什么好处呢?后端
CORS: cookies + CORS 并不能跨不一样的域名。而Bearer验证在任何域名下均可以使用HTTP header头部来传输用户信息。api
对移动端友好: 当你在一个原平生台(iOS, Android, WindowsPhone等)时,使用Cookie验证并非一个好主意,由于你得和Cookie容器打交道,而使用Bearer验证则简单的多。浏览器
CSRF: 由于Bearer验证再也不依赖于cookies, 也就避免了跨站请求攻击。
标准:在Cookie认证中,用户未登陆时,返回一个302
到登陆页面,这在非浏览器状况下很难处理,而Bearer验证则返回的是标准的401 challenge
。
上面介绍的Bearer认证,其核心即是BEARER_TOKEN,而最流行的Token编码方式即是:JSON WEB TOKEN。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登陆(SSO)场景。JWT的声明通常被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也能够增长一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT是由.
分割的以下三部分组成:
Header 通常由两个部分组成:
alg
是是所使用的hash算法,如:HMAC SHA256或RSA,typ
是Token的类型,在这里就是:JWT。
{ "alg": "HS256", "typ": "JWT" }
而后使用Base64Url编码成第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<second part>.<third part>
这一部分是JWT主要的信息存储部分,其中包含了许多种的声明(claims)。
Claims的实体通常包含用户和一些元数据,这些claims分红三种类型:
reserved claims:预约义的 一些声明,并非强制的可是推荐,它们包括 iss (issuer), exp (expiration time), sub (subject),aud(audience) 等(这里都使用三个字母的缘由是保证 JWT 的紧凑)。
public claims: 公有声明,这个部分能够随便定义,可是要注意和 IANA JSON Web Token 冲突。
private claims: 私有声明,这个部分是共享被认定信息中自定义部分。
一个简单的Pyload能够是这样子的:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
这部分一样使用Base64Url编码成第二部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.<third part>
Signature是用来验证发送者的JWT的同时也能确保在期间不被篡改。
在建立该部分时候你应该已经有了编码后的Header和Payload,而后使用保存在服务端的秘钥对其签名,一个完整的JWT以下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
所以使用JWT具备以下好处:
通用:由于json的通用性,因此JWT是能够进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等不少语言均可以使用。
紧凑:JWT的构成很是简单,字节占用很小,能够经过 GET、POST 等放在 HTTP 的 header 中,很是便于传输。
扩展:JWT是自我包涵的,包含了必要的全部信息,不须要在服务端保存会话信息, 很是易于应用的扩展。
关于更多JWT的介绍,网上很是多,这里就再也不多作介绍。下面,演示一下 ASP.NET Core 中 JwtBearer 认证的使用方式。
ASP.NET Core 内置的JwtBearer验证,并不包含Token的发放,咱们先模拟一个简单的实现:
[HttpPost("authenticate")] public IActionResult Authenticate([FromBody]UserDto userDto) { var user = _store.FindUser(userDto.UserName, userDto.Password); if (user == null) return Unauthorized(); var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(Consts.Secret); var authTime = DateTime.UtcNow; var expiresAt = authTime.AddDays(7); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new Claim[] { new Claim(JwtClaimTypes.Audience,"api"), new Claim(JwtClaimTypes.Issuer,"http://localhost:5200"), new Claim(JwtClaimTypes.Id, user.Id.ToString()), new Claim(JwtClaimTypes.Name, user.Name), new Claim(JwtClaimTypes.Email, user.Email), new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber) }), Expires = expiresAt, SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); var tokenString = tokenHandler.WriteToken(token); return Ok(new { access_token = tokenString, token_type = "Bearer", profile = new { sid = user.Id, name = user.Name, auth_time = new DateTimeOffset(authTime).ToUnixTimeSeconds(), expires_at = new DateTimeOffset(expiresAt).ToUnixTimeSeconds() } }); }
如上,使用微软提供的Microsoft.IdentityModel.Tokens帮助类(源码地址:azure-activedirectory-identitymodel-extensions-for-dotnet),能够很容易的建立出JwtToen,就再也不多说。
首先添加JwtBearer
包引用:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 2.0.0
而后在Startup
类中添加以下配置:
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { NameClaimType = JwtClaimTypes.Name, RoleClaimType = JwtClaimTypes.Role, ValidIssuer = "http://localhost:5200", ValidAudience = "api", IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Consts.Secret)) /***********************************TokenValidationParameters的参数默认值***********************************/ // RequireSignedTokens = true, // SaveSigninToken = false, // ValidateActor = false, // 将下面两个参数设置为false,能够不验证Issuer和Audience,可是不建议这样作。 // ValidateAudience = true, // ValidateIssuer = true, // ValidateIssuerSigningKey = false, // 是否要求Token的Claims中必须包含Expires // RequireExpirationTime = true, // 容许的服务器时间偏移量 // ClockSkew = TimeSpan.FromSeconds(300), // 是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比 // ValidateLifetime = true }; }); } public void Configure(IApplicationBuilder app) { app.UseAuthentication(); }
在JwtBearerOptions
的配置中,一般IssuerSigningKey(签名秘钥)
, ValidIssuer(Token颁发机构)
, ValidAudience(颁发给谁)
三个参数是必须的,后二者用于与TokenClaims中的Issuer
和Audience
进行对比,不一致则验证失败(与上面发放Token中的Claims对应)。
而NameClaimType
和RoleClaimType
需与Token中的ClaimType一致,在IdentityServer中也是使用的JwtClaimTypes
,不然会形成User.Identity.Name
为空等问题。
建立一个须要受权的控制器,直接使用Authorize
便可:
[Authorize] [Route("api/[controller]")] public class SampleDataController : Controller { [HttpGet("[action]")] public IEnumerable<WeatherForecast> WeatherForecasts() { return ... } }
最后运行,直接访问/api/SampleData/WeatherForecasts
,将返回一个401
:
HTTP/1.1 401 Unauthorized Server: Kestrel Content-Length: 0 WWW-Authenticate: Bearer
让咱们调用api/oauth/authenticate
,获取一个JWT:
请求: POST http://localhost:5200/api/oauth/authenticate HTTP/1.1 content-type: application/json { "username": "alice", "password": "alice" } 响应: HTTP/1.1 200 OK {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoiYWxpY2UiLCJlbWFpbCI6ImFsaWNlQGdtYWlsLmNvbSIsInBob25lX251bWJlciI6IjE4ODAwMDAwMDAxIiwibmJmIjoxNTA5NDY0MzQwLCJleHAiOjE1MTAwNjkxNDAsImlhdCI6MTUwOTQ2NDM0MH0.Y1TDz8KjLRh_vjQ_3iYP4oJw-fmhoboiAGPqIZ-ooNc","token_type":"Bearer","profile":{"sid":1,"name":"alice","auth_time":1509464340,"expires_at":1510069140}}
最后使用该Token,再次调用受保护资源:
GET http://localhost:5200/api/SampleData/WeatherForecasts HTTP/1.1 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoiYWxpY2UiLCJlbWFpbCI6ImFsaWNlQGdtYWlsLmNvbSIsInBob25lX251bWJlciI6IjE4ODAwMDAwMDAxIiwibmJmIjoxNTA5NDY0MzQwLCJleHAiOjE1MTAwNjkxNDAsImlhdCI6MTUwOTQ2NDM0MH0.Y1TDz8KjLRh_vjQ_3iYP4oJw-fmhoboiAGPqIZ-ooNc
受权成功,返回了预期的数据:
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 [{"dateFormatted":"2017/11/3","temperatureC":35,"summary":"Chilly","temperatureF":94}]
JwtBearer认证中,默认是经过Http的Authorization
头来获取的,这也是最推荐的作法,可是在某些场景下,咱们可能会使用Url或者是Cookie来传递Token,那要怎么来实现呢?
其实实现起来很是简单,如前几章介绍的同样,JwtBearer也在认证的各个阶段为咱们提供了事件,来执行咱们的自定义逻辑:
.AddJwtBearer(o => { o.Events = new JwtBearerEvents() { OnMessageReceived = context => { context.Token = context.Request.Query["access_token"]; return Task.CompletedTask; } }; o.TokenValidationParameters = new TokenValidationParameters { ... };
而后在Url中添加access_token=[token]
,直接在浏览器中访问:
一样的,咱们也能够很容易的在Cookie中读取Token,就再也不演示。
除了OnMessageReceived
外,还提供了以下几个事件:
TokenValidated:在Token验证经过后调用。
AuthenticationFailed: 认证失败时调用。
Challenge: 未受权时调用。
在上面的示例中,咱们简单模拟的Token颁发,功能很是简单,并不适合在生产环境中使用,但是微软也没有提供OIDC服务的实现,好在.NET社区中提供了几种实现,可供咱们选择:
Name | Description |
---|---|
AspNet.Security.OpenIdConnect.Server (ASOS) | Low-level/protocol-first OpenID Connect server framework for ASP.NET Core and OWIN/Katana |
IdentityServer4 | OpenID Connect and OAuth 2.0 framework for ASP.NET Core - officially certified by the OpenID Foundation and under governance of the .NET Foundation |
OpenIddict | Easy-to-use OpenID Connect server for ASP.NET Core |
PwdLess | Simple, stateless, passwordless authentication for ASP.NET Core |
咱们在这里使用IdentityServer4来搭建一个OIDC服务器,并添加以下配置:
/********************OIDC服务器代码片断********************/ public void ConfigureServices(IServiceCollection services) { services.AddMvc(); // 配置IdentitryServer services.AddIdentityServer() .AddInMemoryPersistedGrants() .AddInMemoryApiResources(Config.GetApis()) .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryClients(Config.GetClients()) .AddTestUsers(Config.GetUsers()) .AddDeveloperSigningCredential(); } new Client { ClientId = "jwt.implicit", ClientName = "Implicit Client (Web)", AllowedGrantTypes = GrantTypes.Implicit, AllowAccessTokensViaBrowser = true, RedirectUris = { "http://localhost:5200/callback" }, PostLogoutRedirectUris = { "http://localhost:5200/home" }, AllowedCorsOrigins = { "http://localhost:5200" }, AllowedScopes = { "openid", "profile", "email", "api" }, }
而JwtBearer客户端的配置就更加简单了,由于OIDC具备配置发现的功能:
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { o.Authority = "https://oidc.faasx.com/"; o.Audience = "api"; o.TokenValidationParameters = new TokenValidationParameters { NameClaimType = JwtClaimTypes.Name, RoleClaimType = JwtClaimTypes.Role, }; }); }
如上,最重要的是Authority
参数,用来表示OIDC服务的地址,而后即可以自动发现Issuer
, IssuerSigningKey
等配置,而o.Audience
与o.TokenValidationParameters = new TokenValidationParameters { ValidAudience = "api" }
是等效的,后面分析源码时会介绍。
OIDC兼容OAuth2协议,咱们可使用上一章介绍的受权码模式来获取Token,也能够直接用户名密码模式来获取Token:
请求: POST https://oidc.faasx.com/connect/token HTTP/1.1 Content-Type: application/x-www-form-urlencoded client_id=client.rop&client_secret=secret&grant_type=password&scope=api&username=alice&password=alice 响应: HTTP/1.1 200 OK Content-Type: application/json {"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlYzk5MjVlMmUzMTA2NmY2ZmU2ODgzMDRhZjU1ZmM0IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MDk2NzI1NjksImV4cCI6MTUwOTY3NjE2OSwiaXNzIjoiaHR0cHM6Ly9vaWRjLmZhYXN4LmNvbSIsImF1ZCI6WyJodHRwczovL29pZGMuZmFhc3guY29tL3Jlc291cmNlcyIsImFwaSJdLCJjbGllbnRfaWQiOiJjbGllbnQucm9wIiwic3ViIjoiMDAxIiwiYXV0aF90aW1lIjoxNTA5NjcyNTY5LCJpZHAiOiJsb2NhbCIsIm5hbWUiOiJBbGljZSBTbWl0aCIsImVtYWlsIjoiQWxpY2VTbWl0aEBlbWFpbC5jb20iLCJzY29wZSI6WyJhcGkiXSwiYW1yIjpbInB3ZCJdfQ.PM93LThOZA3lkgPFVwieqGQQQtgmYDCY0oSFVmudv1hpKO6UaaZsmnn4ci9QjbGl5g2433JkDks5UIZsZ0xE62Qqq8PicPBBuaNoYrCf6dxR7j-0uZcoa7-FCKGu-0TrM8OL-NuMvN6_KEpbWa3jlkwibCK9YDIwJZilVoWUOrbbIEsKTa-DdLScmzHLUzksT8GBr0PAVhge9PRFiGqg8cgMLjsA62ZeDsR35f55BucSV5Pj0SAj26anYvrBNTHKOF7ze1DGW51Dbz6DRu1X7uEIxSzWiNi4cRVJ6Totjkwk5F78R9R38o_mYEdehZBjRHFe6zLd91hXcCKqOEh5eQ","expires_in":3600,"token_type":"Bearer"}
咱们使用https://jwt.io解析一下OIDC服务器颁发的Token中的Claims:
{ "nbf": 1509672569, // 2017/11/3 1:29:29 NotBefore Token生效时间,在此以前不可用 "exp": 1509676169, // 2017/11/3 2:29:29 Expiration Token过时时间,在此以后不可用 "iss": "https://oidc.faasx.com", // Issuer 颁发者,一般为STS服务器地址 "aud": [ // Audience Token的做用对象,也就是被访问的资源服务器受权标识 "https://oidc.faasx.com/resources", "api" ], "client_id": "client.rop", // 客户端标识 "sub": "001", "auth_time": 1509672569, // Token颁发时间 "idp": "local", "name": "Alice Smith", "email": "AliceSmith@email.com", "scope": [ "api" ], "amr": [ "pwd" ] }
我在本章的示例代码中,使用前端Angular框架演示了如何从本地登陆获取Tokek或使用简化模式(implicit)从OIDC服务器获取Token,而后保存到sesstionStorage,在发送请求时附加到请求头中的示例,可供你们参考:JwtBearerSample。
在ASP.NET Core 2.0 Options框架中,新增了一种PostConfigure模式,用来在咱们所注册的Options配置执行完以后,再对Options作一些修改。
JwtBearerPostConfigureOptions用来实现配置发现:
public class JwtBearerPostConfigureOptions : IPostConfigureOptions<JwtBearerOptions> { public void PostConfigure(string name, JwtBearerOptions options) { // 若是未设置options.TokenValidationParameters.ValidAudience,则使用options.Audience if (string.IsNullOrEmpty(options.TokenValidationParameters.ValidAudience) && !string.IsNullOrEmpty(options.Audience)) { options.TokenValidationParameters.ValidAudience = options.Audience; } if (options.ConfigurationManager == null) { // 若是未设置MetadataAddress,则使用options.Authority+.well-known/openid-configuration .... options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(options.MetadataAddress, new OpenIdConnectConfigurationRetriever(), new HttpDocumentRetriever(httpClient) { RequireHttps = options.RequireHttpsMetadata }); } } } }
JwtBearerHandler相对于前几章介绍的CookieHandler, OpenIdConnectHandler等,都简单的多。
首先即是从请求中获取Token:
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options); // 先触发MessageReceived事件,来获取Token await Events.MessageReceived(messageReceivedContext); if (messageReceivedContext.Result != null) { return messageReceivedContext.Result; } token = messageReceivedContext.Token; // Token为空时,从Authorization头中获取 if (string.IsNullOrEmpty(token)) { string authorization = Request.Headers["Authorization"]; if (string.IsNullOrEmpty(authorization)) { return AuthenticateResult.NoResult(); } if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { token = authorization.Substring("Bearer ".Length).Trim(); } if (string.IsNullOrEmpty(token)) { return AuthenticateResult.NoResult(); } } ... }
而后初始化TokenValidationParameters
参数,为Token验证作准备:
if (_configuration == null && Options.ConfigurationManager != null) { _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); } var validationParameters = Options.TokenValidationParameters.Clone(); if (_configuration != null) { var issuers = new[] { _configuration.Issuer }; validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers; validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? _configuration.SigningKeys; }
能够看到,从OIDC服务器提供的配置发现中,获取ValidIssuers
和IssuerSigningKeys
。
最后对Token进行验证:
// Options.SecurityTokenValidators 默认为: new List<ISecurityTokenValidator> { new JwtSecurityTokenHandler() } foreach (var validator in Options.SecurityTokenValidators) { if (validator.CanReadToken(token)) { ClaimsPrincipal principal; try { principal = validator.ValidateToken(token, validationParameters, out validatedToken); } catch (Exception ex) { // RefreshOnIssuerKeyNotFound默认为True, 在SignatureKey未找到时,从新从OIDC服务器获取 if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null && ex is SecurityTokenSignatureKeyNotFoundException) { Options.ConfigurationManager.RequestRefresh(); } continue; } ... // 触发TokenValidated事件 await Events.TokenValidated(tokenValidatedContext); // 默认为true,保存Token到`AuthenticationProperties`中,能够经过`context.AuthenticateAsync()`来获取,在咱们须要在服务端使用用户Token调用其余资源是很是有用。 if (Options.SaveToken) { tokenValidatedContext.Properties.StoreTokens(new[] { new AuthenticationToken { Name = "access_token", Value = token } }); } // 验证成功 tokenValidatedContext.Success(); return tokenValidatedContext.Result; } }
其核心的验证也是在Microsoft.IdentityModel.Tokens
中,就不在深究。
当使用JwtBearer认证时,咱们确定不但愿在未登陆时返回一个302
,所以在前面的示例中,咱们配置了x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
,对应的,会执行JwtBearerHandler的HandleChallengeAsync
方法:
protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { var authResult = await HandleAuthenticateOnceSafeAsync(); var eventContext = new JwtBearerChallengeContext(Context, Scheme, Options, properties) { AuthenticateFailure = authResult?.Failure }; if (Options.IncludeErrorDetails && eventContext.AuthenticateFailure != null) { eventContext.Error = "invalid_token"; eventContext.ErrorDescription = CreateErrorDescription(eventContext.AuthenticateFailure); } await Events.Challenge(eventContext); if (eventContext.Handled) { return; } Response.StatusCode = 401; // 最终将相应报文拼接成以下: // https://tools.ietf.org/html/rfc6750#section-3.1 // WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired" }
ASP.NET Core JwtBearer认证的完整源码地址:Microsoft.AspNetCore.Authentication.JwtBearer。
JwtToken其实与Cookie认证中加密后的Cookie值很像,他们都是基于Claim的,认证时无需STS(Security token service)的参与,这在分布式环境下提供了极大的便利。而他们的本质上的区别是:Cookie是微软式的,很难与其余语言集成,而JwtToken则是开放再开放,与平台,语言无关,在前端也能够直接解析出Claims。
PS: 在使用在Bearer认证时,一般还需与刷新Token配合来使用,由于JwtToken的验证是无需通过STS的,而当用户执行了退出,修改密码等操做时,是没法使该Token失效的。因此,一般会给access_token
设置一个较短的有效期(JwtBearer认证默认会验证有效期,经过notBefore
和expires
来验证),当access_token
过时后,能够在用户无感知的状况下,使用refresh_token
自动从STS从新获取access_token
,但这就不属于Bearer认证的范畴了,在后续介绍IdentityServer时再来详细介绍一下。