OAuth2.0 Owin 受權問題

http://www.cnblogs.com/dudu/p/4569857.htmlhtml

OAuth2.0ios

1、什麼是OAuthgit

OAuth是一個關於受權(Authorization)的開放網絡標準,目前的版本是2.2版。github

注意是Authorization(受權),而不是Authentication(認證)。web

用來作Authentication(認證)的標準叫作openid connect。json

 

2、名詞定義api

理解OAuth中的專業術語可以幫助你理解其流程模式,OAuth中經常使用的名詞術語有4個,爲了便於理解這些術語,咱們先假設一個很常見的受權場景:安全

你訪問了一個日誌網站(third party application),你(client)以爲這個網站很不錯,準備之後就要在這個網站上寫日誌了,因此你準備把QQ空間(Resource owner)裏面的日誌都導入進來。此日誌網站想要導入你在QQ空間中的日誌須要知道你的QQ用戶名和密碼才行,爲了安全期間你不會把你的QQ用戶名和密碼直接輸入在日誌網站中,因此日誌網站幫你導航到了QQ認證界面(Authorization Server),當你輸入完用戶名和密碼後,QQ認證服務器返回給日誌網站一個token, 該日誌網站憑藉此token來訪問你在QQ空間中的日誌。服務器

  1. third party application 第三方的應用,想要的到Resource owner的受權
  2. client 表明用戶
  3. Resource owner 資源擁有者,在這裏表明QQ
  4. Authorization server 認證服務,這裏表明QQ認證服務,Resource owner和Authorization server能夠是不一樣的服務器,也能夠是同一個服務器。

3、OAuth2.0 中的四種模式網絡

OAuth定義了四種模式,覆蓋了全部的受權應用場景:

  1. 受權碼模式(authorization code)
  2. 簡化模式(implicit)
  3. 密碼模式(resource owner password credentials)
  4. 客戶端模式(client credentials)

前面咱們假設的場景能夠用前兩種模式來實現,不一樣之處在於:

當日志網站(third party application)有服務端,使用模式1;

當日志網站(third party application)沒有服務端,例如純的js+html頁面須要採用模式2;

本文主描述利用OAuth2.0實現本身的WebApi認證服務,前兩種模式使用場景不符合咱們的需求。

 

4、選擇合適的OAuth模式打造本身的webApi認證服務

場景:你本身實現了一套webApi,想供本身的客戶端調用,又想作認證。

這種場景下你應該選擇模式3或者4,特別是當你的的客戶端是js+html應該選擇3,當你的客戶端是移動端(ios應用之類)能夠選擇3,也能夠選擇4。

密碼模式(resource owner password credentials)的流程:

這種模式的流程很是簡單:

  1. 用戶向客戶端(third party application)提供用戶名和密碼。
  2. 客戶端將用戶名和密碼發給認證服務器(Authorization server),向後者請求令牌(token)。
  3. 認證服務器確認無誤後,向客戶端提供訪問令牌。
  4. 客戶端持令牌(token)訪問資源。

此時third party application表明咱們本身的客戶端,Authorization server和Resource owner表明咱們本身的webApi服務。咱們在日誌網站的場景中提到:用戶不能直接爲日誌網站(third party application)提供QQ(resource owner)的用戶名和密碼。而此時third party application、authorization server、resource owner都是一家人,Resource owner對third party application足夠信任,因此咱們才能採起這種模式來實現。

 

5、使用owin來實現密碼模式

owin集成了OAuth2.0的實現,因此在webapi中使用owin來打造authorization無疑是最簡單最方便的方案。

  1. 新建webApi項目
  2. 安裝Nuget package:

    Microsoft.AspNet.WebApi.Owin

    Microsoft.Owin.Host.SystemWeb

  3. 增長owin的入口類:Startup.cs

在項目中新建一個類,命名爲Startup.cs,這個類將做爲owin的啓動入口,添加下面的代碼

