在WebApi中基於Owin OAuth使用受權發放Token


如何基於Microsoft.Owin.Security.OAuth,使用Client Credentials
Grant受權方式給客戶端發放access token? Client Credentials
Grant的受權方式就是隻驗證客戶端(Client),不驗證用戶(Resource Owner),只要客戶端經過驗證就發access
token。舉一個對應的應用場景例子,好比咱們想提供一個「獲取網站首頁最新博文列表」的WebAPI給客戶端App調用。因爲這個數據與用戶無關,因此不涉及用戶登陸與受權,不須要Resource
Owner的參與。但咱們不想任何人均可以調用這個WebAPI,因此要對客戶端進行驗證,而使用OAuth中的 Client
Credentials Grant 受權方式能夠很好地解決這個問題。html

在App_Start文件夾下新增ApplicationDbInitializer,代碼以下:ajax

public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext>
    {
        protected override void Seed(ApplicationDbContext context)
        {
            InitializeIdentityForEF(context);
            base.Seed(context);
        }

        //建立用戶名爲admin@123.com,密碼爲「Admin@123456」
        public static void InitializeIdentityForEF(ApplicationDbContext dbContext)
        {
            var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
            const string name = "admin@123.com";//用戶名
            const string email = "admin@123.com";//郵箱
            const string password = "Admin@123456";//密碼

            //若是沒有admin@123.com用戶則建立該用戶
            var user = userManager.FindByName(name);
            if (user == null)
            {
                user = new ApplicationUser
                {
                    UserName = name,
                    Email = email
                };
                var result = userManager.Create(user, password);
                result = userManager.SetLockoutEnabled(user.Id, false);
            }

        }
    }

修改Model文件夾下的IdentityModels.cs,添加斜體部分代碼,需添加命名空間:using System.Data.Entity;數據庫

public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
            // 在第一次啓動網站時初始化數據庫添加管理員用戶憑據到數據庫
            Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
        }

我把WebApi的Controller放到一個新建的文件夾APIControllers中,TestController的View的js的測試代碼json

打開Startup.Auth.cs,如下代碼是Oauth相關的配置代碼api

public partial class Startup
{
    public void ConfigureAuth(IAppBuilder app)
    {
        var OAuthOptions = new OAuthAuthorizationServerOptions
            {
                //獲取Token的路徑
                TokenEndpointPath = new PathString("/Token"),
                Provider = new ApplicationOAuthProvider(PublicClientId),
                AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
                //Token 過時時間,默認20分鐘
                AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),                
                //在生產模式下設 AllowInsecureHttp = false
                AllowInsecureHttp = true
            };
        app.UseOAuthBearerTokens(OAuthOptions);
    }
}

使用Client Credentials Grant的受權方式( grant_type= client_credentials)獲取 Access Token,並以這個 Token 調用與用戶相關的 Web API。cookie

咱們須要修改部分代碼,修改ValidateClientAuthentication()方法,繼承實現GrantClientCredentials()方法。代碼以下app

public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
    {
        public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            string clientId, clientSecret;
            context.TryGetBasicCredentials(out clientId, out clientSecret);
            if (clientId == "Mobile" && clientSecret == "Xiaomi")
            {
                context.Validated();
            }
            return Task.FromResult<object>(null);
        }

        public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
        {
            var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
            oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, "Xiaomi"));
            var ticket = new AuthenticationTicket(oAuthIdentity, new AuthenticationProperties());
            context.Validated(ticket);
            return base.GrantClientCredentials(context);
        }
    }

在 ValidateClientAuthentication() 方法中獲取客戶端的 client_id 與 client_secret 進行驗證。在 GrantClientCredentials() 方法中對客戶端進行受權,授了權就能發 access token 。這樣,OAuth的ClientCredentials受權服務端代碼就完成了。在ASP.NET Web API中啓用OAuth的Access Token驗證很是簡單,只需在相應的Controller或Action加上[Authorize]標記,VS已生成部分代碼,詳細查看APIController文件夾下的ValuesControllerasync

下面咱們在客戶端調用一下,添加TestController,生成Index的View,而後在View中添加以下ide

$(function () {
            $("#clientCredentials").on("click", function () {
                GetClientCredentialsAccessToken();
            });
        });

        function GetClientCredentialsAccessToken() {
            $("#clientResult").html("Requesting");
            var clientId = "Mobile";
            var clientSecret = "Xiaomi";
            $.ajax({
                url: "/Token",
                type: "post",
                data: { "grant_type": "client_credentials" },
                dataType: "json",
                headers: {
                    "Authorization": "Basic " + Base64_Encode(clientId + ":" + clientSecret)
                },
                success: function (data) {
                    var accessToken = data.access_token;
                    GetValues(accessToken);
                }
            });
        }


        function GetValues(accessToken) {
            var html = "Token:" + accessToken + "<br/><br/>";
            $.ajax({
                url: "/api/Values",
                type: "get",
                dataType: "json",
                headers: {
                    "Authorization": "Bearer " + accessToken
                },
                success: function (values) {
                    for (var i = 0; i < values.length; i++) {
                        html += "values[" + i + "] :" + values[i] + "<br/>";
                    }
                    $("#clientResult").html(html);
                }
            });
        }
        function Base64_Encode(str) {
            var c1, c2, c3;
            var base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
            var i = 0, len = str.length, string = '';

            while (i < len) {
                c1 = str.charCodeAt(i++) & 0xff;
                if (i === len) {
                    string += base64EncodeChars.charAt(c1 >> 2);
                    string += base64EncodeChars.charAt((c1 & 0x3) << 4);
                    string += "==";
                    break;
                }
                c2 = str.charCodeAt(i++);
                if (i === len) {
                    string += base64EncodeChars.charAt(c1 >> 2);
                    string += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
                    string += base64EncodeChars.charAt((c2 & 0xF) << 2);
                    string += "=";
                    break;
                }
                c3 = str.charCodeAt(i++);
                string += base64EncodeChars.charAt(c1 >> 2);
                string += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
                string += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
                string += base64EncodeChars.charAt(c3 & 0x3F);
            }
            return string;
        }

