《Enterprise Application Pattern—using Xamarin.Forms》中文简述九——认证和授权

Authentication and authorization

认证是一个获取身份证明的过程,一般都是验证用户名和密码是否匹配。如果身份验证通过,然后授权就会决定哪些数据是这身份可以访问的。

有很多种方式可以将认证和授权组件添加到基于ASP.NET MVC的Xamarin.Forms应用程序中,如ASP.NETCore Identity,Microsoft、Google、Facebook等的认证API,或者一些其它的认证中间件。eShopOnContainers手机app则使用的是基于IdentityServer4的容器化认证微服务。app可以从IdentityServer请求一个安全的令牌(token),或者用来验证是否是一个有效的用户,也可以验证用户是否拥有访问某项资源的权限。IdentityServer如果验证到传递过来的某个token有问题,会提示让用户重新登录。但是IdentityServer并不提供可以让用户进行登录的页面,也不直接连接数据库。所以为了解决这个问题,eShopOnContainers手机app添加了一个ASP.NET Core Identity项目。

认证

当应用程序需要知道当前用户的身份时,认证就会被发起。ASP.NET Core识别用户的首选机制就是ASP.NET Core Identity membership系统,它可以将用户的信息存储在数据库中。

在ASP.NET MVC应用程序中,一般都使用cookie来记录身份信息。手机app通常无法使用这套机制,但使用bearer token则可以解决这个问题,bearer token可以可容易的从手机app发出的web request的请求头中获取或写入。

使用IdentityServer4签发bearer token

IdentityServer4是ASP.NET Core中一个整合了OpenID Connect和OAuth2.0的开源框架。它可以被用到多种需要认证和授权的场景中,包括给ASP.NET Core Identity用户签发安全的token。

备注:OpenID Connect和OAuth2.0非常相似,但是职责不同。
OpenID Connect是OAuth2.0协议上面的一个认证层。OAuth2.0是一种协议,它允许应用程序从一个安全的令牌服务上获取access token,并用这个access token与API交互数据。因为认证和授权可以被集中化,所以它降低了客户端程序和API的复杂度。OpenID Connect和OAuth2.0的结合解决了认证和API访问这两个基本问题,而IdentityServer4就是这些协议的一个实现。

在客户端直连微服务的这种应用程序中,如eShopOnContainers,会有一个专门的认证微服务来扮演Security Token Service(STS)的角色去验证一个用户是否合法。

  • 使用IdentityServer对用户进行身份验证是通过手机app请求身份令牌(identity token)来实现的,该token代表了认证过程的结果。这个token中至少包括了用户的标识符,用户进行认证的时间和方式。当然也可以包括其它的身份信息。
  • 使用IdentityServer来访问某一资源是通过请求访问令牌(access token)来实现的。客户端请求一个access token然后再用这个token去获取特定的API资源。access token包括了客户端信息和用户信息。API拿到这个token后去授权它们可以访问数据。

备注:客户端在请求token之前一定要在IdentityServer中注册。

在web application中添加IdentityServer

可以使用nuget在ASP.NET Core web application中安装IdentityServer。一旦安装成功之后,需要在Starup.cs的Configure方法中进行如下的配置:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {     
    ...     
    app.UseIdentity();    
    ... 
} 

将IdentityServer添加到了处理http请求的管道中。因为在管道中http请求的处理顺序是很重要的,所以要把IdentityServer添加到展示任何UI之前。

配置IdentityServer

需要在Startup.cs的ConfigureServices方法中,进行如下的配置:

public void ConfigureServices(IServiceCollection services) 
{     
    ...     
    services.AddIdentityServer(x => x.IssuerUri = "null")                
    .AddSigningCredential(Certificate.Get())                        
    .AddAspNetIdentity<ApplicationUser>()
    .AddConfigurationStore(builder =>builder.UseSqlServer(connectionString, options =>options.MigrationsAssembly(migrationsAssembly)))
    .AddOperationalStore(builder =>builder.UseSqlServer(connectionString, options =>options.MigrationsAssembly(migrationsAssembly)))
    .Services.AddTransient<IProfileService, ProfileService>(); 
}  

