在WebApi中基于Owin OAuth使用授權(quán)發(fā)放Token

OWIN的全稱是Open Web Interface For .Net, 是MS在VS2013期間引入的全新的概念, 網(wǎng)上已經(jīng)有不少的關(guān)于它的信息, 這里我就談下我自己的理解:

  • OWIN****是一種規(guī)范和標(biāo)準(zhǔn), ****不代表特定技術(shù). MS最新出現(xiàn)的一些新的技術(shù), 比如Kanata, Identity, SignalR, 它們只是基于OWIN的不同實(shí)現(xiàn).
  • OWIN****的核心理念是解耦,協(xié)作和開放---這和MS以前的風(fēng)格大相徑庭,值得引起大家的注意。
  • OWIN****是MS****未來Web****開發(fā)的方向,想跟著MS路線繼續(xù)開發(fā)Web應(yīng)用,OWIN是大勢所趨。

在這篇博文中,我們將以 OAuth 的** Resource Owner Password Credentials Grant **的授權(quán)方式( grant_type=password )獲取 Access Token,并以這個(gè) Token 調(diào)用與用戶相關(guān)的 Web API。
Resource Owner Password Credentials 這種模式要求用戶提供用戶名和密碼來交換訪問令牌(access_token)。該模式僅用于非常值得信任的用戶,例如API提供者本人所寫的移動應(yīng)用。雖然用戶也要求提供密碼,但并不需要存儲在設(shè)備上。因?yàn)槌跏简?yàn)證之后,只需將OAuth的令牌記錄下來即可。如果用戶希望取消授權(quán),因?yàn)槠湔鎸?shí)密碼并沒有被記錄,因此無需修改密碼就可以立即取消授權(quán)。token本身也只是得到有限的授權(quán),因此相比最傳統(tǒng)的username/password授權(quán),該模式依然更為安全。

對應(yīng)的應(yīng)用場景是:為自家的網(wǎng)站開發(fā)手機(jī) App(非第三方 App),只需用戶在 App 上登錄,無需用戶對 App 所能訪問的數(shù)據(jù)進(jìn)行授權(quán)。

基本流程

365537-20150923105340553-785686353.jpg
  • A. 向用戶索要認(rèn)證信息
    首先,我們必須得讓用戶將認(rèn)證信息提供給應(yīng)用程序。對于應(yīng)用方來說,如果用戶處于不可信的網(wǎng)絡(luò)中時(shí),除了需要輸入用戶名和密碼外,還需要用戶提供一個(gè)安全令牌作為用戶的第三個(gè)輸入。
  • B. 交換訪問令牌
    這里的訪問令牌交換過程與授權(quán)碼類型的驗(yàn)證授權(quán)(authorization code)很相似。我們要做的就是向認(rèn)證服務(wù)器提交一個(gè)POST請求并在其中提供相應(yīng)的認(rèn)證和客戶信息。

所需的POST參數(shù):
**grant_type 該模式下為"password" **
scope 業(yè)務(wù)訪問控制范圍,是一個(gè)可選參數(shù)
client_id 應(yīng)用注冊時(shí)獲得的客戶id
client_secret 應(yīng)用注冊時(shí)獲得的客戶密鑰
username 用戶的用戶名
password 用戶的密碼

POST https://xxx.com/token HTTP/1.1Content-type:application/x-www-form-urlencodedAuthorization Basic Base64(clientId:clientSecret)username=irving&password=123456&grant_type=password
  • C. 刷新Token
    1).accesstoken 是有過期時(shí)間的,到了過期時(shí)間這個(gè) access token 就失效,需要刷新。
    2).如果accesstoken會關(guān)聯(lián)一定的用戶權(quán)限,如果用戶授權(quán)更改了,這個(gè)accesstoken需要被刷新以關(guān)聯(lián)新的權(quán)限。
    3).為什么要專門用一個(gè) token 去更新 accesstoken 呢?如果沒有 refreshtoken,也可以刷新 accesstoken,但每次刷新都要用戶輸入登錄用戶名與密碼,客戶端直接用 refreshtoken 去更新 accesstoken,無需用戶進(jìn)行額外的操作。
POST http://localhost:19923/tokenContent-Type: Application/x-www-form-
urlencodedAuthorization Basic Base64(clientId:clientSecret)username=irving&password=123456&grant_type=refresh_token

備注:
有了前面相關(guān)token,服務(wù)調(diào)用也很簡單

GET https://xxx.com/api/v1/account/profile HTTP/1.1Content-type:application/x-www-form-urlencodedAuthorization Authorization: Bearer {THE TOKEN}

