Identity Server4 基础应用(二)Authorization Code(Part I)
上一篇中使用Identity Server4的模板搭建了一个简单的授权服务器,并使用这个授权服务器对一个WebApi的应用进行了保护。其中,我们使用的是Client Credentials授权模式,但是这种方式下缺少与用户交互的流程,到真正的生产环境下使用的场景并不是很多。那么如果有用户的参与就需要另外一种授权模式,这种情况下有交互的客户端。这也是比较常见的情形,包括Web应用,SPA,app等都是有用户进行参与的应用程序。针对这种情形设计出了Authorization Code模式,这个方式下走的流程就比Client Credentials要麻烦的多了,由于涉及到用户身份的认证,而OAuth2.0主要是针对授权的协议,因此便需要引入OpenId Connect协议来弥补。
OpenId Connect简介
OpenId Connect (可简称OIDC)是是基于OAuth2.0规范之上的一个身份验证协议,很多情况下都是和OAuth2.0一起出现的,其作用便是通过可通过授权服务器的认证结果来确认用户的身份,并且还能获取用户的基本身份信息(Claims)。Openid Connect允许用户跨网站或应用程序来进行身份认证,而无需知晓用户的账号密码(比如现在很多网站都支持通过第三方账号登录),这就让他们减轻了管理别人账号密码的负担,也节约了用户注册账号的时间。
OpenId Connect支持基本所有形式的客户端,包括基于浏览器的、基于移动设备或者是桌面的应用程序。
Authorization Code 模式
先对这个方式进行一个简单的概括。在Authorization Code Flow中,用户通过客户端代理(最常见的就是我们的浏览器)请求受保护的资源时,会被重定向到授权服务器的身份认证界面(登录页或同意页),在认证界面中完成认证后,授权服务器会向客户端程序发放一个随机字符串叫做“Authorization Code”。这个Authorization Code就像一个兑换券一样,当客户端拥有这个Authorization Code后会利用它和其他的一些认证信息再去授权服务器“兑换”授权的Tokens。那么有了这些Token后,便可以访问受保护的资源了。
接下来通过代码实践这种方式,这次使用一个ASP.NET Core MVC应用程序作为需要访问受保护资源的客户端,并使用OpenIdConnect策略进行身份认证。那么这个客户端的代理便是我们的浏览器了。因为这种方式下有了认证用户的参与,那么受保护的资源中多了一部分用户的身份信息。当客户端被信任时,我们便可以访问到这些资源了。
我们还是先通过一个简图粗略的先认识一下授权的过程,在OAuth2.0官方文档的4.1节(https://tools.ietf.org/html/rfc6749#section-4.1)包含这张请求的流程图和相关的描述,而OpenId Connect协议在与其大致相同。
简单的描述上图中的流程:
(1)当我们通过MVC应用要去访问一个受保护的资源时,在用户代理(浏览器)中输入完地址后发送请求,MVC服务器接收到请求后开启验证流程;
(2)客户端(MVC服务器)会通过将浏览器定向到授权服务器的Authorization Endpoint地址(localhost:5000/content/authorize),并且这次请求是携带着附加信息的,主要的有如下几个,值得注意的是这一步验证是不需要包括client secret的。
response_type | 在这里值为“code”,表示请求Authorization Code |
client Id | 客户端的ClientId |
scope | 请求的范围 |
redirection uri | 重定向地址,当客户端经过授权服务器验证后,授权服务器通过这个地址将Authorization Code发送给客户端 |
state | 本地状态, 看到有人说是为了防止CSRF攻击,服务端的响应会把state原封不动的传回来以证明响应不是伪造的 |
(3)如果授权服务器通过了这个请求的验证(图中的A步骤),此时授权服务器会将浏览器重定向到登录界面,此时用户来向授权服务器验证他的身份,可以是输入用户名密码。
(4)成功进行验证后,授权服务器将浏览器重定回到上面提到的redirection uri,并且携带这Authorization Code和本地状态,这样客户端程序就获得了这个“兑换券”Authorization Code了。
(5)MVC服务器触发Authorizaion流程,MVC服务器向授权服务器的Token终结点发送请求,请求中包含上一步获取到的Authorization Code 等
Client Id | 客户端的ClientId |
Client Secret | 等到这一步就要带上这个信息来进行验证了 |
Code | 就是Authorization Code,上一步获取到的“兑换券” |
Redirection Uri | 没错,这个地址也要发回去核对,用以给授权服务器来验证这个是不是和刚刚发回给客户端的地址一样 |
grant_type | authorization_code,向授权服务器指明授权方式 |
(6)授权服务器验证通过后便会返回Tokens(包括Access Token、与可选的Identity Token与Refresh Token)
这种方式包含两次交互。首先通过前端通道(浏览器)的登录页面或同意页面等与用户进行交互,成功后产生Authorization Code并由授权服务器发送给客户端。随后通过后端通道,使用Authorization Code得到请求用的Tokens(Access Token、与可选的Identity Token和Refresh Token)。相比之下,在这个流程更加安全,首先前端通道不会泄露数据(不发送Client Secret,authorization code)。并且Authorization code只能被使用一次且有超时时间,是不是很像是“兑换券”,并且只能是被信任的用户(通过client secret)可以使用Authorization code获取access token等tokens。可以根据下面这个简图来梳理下认证和授权的流程(图片来自博文)。
但是我们需要注意的一点是,在上图中资源服务器验证AccessToken的方式是资源服务器和授权服务器的Introspection Endpoint(自省终结点)通信来进行验证,而在我们的程序中使用的是JWT来验证,即授权服务器在发放JWT形式的Access Token时使用私钥进行了签名,在AccessToken发送到资源服务器时,资源服务器使用公钥校验JWT Token,从而可得到Token的状态和包含在其中的信息。在这种方式下资源服务器和授权服务器就不再需要通信了。
创建ASP.NET Core MVC
接下来我们通过代码来实践下这个模式,以下代码是在上一篇的基础上进行扩展的,使用的是.NET Core3.1版本与IDentityServer4的3.1.0.0版本进行开发。
先创建一个MVC应用作为客户端,配置其启动地址为 localhost:5002
(1)使用VS新建一个空白的MVC应用,默认就包含一个HomeController。
(2)来到Startup.cs,在ConfigureServices中添加代码
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddControllersWithViews(); 4 JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); 5 JwtSecurityTokenHandler.DefaultMapInboundClaims = false; 6 services.AddAuthentication(options => 7 { 8 options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; //常量:“Cookies” 9 options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; //常量:“OpenIdConnect” 10 }) 11 .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) //使用Cookie作为验证用户的首选方式 12 .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => 13 { 14 options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; 15 16 options.Authority = "http://localhost:5000"; //授权服务器地址 17 options.RequireHttpsMetadata = false; //暂时不用https 18 options.ClientId = "mvc"; 19 options.ClientSecret = "mvc secret"; 20 options.ResponseType = "code"; //代表Authorization Code 21 options.SaveTokens = true; //表示把获取的Token存到Cookie中 22 }); 23 }
(3)继续在Configure方法中添加代码
1 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 2 { 3 app.UseStaticFiles(); 4 app.UseRouting(); 5 app.UseAuthentication(); 6 app.UseAuthorization(); 7 app.UseEndpoints(endpoints => 8 { 9 endpoints.MapDefaultControllerRoute() 10 .RequireAuthorization(); 11 //在这里配置RequireAuthorization()的话整个网站应用都会要进行授权验证, 12 //我们也可通过在Controller上应用[Authorize]特性来达到同样的效果 13 }); 14 }
(4)修改Home的View视图代码,让视图上显示当前用户的claims(类似于属性)
@using Microsoft.AspNetCore.Authentication <h2>Claims</h2> <dl> @foreach (var claim in User.Claims) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl> <h2>Properties</h2> <dl> @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items) { <dt>@prop.Key</dt> <dd>@prop.Value</dd> } </dl>
修改授权服务器代码
在Identity Server上新增一个Client并做相关配置。
(1)在Config.cs中的Client集合中添加一个新的Client。
1 new Client 2 { 3 ClientId = "mvc", 4 ClientName = "MVC Client", 5 AllowedGrantTypes = GrantTypes.Code, 6 RequirePkce = false, 7 ClientSecrets = { new Secret("mvc secret".Sha256()) }, 8 RedirectUris = { "http://localhost:5002/signin-oidc" }, 9 FrontChannelLogoutUri = "http://localhost:5002/signout-oidc", 10 PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" }, 11 AllowOfflineAccess = true, 12 AllowedScopes = 13 { 14 "api1", 15 //因为我们要请求的资源包含用户信息,所以在scope中需要包括上 16 IdentityServer4.IdentityServerConstants.StandardScopes.OpenId, 17 IdentityServer4.IdentityServerConstants.StandardScopes.Profile, 18 IdentityServer4.IdentityServerConstants.StandardScopes.Email, 19 IdentityServer4.IdentityServerConstants.StandardScopes.Phone 20 } 21 }
(2)检查Config.cs中是否有Ids集合,如果使用Identity Server4的VS模板创建的项目的话应该是默认有配置的。
1 public static IEnumerable<IdentityResource> Ids => 2 new IdentityResource[] 3 { 4 new IdentityResources.OpenId(), 5 new IdentityResources.Profile(), 6 new IdentityResources.Email(), 7 new IdentityResources.Phone() 8 };
(3)因为这次我们要使用到用户的登录,那要确保我们添加了测试用户(生产环境应该是将用户数据放在数据库中),我们从TestUsers导航进去可以看到,模板默认为我们配置了两个用户alice和bob。
1 //在这里注册Identity Server4 2 var builder = services.AddIdentityServer(options => 3 { 4 options.Events.RaiseErrorEvents = true; 5 options.Events.RaiseInformationEvents = true; 6 options.Events.RaiseFailureEvents = true; 7 options.Events.RaiseSuccessEvents = true; 8 }); 9 10 // in-memory, code config 11 builder.AddInMemoryIdentityResources(Config.Ids); //用户的身份信息,也是受保护资源的一部分(Identity Data) 12 builder.AddInMemoryApiResources(Config.Apis); //受保护的资源(Apis) 13 builder.AddInMemoryClients(Config.Clients); //授权的用户模式 14 builder.AddTestUsers(TestUsers.Users); //增加了测试用户
查看结果
运行授权服务器程序,再运行MVC程序,可以看到我们被重定向到了Identity Server的登录界面。
在看到登录界面之前,客户端(MVC服务器)会向授权服务器Authorization Endpoint进行验证。我们使用fiddler可以看到,向Authorization Endpoint发送的请求附带着我们之前说的那些数据(包括clientid,redirect_uri等),其中还包括两项我们暂时还不熟悉的code_challenge和code_challenge_method,来为我们提供安全保障,在后续再展开讨论。
输入alice或者bob的密码之后并完成登录之后,我们便可以看到返回的资源界面了,显示了登录者的Claim信息,如下图所示。
并且这个时候我们访问Identity Server时也能看到登录信息。
我们使用Fiddler捕获在用户完成登录之后发生的事情,可以看到在登录后,授权服务器向重定向地址发送了Authorization Code等数据给MVC程序。
随后MVC向授权客户端的Token终结点发送请求,从下图可以看到这次请求包含了client_id,client_secret,code,grant_type和redirect_uri,并且在响应中可以看到access token和id token。
参考
- OAuth官方文档4.1节:https://tools.ietf.org/html/rfc6749#section-4.1
- OpenID Connect官文:https://openid.net/connect/
- https://blog.yorkxin.org/2013/09/30/oauth2-4-1-auth-code-grant-flow.html
- https://oauthlib.readthedocs.io/en/latest/oauth2/oidc/grants.html
- Identity Server4官方文档:https://identityserver4.readthedocs.io/en/latest/quickstarts/2_interactive_aspnetcore.html#
- 杨旭大佬的教程(推荐):https://www.bilibili.com/video/av42364337?p=9
- https://learnku.com/articles/20082