• Web API在OWIN下实现OAuth


    Nuget引用:

    Install-Package Microsoft.AspNet.WebApi.OwinSelfHost

    或者引用以下三个

    Install-Package Microsoft.AspNet.WebApi.Owin (让WebApi作为中间件)
    Install-Package Microsoft.Owin.Hosting (Hosting接口默认使用HttpListener作为Server)
    Install-Package Microsoft.Owin.Host.HttpListener (默认的Server实现)

    在App_Start文件夹下新增ApplicationDbInitializer,代码如下:

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

    修改Model文件夹下的IdentityModels.cs,添加斜体部分代码,需添加命名空间:using System.Data.Entity;

    public ApplicationDbContext()
                : base("DefaultConnection", throwIfV1Schema: false)
            {
                // 在第一次启动网站时初始化数据库添加管理员用户凭据到数据库
                Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
            }

    我把WebApi的Controller放到一个新建的文件夹APIControllers中,TestController的View的js的测试代码

    打开Startup.Auth.cs,以下代码是Oauth相关的配置代码

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

    使用Client Credentials Grant的授权方式( grant_type= client_credentials)获取 Access Token,并以这个 Token 调用与用户相关的 Web API。

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

    在 ValidateClientAuthentication() 方法中获取客户端的 client_id 与 client_secret 进行验证。在 GrantClientCredentials() 方法中对客户端进行授权,授了权就能发 access token 。这样,OAuth的ClientCredentials授权服务端代码就完成了。在ASP.NET Web API中启用OAuth的Access Token验证非常简单,只需在相应的Controller或Action加上[Authorize]标记,VS已生成部分代码,详细查看APIController文件夹下的ValuesController

    下面我们在客户端调用一下,添加TestController,生成Index的View,然后在View中添加如下

    $(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;
            }
    View Code

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

    在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);
                    }
                });
            }
    View Code

    至此,使用WebApi 的两种授权方式发放Token和两种授权方式区别,以及使用Token调用受保护的api已经介绍完了,Oauth其实还有一个refresh token,refresh token 是专用于刷新 access token 的 token。一是因为 access token 是有过期时间的,到了过期时间这个 access token 就失效,需要刷新;二是因为一个 access token 会关联一定的用户权限,如果用户授权更改了,这个 access token 需要被刷新以关联新的权限。
    这个就不单独介绍了,有兴趣的可以自己研究下。转:https://www.cnblogs.com/YamatAmain/p/5029466.html

    api测试:

        /// <summary>
        ///客户端模式【Client Credentials Grant】
        ///http://www.asp.net/web-api/overview/security/individual-accounts-in-web-api
        /// </summary>
        [RoutePrefix("api/v1/oauth2")]
        public class OAuth2Controller : ApiController
        {
            /// <summary>
            /// 获得资讯
            /// </summary>
            /// <returns></returns>
            [Authorize]
            [Route("news")]
            public async Task<IHttpActionResult> GetNewsAsync()
            {
                var authentication = HttpContext.Current.GetOwinContext().Authentication;
                var ticket = authentication.AuthenticateAsync("Bearer").Result;
    
                var claimsIdentity = User.Identity as ClaimsIdentity;
                var data = claimsIdentity.Claims.Where(c => c.Type == "urn:oauth:scope").ToList();
                var claims = ((ClaimsIdentity)Thread.CurrentPrincipal.Identity).Claims;
                return Ok(new { IsError = true, Msg = string.Empty, Data = Thread.CurrentPrincipal.Identity.Name + " It's about news !!! token expires: " + ticket.Properties.Dictionary.ToJson() });
            }
        }
    View Code
        /// <summary>
        ///客户端模式【Client Credentials Grant】
        ///http://www.asp.net/web-api/overview/security/individual-accounts-in-web-api
        /// </summary>
        [RoutePrefix("api/v1/oauth2")]
        public class OAuth2Controller : ApiController
        {
            /// <summary>
            /// 获取token
            /// </summary>
            /// <returns></returns>
            [Route("token")]
            public async Task<IHttpActionResult> GetTokenAsync()
            {
                //获得token
                var dict = new SortedDictionary<string, string>();
                dict.Add("client_id", "irving");
                dict.Add("client_secret", "123456");
                dict.Add("grant_type", "client_credentials");
                var data = await (@"http://" + Request.RequestUri.Authority + @"/token").PostUrlEncodedAsync(dict).ReceiveJson<Token>();
                //根据token获得咨询信息 [Authorization: Bearer {THE TOKEN}]
                //var news = await (@"http://" + Request.RequestUri.Authority + @"/api/v1/oauth2/news").WithHeader("Authorization", "Bearer " + data.access_token).GetAsync().ReceiveString();
                var news = await (@"http://" + Request.RequestUri.Authority + @"/api/v1/oauth2/news").WithOAuthBearerToken(data.access_token).GetAsync().ReceiveString();
                return Ok(new { IsError = true, Msg = data, Data = news });
            }
        }
        public class Token
        {
            public string access_token { get; set; }
            public string token_type { get; set; }
            public string expires_in { get; set; }
        }
    View Code

     全:https://www.cnblogs.com/xsj1989/p/5557251.html

    https://www.cnblogs.com/Irving/p/4607104.html

    /************     自己测试的    ****************/

    using Microsoft.Owin.Security.OAuth;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web.Http;
    
    namespace WebApplication1
    {
        public static class WebApiConfig
        {
            public static void Register(HttpConfiguration config)
            {
                // Web API 配置和服务  //添加的配置  //匿名身份验证
                //config.SuppressDefaultHostAuthentication();   
                //config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType)); 
    
                // Web API 路由
                config.MapHttpAttributeRoutes();
    
                config.Routes.MapHttpRoute(
                    name: "DefaultApi",
                    routeTemplate: "api/{controller}/{id}",
                    defaults: new { id = RouteParameter.Optional }
                );
            }
        }
    }
    WebApiConfig

     var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();

    jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
    加在里面,最后两句话将会使用CamelCase命名法序列化webApi的返回结果。
     
    using System;
    using System.Threading.Tasks;
    using Microsoft.Owin;
    using Owin;
    using System.Web.Http;
    
    [assembly: OwinStartup(typeof(WebApplication1.Startup))]
    
    namespace WebApplication1
    {
        public partial class Startup
        {
            public void Configuration(IAppBuilder app)
            {
                // 有关如何配置应用程序的详细信息,请访问 http://go.microsoft.com/fwlink/?LinkID=316888
    
                var config = new HttpConfiguration();   //去掉?
                WebApiConfig.Register(config);  // 去掉??
    
                ConfigureAuth(app);//开启OAuth服务
    
                app.UseWebApi(config);//这一行代码必须放在ConfiureOAuth(app)之后
            }
    
        }
    }
    Startup.cs
    using Microsoft.Owin;
    using Microsoft.Owin.Security.OAuth;
    using Owin;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using WebApplication1.Providers;
    
    namespace WebApplication1
    {
        public partial class Startup
        {
            public static OAuthBearerAuthenticationOptions OAuthBearerOptions { get; private set; }
            public void ConfigureAuth(IAppBuilder app)
            {
                //OAuthBearerOptions = new OAuthBearerAuthenticationOptions();  //匿名身份验证
                //Token 生成配置
                var oAuthOptions = new OAuthAuthorizationServerOptions
                {
                    AllowInsecureHttp = true, //允许客户端使用Http协议请求
                    AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active,
                    TokenEndpointPath = new PathString("/token"),
                    AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(5),
                    //提供认证策略
                    Provider = new OpenAuthorizationServerProvider()
                    //RefreshTokenProvider = new RefreshAuthenticationTokenProvider()
    
                };
                //app.UseOAuthAuthorizationServer(oAuthOptions);
                //app.UseOAuthBearerAuthentication(OAuthBearerOptions); //匿名身份验证
                app.UseOAuthBearerTokens(oAuthOptions);
            }
        }
    }
    Startup.Auth.cs
    using Microsoft.Owin.Security.OAuth;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using System.Web;
    
    namespace WebApplication1.Providers
    {
        public class OpenAuthorizationServerProvider : OAuthAuthorizationServerProvider
        {
            public OpenAuthorizationServerProvider()
            {
            }
    
            /// <summary>
            /// 客户端授权[生成access token]
            /// </summary>
            /// <param name="context"></param>
            /// <returns></returns>
            public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
            {
                var oAuthIdentity = new System.Security.Claims.ClaimsIdentity(context.Options.AuthenticationType);
                oAuthIdentity.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, context.OwinContext.Get<string>("as:client_id")));
                var ticket = new Microsoft.Owin.Security.AuthenticationTicket(oAuthIdentity, new Microsoft.Owin.Security.AuthenticationProperties { AllowRefresh = true });
                context.Validated(ticket);
                return base.GrantClientCredentials(context);
            }
    
            /// <summary>
            /// 验证客户端
            /// </summary>
            /// <param name="context"></param>
            public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
            {
                string clientId;
                string clientSecret;
                context.TryGetFormCredentials(out clientId, out clientSecret);//clienti认证
                //context.TryGetBasicCredentials(out clientId, out clientSecret); //Basic认证
    
                //TODO:读库,验证
                if (clientId != "malfy" && clientSecret != "111111")
                {
                    context.SetError("invalid_client", "client is not valid");
                    return Task.FromResult<object>(null);
                }
                context.OwinContext.Set("as:client_id", clientId);
    
                //ILifetimeScope scope = context.OwinContext.GetAutofacLifetimeScope();
                //var authService = scope.Resolve<IAuthService>();
                //var client = authService.GetClient(clientId);
                //context.OwinContext.Set<string>("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString());
    
                context.Validated(clientId);
                return Task.FromResult<object>(null);
            }
        }
    }
    OpenAuthorizationServerProvider .cs

    Controller :  [Authorize]

    //**********  end ***********/

    Provider :提供具体的认证策略;

    ///添加新的RefreshTokenProvider

    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.GetHash(),
                    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)
        {
     
            string hashedTokenId = context.Token.GetHash();
     
            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();
        }
     
    }
    View Code

    我们实现了其中两个异步方法,对两个同步方法不做实现。其中CreateAsync用来生成RefreshToken值,生成后需要持久化在数据库中,客户端需要拿RefreshToken来请求刷新token,此时ReceiveAsync方法将拿客户的RefreshToken和数据库中RefreshToken做对比,验证成功后删除此refreshToken。

    2、重新请求token

    可以看到这次请求不但得到了token,还得到了refresh_token

    3、当token过期后,凭借上次得到的refresh_token重新获取token

    此次请求又得到了新的refresh_token,每次refresh_token只能用一次,因为在方法ReceiveAsync中我们一旦拿到refresh_token就删除了记录。

    七、总结

    此文重点介绍了OAuth2.0中resource owner password credentials模式的使用,此模式可以实现资源服务为自己的客户端授权。另外文章中也提到模式4-client credentials也可以实现这种场景,但用来给有服务端的客户端使用-区别于纯html+js客户端。原因在于模式4-client credentials使用appKey+appSecrect来验证客户端,如果没有服务端的话appSecrect将暴露在js中。

    同样的道理:模式1-授权码模式(authorization code)和模式2-简化模式(implicit)的区别也在于模式2-简化模式(implicit)用在无服务端的场景下,请求头中不用带appSecrect。

    在webApi中使用owin来实现OAuth2.0是最简单的解决方案,另外一个方案是使用DotNetOpenOauth,这个方案的实现稍显复杂,可用的文档也较少,源码中带有几个例子我也没有直接跑起来,最后无奈之下几乎读完了整个源码才理解。

    八、客户端的实现

    我们将采用jquery和angular两种js框架来调用本文实现的服务端。下一篇将实现此功能,另外还要给我们的服务端加上CORS(同源策略)支持。

    代码都同步更新在 https://git.oschina.net/richieyangs/OAuthPractice.git

    转:https://www.cnblogs.com/Leo_wl/p/4919783.html

     数据并行:http://www.cnblogs.com/Leo_wl/p/4919814.html

  • 相关阅读:
    nginx+uwsgi部署Django
    Git----忽略特殊文件
    Git 分支管理
    Django admin 页面中文名称加s,去除s的设置
    hive-sql参数调优及资源分配
    常用数仓架构/计算引擎
    maven 打包可运行jar包(转)
    spark sql遇到的问题
    分析跨域
    nio案例一:个简单的客户-服务的案例
  • 原文地址:https://www.cnblogs.com/love201314/p/7902720.html
Copyright © 2020-2023  润新知