WebApi Startup

get access_token

在 C# 中用 HttpClient 實(shí)現(xiàn)一個(gè)簡單的客戶端,代碼如下:

using System;
using System.Collections.Generic;
using System.Net.Http;

namespace Tdf.OAuthClientTest
{
    public class OAuthClientTest
    {
        private HttpClient _httpClient;

        public OAuthClientTest()
        {
            _httpClient = new HttpClient();
            _httpClient.Timeout = TimeSpan.FromMinutes(30);

            _httpClient.BaseAddress = new Uri("http://localhost:13719");
        }

        public void Get_Accesss_Token_By_Client_Credentials_Grant()
        {
            var parameters = new Dictionary<string, string>();
            parameters.Add("UserName", "Bobby");
            parameters.Add("Password", "123");
            parameters.Add("grant_type", "password");

            Console.WriteLine(_httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters))
                .Result.Content.ReadAsStringAsync().Result);
        }

    }
}

這樣,運(yùn)行客戶端程序就可以拿到 Access Token 了。


refresh_token

在 C# 中用 HttpClient 實(shí)現(xiàn)一個(gè)簡單的客戶端,代碼如下:

using System;
using System.Collections.Generic;
using System.Net.Http;

namespace Tdf.OAuthClientTest
{
    public class OAuthClientTest
    {
        private HttpClient _httpClient;

        public OAuthClientTest()
        {
            _httpClient = new HttpClient();
            _httpClient.Timeout = TimeSpan.FromMinutes(30);

            _httpClient.BaseAddress = new Uri("http://localhost:13719");
        }

        public void Get_Accesss_Token_By_Client_Credentials_Grant()
        {
            var parameters = new Dictionary<string, string>();

            // refresh_token
            parameters.Add("grant_type", "refresh_token");
            parameters.Add("refresh_token", "DAB1FE2B-2F84-4534-A620-F6B9B474B503");

            Console.WriteLine(_httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters))
                .Result.Content.ReadAsStringAsync().Result);
        }

    }
}

這樣,運(yùn)行客戶端程序就可以拿到 Access Token 了。


OWIN WEBAPI

默認(rèn)Startup.Auth.cs

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Google;
using Microsoft.Owin.Security.OAuth;
using Owin;
using Ems.Web.Providers;
using Ems.Web.Models;

namespace Ems.Web
{
    /// <summary>
    /// Startup
    /// </summary>
    public partial class Startup
    {
        /// <summary>
        /// OAuthOptions
        /// </summary>
        public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }

        /// <summary>
        /// PublicClientId
        /// </summary>
        public static string PublicClientId { get; private set; }

        /// <summary>
        /// ConfigureAuth
        /// 有關(guān)配置身份驗(yàn)證的詳細(xì)信息,請?jiān)L問 http://go.microsoft.com/fwlink/?LinkId=301864
        /// </summary>
        /// <param name="app"></param>
        public void ConfigureAuth(IAppBuilder app)
        {
            // 將數(shù)據(jù)庫上下文和用戶管理器配置為對每個(gè)請求使用單個(gè)實(shí)例
            app.CreatePerOwinContext(ApplicationDbContext.Create);
            app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

            // 使應(yīng)用程序可以使用 Cookie 來存儲已登錄用戶的信息
            // 并使用 Cookie 來臨時(shí)存儲有關(guān)使用第三方登錄提供程序登錄的用戶的信息
            app.UseCookieAuthentication(new CookieAuthenticationOptions());
            app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

            // 針對基于 OAuth 的流配置應(yīng)用程序
            PublicClientId = "self";
            OAuthOptions = new OAuthAuthorizationServerOptions
            {
                TokenEndpointPath = new PathString("/Token"),
                Provider = new ApplicationOAuthProvider(PublicClientId),
                AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
                AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
                //在生產(chǎn)模式下設(shè) AllowInsecureHttp = false
                AllowInsecureHttp = true
            };

            // 使應(yīng)用程序可以使用不記名令牌來驗(yàn)證用戶身份
            app.UseOAuthBearerTokens(OAuthOptions);

            // 取消注釋以下行可允許使用第三方登錄提供程序登錄
            //app.UseMicrosoftAccountAuthentication(
            //    clientId: "",
            //    clientSecret: "");

            //app.UseTwitterAuthentication(
            //    consumerKey: "",
            //    consumerSecret: "");

            //app.UseFacebookAuthentication(
            //    appId: "",
            //    appSecret: "");

            //app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
            //{
            //    ClientId = "",
            //    ClientSecret = ""
            //});
        }
    }
}

