zoukankan      html  css  js  c++  java
  • OAuth2.0 Owin 授权问题

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

    OAuth2.0

    一、什么是OAuth

    OAuth是一个关于授权(Authorization)的开放网络标准,目前的版本是2.2版。

    注意是Authorization(授权),而不是Authentication(认证)。

    用来做Authentication(认证)的标准叫做openid connect。

    二、名词定义

    理解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可以是不同的服务器,也可以是同一个服务器。

    三、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认证服务,前两种模式使用场景不符合我们的需求。

    四、选择合适的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足够信任,所以我们才能采取这种模式来实现。

    五、使用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; }
        }
    }
    

      

    6、增加TestController:ApiController

            /// <summary>
            /// 依据用户ID,获得头像
            /// </summary>
            /// <returns></returns>
            [AllowAnonymous]
            [AcceptVerbs("GET")]
            public HttpResponseMessage GetHeadImg(int UserID)
            {
                //.........
            }        
    

    方法上加了AllowAnonymous标签,意味着调用这个api无需任何授权

    方法上加了Authorize标签,则此api在没有授权的情况下将返回401 Unauthorize。

    7、类 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中。

    8、向服务器请求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#undefined
    http://oauth.net/code/
    http://www.cnblogs.com/n-pei/archive/2012/05/29/2524673.html
    https://github.com/feiyit/MvcApiSecurity

  • 相关阅读:
    Java程序设计11——异常处理
    Hive安装及配置
    zookeeper集群安装(转)
    Linux网络服务管理命令
    Linux网络配置常用命令
    IP地址及子网掩码计算
    linux ftp、sftp、telnet服务开通、更改Orale最大连接数
    SQL多表连接
    用css伪类实现提示框效果
    关于DOM操作的性能优化
  • 原文地址:https://www.cnblogs.com/xsj1989/p/5287375.html
Copyright © 2011-2022 走看看