在调用services.AddIdentityServer之后,又配置了:

  • 签名所用的凭据
  • 可能会被请求到的API和身份资源
  • 会连接到IdentityServer的客户端
  • ASP.NET Core Identity

动态加载IdentityServer4的配置
在eShopOnContainers中,这些配置是代码写死了,而在生产环境中这些配置可以从配置文件或者数据库中读取。详见IdentityServer4的文档。

配置API resources

在配置API resources时,AddInMemoryApiRresources方法需要一个IEnumeralbe集合。如下:

public static IEnumerable<ApiResource> GetApis() 
{     
    return new List<ApiResource>     
    {         
        new ApiResource("orders", "Orders Service"),         
        new ApiResource("basket", "Basket Service")     
    }; 
}  

这里指定了IdentityServer需要保护ordersbasket这两个API。因此当请求这两个API时,IdentityServer就会索要并验证access token。

配置Identity resources

同理,在配置时,AddInMemoryIdentityResources方法需要一个IEnumerabl集合。Identity resources是一种类似ID,name,email之类的数据。每一个Identity resource都有一个唯一的name和一些claim,这些数据到时候都会包括在identity token中。以下是eShopOnContainer中的示例:

public static IEnumerable<IdentityResource> GetResources() 
{     
    return new List<IdentityResource>     
    {         
        new IdentityResources.OpenId(),         
        new IdentityResources.Profile()     
    }; 
}  

OpenId Connect指定了一些标准的identity resources。最低的要求是需要支持向用户发放唯一ID,这个可以通过IdentityResources.OpenId()方法实现。

备注:IdentityResources支持OpenID Connect定义的所有scope,比如openid、email、profile、telephone、address等。而且还支持自定义的Identity resources。

配置客户端

只要向IdentityServer请求token的都叫客户端,包括web client、desktop client、mobile client等。以下这几项对于客户端来说是必须要配置的:

  1. 客户端的一个唯一ID
  2. 授权类型(grant type)
  3. 需要将Identity token或者access token发送到哪里的一个url
  4. 客户端能访问的资源(即scopes)

当配置的时候,需要给AddInMemoryClients方法传入一个IEnumerable集合。如下所示:

public static IEnumerable<Client> GetClients(Dictionary<string,string> clientsUrl) 
{     
    return new List<Client>     
    {         
        ...         
        new Client         
        {             
            ClientId = "xamarin",             
            ClientName = "eShop Xamarin OpenId Client",             
            AllowedGrantTypes = GrantTypes.Hybrid,             
            ClientSecrets =             
            {                 
                new Secret("secret".Sha256())             
                
            },             
            RedirectUris = { clientsUrl["Xamarin"] },            
            RequireConsent = false,             
            RequirePkce = true,             
            PostLogoutRedirectUris = { $"{clientsUrl["Xamarin"]}/Account/Redirecting" },            
            AllowedCorsOrigins = { "http://eshopxamarin" },             
            AllowedScopes = new List<string>             
            {                 
                IdentityServerConstants.StandardScopes.OpenId,                 
                IdentityServerConstants.StandardScopes.Profile,                 
                IdentityServerConstants.StandardScopes.OfflineAccess,                 
                "orders",                 
                "basket"             
            },              
            AllowOfflineAccess = true,             
            AllowAccessTokensViaBrowser = true         
        },         
        ...     
    }; 
}