Startup.cs

using Microsoft.Owin;
using Microsoft.Owin.Security.OAuth;
using Microsoft.Practices.Unity;
using Owin;
using System;
using System.Web.Http;
using Tdf.Application.Act.UserMgr;
using Tdf.Utils.Config;
using Tdf.WebApi.Providers;
using Unity.WebApi;

[assembly: OwinStartup(typeof(Tdf.WebApi.Startup))]

namespace Tdf.WebApi
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // 有關(guān)如何配置應(yīng)用程序的詳細(xì)信息,請?jiān)L問 http://go.microsoft.com/fwlink/?LinkID=316888
            var config = new HttpConfiguration();
            WebApiConfig.Register(config);

            IUnityContainer container = UnityConfig.GetConfiguredContainer();
            config.DependencyResolver = new UnityDependencyResolver(container);
            ConfigureOAuth(app, container);

            // 這一行代碼必須放在ConfiureOAuth(app)之后
            // Microsoft.AspNet.WebApi.Owin
            app.UseWebApi(config);
        }

        public void ConfigureOAuth(IAppBuilder app, IUnityContainer container)
        {
            IUserAppService userService = container.Resolve<IUserAppService>();
            OAuthAuthorizationServerOptions oAuthServerOptions = new OAuthAuthorizationServerOptions()
            {
#if DEBUG
                AllowInsecureHttp = true,
#endif
                TokenEndpointPath = new PathString("/token"),
                AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(double.Parse(ConfigHelper.GetValue("TokenExpireMinute", "120"))),
                RefreshTokenProvider = new TdfRefreshTokenProvider(userService),
                Provider = new TdfAuthorizationServerProvider(userService)

            };

            // Token Generation
            app.UseOAuthAuthorizationServer(oAuthServerOptions);

            var opts = new OAuthBearerAuthenticationOptions()
            {
                Provider = new TdfOAuthBearerProvider("Token")
            };
            app.UseOAuthBearerAuthentication(opts);

        }
    }
}

TdfAuthorizationServerProvider.cs

using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OAuth;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Tdf.Application.Act.UserMgr;
using Tdf.Application.Act.UserMgr.Dtos;
using Tdf.Domain.Act.Entities;

namespace Tdf.WebApi.Providers
{
    /// <summary>
    /// Resource Owner Password Credentials Grant 授權(quán)
    /// </summary>
    public class TdfAuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        /// <summary>
        /// Password Grant 授權(quán)服務(wù)
        /// </summary>
        readonly IUserAppService _userService;

        /// <summary>
        /// 構(gòu)造函數(shù)
        /// </summary>
        /// <param name="userService">Password Grant 授權(quán)服務(wù)</param>
        public TdfAuthorizationServerProvider(IUserAppService userService)
        {
            this._userService = userService;
        }

        /// <summary>
        /// Resource Owner Password Credentials Grant 的授權(quán)方式;
        /// 驗(yàn)證用戶名與密碼 [Resource Owner Password Credentials Grant[username與password]|grant_type=password&username=irving&password=654321]
        /// 重載 OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials() 方法即可
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            var user = await _userService.Login(new LoginInput() { UserName = context.UserName, Password = context.Password });
            var userInfo = user.Result as User;
            if (user.ErrCode != 0 || userInfo == null)
            {
                context.SetError("invalid_grant", "The user name or password is incorrect.");
                return;
            }
            else
            {
                // 驗(yàn)證context.UserName與context.Password 
                // 調(diào)用后臺的登錄服務(wù)驗(yàn)證用戶名與密碼
                var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
                oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, userInfo.Id.ToString()));
                oAuthIdentity.AddClaim(new Claim("UserId", userInfo.Id.ToString()));
                oAuthIdentity.AddClaim(new Claim("UserName", userInfo.UserName));

                var props = new AuthenticationProperties(new Dictionary<string, string> {
                    { "user_id", userInfo.Id.ToString() },
                    { "user_name", userInfo.UserName }
                });

                var ticket = new AuthenticationTicket(oAuthIdentity, props);

                context.Validated(ticket);

                await base.GrantResourceOwnerCredentials(context);
            }
        }

        /// <summary>
        /// 把Context中的屬性加入到token中
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task TokenEndpoint(OAuthTokenEndpointContext context)
        {
            foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
            {
                context.AdditionalResponseParameters.Add(property.Key, property.Value);
            }

            return Task.FromResult<object>(null);
        }

        /// <summary>  
        /// 驗(yàn)證客戶端 [Authorization Basic Base64(clientId:clientSecret)|Authorization: Basic 5zsd8ewF0MqapsWmDwFmQmeF0Mf2gJkW]
        /// 對third party application 認(rèn)證,  
        /// 為third party application頒發(fā)appKey和appSecrect,在此省略了頒發(fā)appKey和appSecrect的環(huán)節(jié),  
        /// 認(rèn)為所有的third party application都是合法的  
        /// </summary>  
        /// <param name="context"></param>  
        /// <returns></returns>  
        public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            // 表示所有允許此third party application請求  
            context.Validated();
            return Task.FromResult<object>(null);
        }

    }
}