測試結果:post

Token:4iIu7HProfJaxRiklsl-ORRO3hdyrsu50pQc1Eh2-Q5lSWK8UJgz6719ZaeeULhwkMPpEFYfk6QDOOMEyFqULULk65Sb0JY29wskyZyQhKJ3_P-eSVQ2PlbKbjH9ZcziAZsVOiNLp8CfUqL5qWUq8ggVAa8KRcnlJ1DIVWnEu0XvTEDZaLDpFqqj2Cex2CX7TmTgfs07RUBdx5_3WDavNA

Ps:
傳遞clientId與clientSecret有兩種方式,上例使用BasicAuthentication,服務端使用TryGetBasicCredentials();另一種方式是普通From的,把參數放到Ajax的data中,如:

{「clientId」: id ,」 clientSecret」:」secret」, "grant_type":"client_credentials"}

對應服務端使用TryGetFormCredentials()獲取clientId和clientSecret;

推薦使用Basic Authentication方式;


使用Resource Owner Password Credentials Grant 的受權方式( grant_type=password )獲取 Access Token,並以這個 Token 調用與用戶相關的 Web API。
Resource Owner Password Credentials Grant 受權方式(須要驗證登陸用戶)

由於咱們剛開始時已經初始化EF,添加了一個用戶信息。ApplicationOAuthProvider.cs 的GrantResourceOwnerCredentials()方法(VS幫咱們自動生成了),已經實現了先關的代碼

public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            //調用後臺的登陸服務驗證用戶名與密碼

            var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
            ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
            if (user == null)
            {
                context.SetError("invalid_grant", "用戶名或密碼不正確。");
                return;
            }
            ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager, OAuthDefaults.AuthenticationType);
            ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager, CookieAuthenticationDefaults.AuthenticationType);
            AuthenticationProperties properties = CreateProperties(user.UserName);
            AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
            context.Validated(ticket);
            context.Request.Context.Authentication.SignIn(cookiesIdentity);
        }

添加一個測試用的Controller

public class UsersController : ApiController
{
    [Authorize]
    public string GetCurrent()
    {
        return User.Identity.Name;
        //這裏能夠調用後臺用戶服務,獲取用戶相關數所,或者驗證用戶權限進行相應的操做
    }
}

在Test的index.cshtml 中新增測試的代碼,以下

function GetResourceOwnerCredentialsAccessToken() {
            $("#resourceOwnerresult").html("Requesting");
            var clientId = "Mobile";
            var clientSecret = "Xiaomi";
            $.ajax({
                url: "/Token",
                type: "post",
                data: {
                    "grant_type": "password",
                    "username": "admin@123.com",
                    "password": "Admin@123456"
                },
                dataType: "json",
                headers: {
                    "Authorization": "Basic " + Base64_Encode(clientId + ":" + clientSecret)
                },
                success: function (data) {
                    var accessToken = data.access_token;
                    GetCurrentUserName(accessToken);
                }
            });
        }

        function GetCurrentUserName(accessToken) {
            var html = "Token:" + accessToken + "<br/><br/>";
            $.ajax({
                url: "/api/User",
                type: "get",
                dataType: "text",
                headers: {
                    "Authorization": "Bearer " + accessToken
                },
                success: function (userName) {
                    html += "CurrentUserName:" + userName + "<br/>";
                    $("#resourceOwnerresult").html(html);
                }
            });
        }

測試結果以下

Token:Cvct6BAKix_xLNEEOfidpEG0ymJihOSjdACazP2R2tJSB3TKVnxicgQK27DzDrICUC4A7vITqhkhBRsT5cRgiow--VkbiR4we3yQ54tc6B_W8KRrdGabjase_gpmFv8oYUPGLpI82acDpcZPzCkmgLLwAq8qfkmlK7iHm5tLM6-NRR8tgfEeOVBljHq4smIXw_eVuces3sRQm-PXTD4xmp05JdrJ9zFeRb_SAN0ADqDJfJxk1nNooCtdJyeHB6r1S2D81H6P7bhRK_edneWdkX5QCNBHL8b39UKnnk0ywza6vXcWct4RaATBYOw20iNu0XR6JRx5opP9vqqC2ag8Ux6s3GHl-vAZTaYuwunmWyY0FyJJWpjNnFpPo-pkxZaK1XJxgGPpSV-JJjEZLarnq9O57hQGfbVLCd3KtWuJflo5rMnfkAz2nXlcd3gAgjIhipAIlpsG72StzN0qBL8Ml2XvV9Re1Z8U4QtrE7tzjkE

CurrentUserName:"admin@123.com"

至此,使用WebApi 的兩種受權方式發放Token和兩種受權方式區別,以及使用Token調用受保護的api已經介紹完了,Oauth其實還有一個refresh token,refresh token 是專用於刷新 access token 的 token。一是由於 access token 是有過時時間的,到了過時時間這個 access token 就失效,須要刷新;二是由於一個 access token 會關聯必定的用戶權限,若是用戶受權更改了,這個 access token 須要被刷新以關聯新的權限。
這個就不單獨介紹了,有興趣的能夠本身研究下。

Asp.Net 高級技術羣 89336052,羣共享有源碼

感謝:晨風的指導和教育,提供demo和文章,我只是一個發佈文章的戰五渣!

相關文章
相關標籤/搜索