using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;
using Microsoft.Owin.Security.OAuth;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using ADT.TuDou.API.Auth;

[assembly: OwinStartup(typeof(ADT.TuDou.API.Startup))]
namespace ADT.TuDou.API
{
    public class Startup
    {
        //ConfigureOAuth(IAppBuilder app)方法開啓了OAuth服務。簡單說一下OAuthAuthorizationServerOptions中各參數的含義:
        //AllowInsecureHttp:容許客戶端使用http協議請求;
        //TokenEndpointPath:token請求的地址,即http://localhost:端口號/token;
        //AccessTokenExpireTimeSpan :token過時時間;

        //註冊用戶 http://localhost:33590/api/account/register)
        //content-type: application/json
        //{"UserName":"jay","Password":"xsj1989","ConfirmPassword":"xsj1989"}


        //獲取token方法
        //獲取token的地址:http://localhost:33590/token
        //提交方式:POST
        //參數:grant_type=password&username=jay&password=xsj1989
        //內容類型:content-type: application/x-www-form-urlencoded
        //返回數據:{"access_token":"JznxS2sYbU9fItS-ihnHB6kPnqzFd-C8uZGargqv2TF6mrNhvZYLY4OG1VclTh2PYkqLgeGzZQrnicf633coKEiJsHQCVZQTMHwli1uNQ3fJF2t0ab3CIO7Kj8y2ZvCS5ypOLAOuKkpkP1oAgCJHMkVOMZRbPfj1tqijPSt1EKcRPfzZMcOo0-OxYmbqjBrkHoB-18ZApYy4kyG6g7cHX-kh3Fq4TEAFeShfk5lOn7NKJxUJf9RWs---tCWwcqWVI-XwA3am0G8KW95-OEDq6d1gr2qHxeK020bhbvQ-OWSiR8MEq617wi-jWqdngdl_","token_type":"bearer","expires_in":1799,"as:client_id":"","userName":"jay",".issued":"Thu, 17 Mar 2016 06:19:04 GMT",".expires":"Thu, 17 Mar 2016 06:49:04 GMT"}

        //使用token訪問受保護的接口
        //http://localhost:33590/api/orders
        //User-Agent: Fiddler
        //Host: localhost:33590
        //Content-Length: 51
        //content-type: application/x-www-form-urlencoded
        //Authorization: Bearer UcaIhVTpveBVlAVEhwp1iELcd2jMhHLzRdKKcTmja1Ii5520PQ6fr56kt1mN_7O92WBrkg0AkR45i1BvPmiuIAtVCk-aJsvKd7w0uaBJIGnQBycjR3WyW-plFtlGrErtEbxTOjLe4QpLgn6ofTB61wK4MS7RR91skVEhIUt4NPY0gYKn_EqE1ihPoOMuYAIciQUmQH9aKDyo3tYFjDrhtRGS6SfSBoWFdRaIPEOtQvFG4KMnbCO1XymYAsDS0vDnUZ_BgcQAYC_PbYfNRCTGfAkwDc4hidiotwde0---nPpUt2YXbFfI8oWQ48Jgi_Fk

        //1.刷新Token,再次請求:http://localhost:33590/token
        //返回數據:
        //{"access_token":"vZPcwG6szzTaybqPWsS7ESlxMKZa7kYDHTwwbXzyEes8-9kwPTCUaf3a8vbq8qp99l265jwzqXMDzMmWa89kmCPHMI-82OYgZVhw86qtbYUGTvFEwHEyysmGD9MAH524CBbaDsCJSl1sg-VaBIC8wgl1pAHZTh56__iHj3ASUhTvphT68GpU6TyhVhnIuBTonkVoE7vjpNjIdzvwGFshHGrJKS84iZYTKlM9Kv7AItrDsRon-QwfgQOmCZ2ceCvUH8sGI5O2CYJVYhH8p7TRGLgBa4p0LYnbMC54xFyO8ZmjXr7vpG0LVTsyd0q-c6pd","token_type":"bearer","expires_in":1799,"refresh_token":"d5fb57fcc7084d128ad70e5e65643045","as:client_id":"","userName":"jay",".issued":"Fri, 18 Mar 2016 02:28:21 GMT",".expires":"Fri, 18 Mar 2016 02:58:21 GMT"}
        //包含數據:"refresh_token":"d5fb57fcc7084d128ad70e5e65643045"
        //當token過時後,憑藉上次獲得的refresh_token從新獲取token