TdfRefreshTokenProvider.cs

using Microsoft.Owin.Security.Infrastructure;
using System;
using System.Threading.Tasks;
using Tdf.Application.Act.UserMgr;
using Tdf.Domain.Act.Entities;
using Tdf.Utils.Config;
using Tdf.Utils.GuidHelper;

namespace Tdf.WebApi.Providers
{
    /// <summary>
    /// 刷新Token
    /// 生成與驗(yàn)證Token
    /// </summary>
    public class TdfRefreshTokenProvider : AuthenticationTokenProvider
    {
        /// <summary>
        /// 授權(quán)服務(wù)
        /// </summary>
        private IUserAppService _userService;

        /// <summary>
        /// 構(gòu)造函數(shù)
        /// </summary>
        /// <param name="userService">授權(quán)服務(wù)</param>
        public TdfRefreshTokenProvider(IUserAppService userService)
        {
            this._userService = userService;
        }

        /// <summary>
        /// 創(chuàng)建refreshToken
        /// </summary>
        /// <param name="context">上下文</param>
        /// <returns></returns>
        public override async Task CreateAsync(AuthenticationTokenCreateContext context)
        {
            if (string.IsNullOrEmpty(context.Ticket.Identity.Name)) return;

            var refreshTokenLifeTime = ConfigHelper.GetValue("TokenExpireMinute", "120");
            if (string.IsNullOrEmpty(refreshTokenLifeTime)) return;

            // generate access token
            var refreshTokenId = new RegularGuidGenerator().Create();

            context.Ticket.Properties.IssuedUtc = DateTime.UtcNow;
            context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddMinutes(double.Parse(refreshTokenLifeTime));

            var refreshToken = new RefreshToken()
            {
                Id = refreshTokenId,
                UserId = new Guid(context.Ticket.Identity.Name),
                IssuedUtc = DateTime.Parse(context.Ticket.Properties.IssuedUtc.ToString()),
                ExpiresUtc = DateTime.Parse(context.Ticket.Properties.ExpiresUtc.ToString()),
                ProtectedTicket = context.SerializeTicket()
            };

            // Token沒有過期的情況強(qiáng)行刷新,刪除老的Token保存新的Token
            var jsonMsg = await _userService.SaveTokenAsync(refreshToken);
            if (jsonMsg.ErrCode == 0)
            {
                context.SetToken(refreshTokenId.ToString());
            }
        }

        /// <summary>
        /// 刷新refreshToken[刷新access token時(shí),refresh token也會重新生成]
        /// </summary>
        /// <param name="context">上下文</param>
        /// <returns></returns>
        public override async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
        {
            var jsonMsg = await _userService.GetToken(context.Token);
            if (jsonMsg.ErrCode == 0)
            {
                var refreshToken = jsonMsg.Result as RefreshToken;
                if (refreshToken != null)
                {
                    context.DeserializeTicket(refreshToken.ProtectedTicket);
                    await _userService.RemoveToken(context.Token);
                }
            }
        }
    }
}

TdfOAuthBearerProvider.cs

using Microsoft.Owin.Security.OAuth;
using System.Threading.Tasks;

namespace Tdf.WebApi.Providers
{
    public class TdfOAuthBearerProvider : OAuthBearerAuthenticationProvider
    {
        readonly string _name;

        public TdfOAuthBearerProvider(string name)
        {
            _name = name;
        }

        public override Task RequestToken(OAuthRequestTokenContext context)
        {
            var value = context.Request.Query.Get(_name);
            if (!string.IsNullOrEmpty(value))
            {
                context.Token = value;
            }
            return Task.FromResult<object>(null);
        }
    }
}

結(jié)合 ASP.NET 現(xiàn)有的安全機(jī)制,借助 OWIN 的威力,Microsoft.Owin.Security.OAuth 的確讓開發(fā)基于 OAuth 的 Web API 變得更簡單。

更多資料和資源

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容