配置的意思如下:

  1. ClientId:客户端的唯一id
  2. ClientName:客户端的名称,随便写,一般用来展示和记录日志。
  3. AllowedGrantTypes:指定客户端与IdentityServer的交互类型,请见IdentityServer文档。
  4. ClientSecrets:指定当客户端获取token时,所使用的客户端密码凭据。
  5. RedirectUris:指定允许返回token和授权码(authorization code)的Uris。
  6. RequireConsent:是否显示consent页面。
  7. RequirePkce:指定客户端使用authorization code时,是否必须发送证明秘钥。
  8. PostLogoutRedirectUris:退出登录后,导向到哪个Uri。
  9. AllowedCorsOrigins:指定客户端的来源,以便IdentityServer允许跨来源(跨域)访问。
  10. AllowedScopes:指定客户端能访问哪些资源,默认客户端任何资源都不访问。
  11. AllowOfflineAccess:指定客户端是否可以请求refresh token。

配置认证流程

客户端和IdentityServer间的认证流程可以在客户端指定grant type来配置。OpenID Connect和OAuth2.0指定了一些认证流程,包括:

  • Implicit:专为基于浏览器的应用程序进行了优化,仅用户身份验证、请求access token和authentication token。因为所有的token都通过浏览器转发,所以一些高级功能如refresh token是不支持的。
  • Authorization code:提供了在后台通道(back channel)接受token的功能,而不是从浏览器的前台通道(front channel)接受,同样支持客户端认证。
  • Hybrid:是上述两种方式的结合。浏览器的前台通道用来转发Identity token和authorization code。认证成功之后,后台通道用来接受access token和refresh token。

提示:建议使用hybrid流程。相对于浏览器的前台通道来说可以减轻一些攻击,而且也是本地应用程序接受access token和refresh token的建议流程。更多信息请参考IdentityServer4文档。

开始身份认证

对于IdentityServer来说发放的token就代表了用户,所以用户必须在IdentityServer进行注册,但是IdentityServer不提供进行注册的页面。于是乎就用ASP.NET Core Identity来解决这个问题。

在eShopOnContainer手机app中,与IdentityServer的通信是由IdentityServie这个类进行的。它实现了IIdentityService接口,并实现了三个方法:CreateAuthorizationRequest,CreateLogoutRequest,GetTokenAsync

登录

当用户点击登录是,将会调用LoginViewModel的如下代码:

private async Task SignInAsync() 
{     
    ...     
    LoginUrl = _identityService.CreateAuthorizationRequest();     
    IsLogin = true;     
    ... 
} 

这里调用了_identityService的CreateAuthorizationRequest方法,具体实现是:

public string CreateAuthorizationRequest() 
{     
    // Create URI to authorization endpoint     
    var authorizeRequest = new AuthorizeRequest(GlobalSetting.Instance.IdentityEndpoint); 
 
    // Dictionary with values for the authorize request     
    var dic = new Dictionary<string, string>();     
    dic.Add("client_id", GlobalSetting.Instance.ClientId);     
    dic.Add("client_secret", GlobalSetting.Instance.ClientSecret);      
    dic.Add("response_type", "code id_token");     
    dic.Add("scope", "openid profile basket orders locations marketing offline_access");     
    dic.Add("redirect_uri", GlobalSetting.Instance.IdentityCallback);     
    dic.Add("nonce", Guid.NewGuid().ToString("N"));     
    dic.Add("code_challenge", CreateCodeChallenge());     
    dic.Add("code_challenge_method", "S256");
    
    // Add CSRF token to protect against cross-site request forgery attacks.     
    var currentCSRFToken = Guid.NewGuid().ToString("N");     
    dic.Add("state", currentCSRFToken); 
 
    var authorizeUri = authorizeRequest.Create(dic);      
    return authorizeUri; 
} 

一旦登录成功后,WebView会被导航到返回的URI上(上面的authorizeUri),然后会触发LoginViewModel的NavigateAsync方法:

private async Task NavigateAsync(string url) 
{     
    ...     
    var authResponse = new AuthorizeResponse(url);     
    if (!string.IsNullOrWhiteSpace(authResponse.Code))     
    {         
        var userToken = await _identityService.GetTokenAsync(authResponse.Code);         
        string accessToken = userToken.AccessToken; 
        if (!string.IsNullOrWhiteSpace(accessToken))         
        {             
            Settings.AuthAccessToken = accessToken;             
            Settings.AuthIdToken = authResponse.IdentityToken; 
            await NavigationService.NavigateToAsync<MainViewModel>();             
            await NavigationService.RemoveLastFromBackStackAsync();         
        }     
    }    
    ... 
} 