        //2.再次請求:http://localhost:33590/token
        //參數:grant_type=refresh_token&refresh_token=d5fb57fcc7084d128ad70e5e65643045
        //報錯:invalid_grant  暫時沒解決


        //Provider :提供具體的認證策略;
        public void Configuration(IAppBuilder app)
        {
            var config = new HttpConfiguration();
            WebApiConfig.Register(config);

            ConfigureOAuth(app);

            //這一行代碼必須放在ConfiureOAuth(app)以後
            app.UseWebApi(config);
        }


        public void ConfigureOAuth(IAppBuilder app)
        {
            OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
            {
                AllowInsecureHttp = true,
                TokenEndpointPath = new PathString("/token"),
                AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),
                Provider = new SimpleAuthorizationServerProvider(),

                //refresh token provider
                RefreshTokenProvider = new SimpleRefreshTokenProvider()
            };

            // Token Generation
            app.UseOAuthAuthorizationServer(OAuthServerOptions);
            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

            //ConfigureOAuth(IAppBuilder app)方法開啓了OAuth服務。簡單說一下OAuthAuthorizationServerOptions中各參數的含義:
            //AllowInsecureHttp:容許客戶端使用http協議請求;
            //TokenEndpointPath:token請求的地址,即http://localhost:端口號/token;
            //AccessTokenExpireTimeSpan :token過時時間;
            //Provider :提供具體的認證策略;
        }
    }


    public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            context.Validated();
            return Task.FromResult<object>(null);
        }

        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            using (AuthRepository _repo = new AuthRepository())
            {
                IdentityUser user = await _repo.FindUser(context.UserName, context.Password);

                if (user == null)
                {
                    context.SetError("invalid_grant", "The user name or password is incorrect.");
                    return;
                }
            }

            var identity = new ClaimsIdentity(context.Options.AuthenticationType);
            identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
            identity.AddClaim(new Claim(ClaimTypes.Role, "user"));
            identity.AddClaim(new Claim("sub", context.UserName));

            var props = new AuthenticationProperties(new Dictionary<string, string>
            {
                {
                    "as:client_id", context.ClientId ?? string.Empty
                },
                {
                    "userName", context.UserName
                }
            });

            var ticket = new AuthenticationTicket(identity, props);
            context.Validated(ticket);
        }

        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);
        }
    }

    public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
    {
        public async Task CreateAsync(AuthenticationTokenCreateContext context)
        {
            var refreshTokenId = Guid.NewGuid().ToString("n");

            using (AuthRepository _repo = new AuthRepository())
            {
                var token = new RefreshToken()
                {
                    Id = refreshTokenId.GetHashCode(),
                    Subject = context.Ticket.Identity.Name,
                    IssuedUtc = DateTime.UtcNow,
                    ExpiresUtc = DateTime.UtcNow.AddMinutes(30)
                };

                context.Ticket.Properties.IssuedUtc = token.IssuedUtc;
                context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc;

                token.ProtectedTicket = context.SerializeTicket();

                var result = await _repo.AddRefreshToken(token);

                if (result)
                {
                    context.SetToken(refreshTokenId);
                }

            }
        }

        public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
        {

            int hashedTokenId = context.Token.GetHashCode();

            using (AuthRepository _repo = new AuthRepository())
            {
                var refreshToken = await _repo.FindRefreshToken(hashedTokenId);

                if (refreshToken != null)
                {
                    //Get protectedTicket from refreshToken class
                    context.DeserializeTicket(refreshToken.ProtectedTicket);
                    var result = await _repo.RemoveRefreshToken(hashedTokenId);
                }
            }
        }

        public void Create(AuthenticationTokenCreateContext context)
        {
            throw new NotImplementedException();
        }

        public void Receive(AuthenticationTokenReceiveContext context)
        {
            throw new NotImplementedException();
        }

    }


    public class RefreshToken
    {
        public int Id { get; set; }
        public string Subject { get; set; }
        public DateTime IssuedUtc { get; set; }
        public DateTime ExpiresUtc { get; set; }
        public string ProtectedTicket { get; set; }
    };
}

 

另外修改WebApiConfig.Register(HttpConfiguration config)方法:

public static void Register(HttpConfiguration config)
        {
            config.MapHttpAttributeRoutes();

            //config.Routes.MapHttpRoute(
            //    name: "DefaultApi",
            //    routeTemplate: "api/{controller}/{id}",
            //    defaults: new { id = RouteParameter.Optional }
            //);
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            //DO:將會使用CamelCase命名法序列化webApi的返回結果
            var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();
            jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        }

 

最後兩句話將會使用CamelCase命名法序列化webApi的返回結果。

3.使用ASP.NET Identity 實現一個簡單的用戶認證功能,以便咱們生成用戶名和密碼

安裝nuget package:

Microsoft.AspNet.Identity.Owin

Microsoft.AspNet.Identity.EntityFramework

4.新建一個Auth的文件夾,並添加類:

using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;

namespace ADT.TuDou.API.Auth
{
    public class AuthContext : IdentityDbContext<IdentityUser>
    {
        public AuthContext()
            : base("AuthContext")
        {

        }
        public DbSet<RefreshToken> RefreshTokens { get; set; }
    }
}

  

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using ADT.TuDou.API.Entities;

namespace ADT.TuDou.API.Auth
{
    public class AuthRepository : IDisposable
    {
        private AuthContext _ctx;

        private UserManager<IdentityUser> _userManager;

        public AuthRepository()
        {
            _ctx = new AuthContext();
            _userManager = new UserManager<IdentityUser>(new UserStore<IdentityUser>(_ctx));
        }

        //註冊用戶
        public async Task<IdentityResult> RegisterUser(UserModel userModel)
        {
            IdentityUser user = new IdentityUser
            {
                UserName = userModel.UserName
            };

            var result = await _userManager.CreateAsync(user, userModel.Password);

            return result;
        }

        //查詢用戶
        public async Task<IdentityUser> FindUser(string userName, string password)
        {
            IdentityUser user = await _userManager.FindAsync(userName, password);

            return user;
        }

        public async Task<bool> AddRefreshToken(RefreshToken token)
        {

            var existingToken = _ctx.RefreshTokens.SingleOrDefault(r => r.Subject == token.Subject);

            if (existingToken != null)
            {
                var result = await RemoveRefreshToken(existingToken);
            }

            _ctx.RefreshTokens.Add(token);

            return await _ctx.SaveChangesAsync() > 0;
        }

        public async Task<bool> RemoveRefreshToken(int refreshTokenId)
        {
            var refreshToken = await _ctx.RefreshTokens.FindAsync(refreshTokenId);

            if (refreshToken != null)
            {
                _ctx.RefreshTokens.Remove(refreshToken);
                return await _ctx.SaveChangesAsync() > 0;
            }

            return false;
        }

        public async Task<bool> RemoveRefreshToken(RefreshToken refreshToken)
        {
            _ctx.RefreshTokens.Remove(refreshToken);
            return await _ctx.SaveChangesAsync() > 0;
        }

        public async Task<RefreshToken> FindRefreshToken(int refreshTokenId)
        {
            var refreshToken = await _ctx.RefreshTokens.FindAsync(refreshTokenId);

            return refreshToken;
        }

        public void Dispose()
        {
            _ctx.Dispose();
            _userManager.Dispose();

        }
    }
}
View Code

 