这个方法从返回的uri中取出了access token和identity token并存到本地。

退出

当点击退出时会调用LoginViewModel中的Logout方法:

private void Logout() 
{     
    var authIdToken = Settings.AuthIdToken;     
    var logoutRequest = _identityService.CreateLogoutRequest(authIdToken); 
    if (!string.IsNullOrEmpty(logoutRequest))     
    {         
        // Logout         
        LoginUrl = logoutRequest;
    }     
    ...
}

这段代码调用了_identityService的CreateLogoutRequest方法,并传入了identity token。方法的实现如下:

public string CreateLogoutRequest(string token) 
{     
    ...     
    return string.Format("{0}?id_token_hint={1}&post_logout_redirect_uri={2}",  GlobalSetting.Instance.LogoutEndpoint,token,GlobalSetting.Instance.LogoutCallback); 
}  

返回了一个logout之后的重定向uri给LoginUrl。然后会触发LoginViewModel的NavigateAsync方法:

private async Task NavigateAsync(string url) 
{     
    ...     
    Settings.AuthAccessToken = string.Empty;     
    Settings.AuthIdToken = string.Empty;     
    IsLogin = false;     
    LoginUrl = _identityService.CreateAuthorizationRequest();     
    ... 
} 

这里清除了本地的access token和identity token,重新赋值了LoginUrl以便重新登录。

授权

认证通过之后,ASP.NET Core web API一般也需要授权后才能访问。

可以在controller上或者action上添加[Authorize]注解,表明这个controller或者action需要授权后才能访问。如:

[Authorize] 
public class BasketController : Controller 
{     
    ... 
}

如果一个未授权的用户来访问这个BasketController,那么系统返回一个401错误。

备注:可以通过给Authorize注解添加参数的方式来限制哪些用户可以访问这个controller

access token包括了客户端信息和用户信息,API可以用这些信息来判断是否允许访问。详见前面讲过的《配置API resources》章节。

配置IdentityServer进行授权

如果要IdentiServer进行授权管理,则需要把它的中间件配置到http请求管道中,在Startup.cs文件中添加如下的虚方法:

protected virtual void ConfigureAuth(IApplicationBuilder app) 
{     
    var identityUrl = Configuration.GetValue<string>("IdentityUrl");     app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions     
    {         
        Authority = identityUrl.ToString(),         
        ScopeName = "basket",         
        RequireHttpsMetadata = false     
    }); 
} 

这个方法确保了API只能被正确的access token所访问。它会验证来访的token确保是来自于被信任的发放者,并且验证这个token能否访问这个API。因为如果你直接访问order或者basket controller的话,会直接返回401.

注意:IdentityServer的授权中间件,必须添加到http请求管理的app.UseMVC()或app.UseMvcWithDefaultRoute()前面。

请求访问API

在访问order或者basket微服务是,必须将上面过程中获取到的access token放到请求里:

var authToken = Settings.AuthAccessToken; 
Order = await _ordersService.GetOrderAsync(Convert.ToInt32(order.OrderNumber), authToken);  

同样的,当向被IdentityServer保护的API发送数据时,也要带上access token:

var authToken = Settings.AuthAccessToken; 
await _basketService.UpdateBasketAsync(new CustomerBasket 
{    
    BuyerId = userInfo.UserId,      
    Items = BasketItems.ToList() 
}, authToken);  

eShopOnContainers中的RequestProvider类专门用来发送http请求,本质是用HttpClient来实现的。将access token放到了httpClient对象的请求头里,代码如下:

httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); 

当请求被发送到RESTful API上时,请求头里的数据就会被解压认证,确保是可信的,然后再确定这个用户能否访问这个API。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JimCarter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值