同時在web.config中添加connectionString:

<connectionStrings>
  <add name="AuthContext" connectionString="Data Source=.;Initial Catalog=OAuthPractice;Integrated Security=SSPI;" providerName="System.Data.SqlClient" />
</connectionStrings>

  

5.增長一個Entities文件夾並添加UserModel類:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;

namespace ADT.TuDou.API.Entities
{
    public class UserModel
    {
        [Required]
        [Display(Name = "UserModel name")]
        public string UserName { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }
}

  

六、增長TestController:ApiController

        /// <summary>
        /// 依據用戶ID,得到頭像
        /// </summary>
        /// <returns></returns>
        [AllowAnonymous]
        [AcceptVerbs("GET")]
        public HttpResponseMessage GetHeadImg(int UserID)
        {
            //.........
        }        

方法上加了AllowAnonymous標籤,意味着調用這個api無需任何受權

方法上加了Authorize標籤,則此api在沒有受權的狀況下將返回401 Unauthorize。

 

七、類 SimpleAuthorizationServerProvider 說明:

ValidateClientAuthentication方法用來對third party application 認證,具體的作法是爲third party application頒發appKey和appSecrect,在本例中咱們省略了頒發appKey和appSecrect的環節,咱們認爲全部的third party application都是合法的,context.Validated(); 表示全部容許此third party application請求。
GrantResourceOwnerCredentials方法則是resource owner password credentials模式的重點,因爲客戶端發送了用戶的用戶名和密碼,因此咱們在這裏驗證用戶名和密碼是否正確,後面的代碼採用了ClaimsIdentity認證方式,其實咱們能夠把他看成一個NameValueCollection看待。最後context.Validated(ticket); 代表認證經過。

只有這兩個方法同時認證經過纔會頒發token。

TokenEndpoint方法將會把Context中的屬性加入到token中。

 

八、向服務器請求token

resource owner password credentials模式須要body包含3個參數:

grant_type-必須爲password

username-用戶名

password-用戶密碼

//獲取token方法
//獲取token的地址:http://localhost:33590/token
//提交方式:POST
//參數:grant_type=password&username=jay&password=xsj1989
//內容類型:content-type: application/x-www-form-urlencoded
//返回數據:{"access_token":"JznxS2sYbU9fItS-ihnHB6kPnqzFd-C8uZGargqv2TF6mrNhvZYLY4OG1VclTh2PYkqLgeGzZQrnicf633coKEiJsHQCVZQTMHwli1uNQ3fJF2t0ab3CIO7Kj8y2ZvCS5ypOLAOuKkpkP1oAgCJHMkVOMZRbPfj1tqijPSt1EKcRPfzZMcOo0-OxYmbqjBrkHoB-18ZApYy4kyG6g7cHX-kh3Fq4TEAFeShfk5lOn7NKJxUJf9RWs---tCWwcqWVI-XwA3am0G8KW95-OEDq6d1gr2qHxeK020bhbvQ-OWSiR8MEq617wi-jWqdngdl_","token_type":"bearer","expires_in":1799,"as:client_id":"","userName":"jay",".issued":"Thu, 17 Mar 2016 06:19:04 GMT",".expires":"Thu, 17 Mar 2016 06:49:04 GMT"}

 

實踐中的問題:

異常:安全透明方法「System.Web.Http.GlobalConfiguration.get_Configuration()」嘗試訪問安全關鍵類型「System.Web.Http.HttpConfiguration」失敗

解決:Nuget 安裝 Microsoft.AspNet.WebApi -IncludePrerelease  包,更新爲最新的包。

參考:

http://www.cnblogs.com/richieyang/p/4918819.html?utm_source=tuicool&amp;utm_medium=referral#undefinedhttp://oauth.net/code/http://www.cnblogs.com/n-pei/archive/2012/05/29/2524673.htmlhttps://github.com/feiyit/MvcApiSecurity

相關文章
相關標籤/搜索