基于OWIN+DotNetOpenOAuth实现OAuth2.0

这几天时间一直在研究怎么实现自己的OAuth2服务器,对于太了解OAuth原理以及想自己从零开始实现的,我建议可以参考《Apress.Pro ASP.NET Web API Security》里面的章节。最后发现其实微软在这方面也已经做了实现,所以文介绍下怎么基于OWIN来实现自己的OAuth2.0授权服务器,,以及怎么使用DotNetOpenAuth作为客户端来访问受保护的资源。  OWIN是一套specification,微软的Katana开源项目是基于OWIN标准开发的,所以本本文更准确的说是用katana来实现OAuth2.0授权服务器。DotNetOpenAuth也是一个非常优秀的框架,它也可以实现OAuth2.0服务器,本文是用这个框架来做客户端。更多的关于Owin, OAuth等标准和框架,不是本文的重点,您可以自己查阅一些相关资料。本文假设您已经对OAuth,OWIN已经有一定的了解。

利用微软的OWIN搭建OAuth非常简单,具体步骤如下:

  1. 先创建一个ASP.NET项目,最简单直接的的方式是创建一个MVC项目并且在身份验证时选择Individual User Accounts,这样会自动将OWIN相关的DLL加入到项目
  2. 接下来找到App_Start文件夹下面的Startup.Auth.cs文件,加入如下代码:(不知道为什么插入代码的时候我不能折叠:( )

                //创建OAuth授权服务器
                app.UseOAuthAuthorizationServer(new Microsoft.Owin.Security.OAuth.OAuthAuthorizationServerOptions()
                {
                    AuthorizeEndpointPath = new PathString("/OAuth/Authorize"),
                    TokenEndpointPath = new PathString("/OAuth/Token"),
                    AccessTokenExpireTimeSpan=TimeSpan.FromMinutes(1),
                    Provider = new OAuthAuthorizationServerProvider()
                    {
                        OnValidateClientRedirectUri = context =>
                        {
                            context.Validated();
                            return Task.FromResult(0);
                        },
    
                        OnValidateClientAuthentication = context =>
                        {
                            string clientId;
                            string clientSecret;
                            if (context.TryGetBasicCredentials(out clientId, out clientSecret) ||
                                  context.TryGetFormCredentials(out clientId, out clientSecret))
                            context.Validated();
    
                            return Task.FromResult(0);
                        }
    
                    },
    
                    AuthorizationCodeProvider = new AuthenticationTokenProvider()
                    {
                        OnCreate = context =>
                        {
                            context.SetToken(DateTime.Now.Ticks.ToString());
                            string token = context.Token;
                            string ticket = context.SerializeTicket();
                            _authenticationCodes[token] = ticket;
                        },
    
                        OnReceive = context =>
                        {
                            string token = context.Token;
                            string ticket;
                            if (_authenticationCodes.TryRemove(token, out ticket))
                            {
                                context.DeserializeTicket(ticket);
                            }
                        },
    
                    },
    
                    RefreshTokenProvider = new AuthenticationTokenProvider()
                    {
                        OnCreate = context =>
                        {
                            context.SetToken(context.SerializeTicket());
                        },
    
                        OnReceive = context =>
                        {
                            context.DeserializeTicket(context.Token);
                        },
                    }
    
                });

    其中AuthorizeEndpointPath是授权终结点,这个需要自己去写实现逻辑,等会会介绍。TokenEndpointPath是生成Token的终结点,这个不需要我们写额外的逻辑了,因为Token的生成涉及到很多方面,例如序列化反序列加密解密等逻辑,所以框架默认已经帮我们做好了。 Provider里面是用来验证客户端跳转地址和客户端验证,我这里实现比较简单,全部都验证通过,正确的做法应该是先判断客户端发送过来的ClientID是否合法,如果合法则验证通过。AuthorizationCodeProvider是对于Authorize Code模式而言的(我等会客户端访问只接受这种模式,也是最负责的一种,其他三种模式Implicit,Client,Resource相对比较简单,就不在这里介绍了,有兴趣的同学可以线下联系),里面的两个Action委托顾名思义一个是创建Code,一个是用Code来来交换Token。最后是RefreshTokenProvider

  3. 接下来就看下授权终结点,这里的逻辑需要自己来处理

     public ActionResult Authorize()
            {           
    
                IAuthenticationManager authentication= HttpContext.GetOwinContext().Authentication;
                AuthenticateResult ticket = authentication.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie).Result;
                ClaimsIdentity identity = ticket == null ? null : ticket.Identity;
    
                if (identity == null)
                {
                    //如果没有验证通过,则必须先通过身份验证,跳转到验证方法
                    authentication.Challenge(DefaultAuthenticationTypes.ApplicationCookie);
                    return new HttpUnauthorizedResult();
                }
    
                identity = new ClaimsIdentity(identity.Claims, "Bearer");
                //hardcode添加一些Claim,正常是从数据库中根据用户ID来查找添加
                identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
                identity.AddClaim(new Claim(ClaimTypes.Role, "Normal"));
                identity.AddClaim(new Claim("MyType", "MyValue"));
    
                authentication.SignIn(new AuthenticationProperties() { IsPersistent=true }, identity);
    
                return View();
            }

    通常来说,客户端发送到授权终结点的请求是会被OWIN截取跳转到第二步中注册的 OnValidateClientRedirectUri委托,如果验证通过再跳转到授权终结点,这里首先看当前用户是否已经验证通过(Authentication),如果没有则跳转到验证终结点,注意这里的方法authentication.Challenge(DefaultAuthenticationTypes.ApplicationCookie)中的参数,名称必须要和Startup.Auth.cs中app.UseCookieAuthentication内定义的AuthenticationType一样,否则无法跳转到在app.UseCookieAuthentication内注册的LoginPath终结点

  4. 验证节点(Authentication)代码

            [HttpPost]
            [AllowAnonymous]
            [ValidateAntiForgeryToken]
            public ActionResult Login(LoginViewModel model, string returnUrl)
            {
                if (!ModelState.IsValid)
                {
                    return View(model);
                }
    
                if (_userManagerService.VerifyUser(model.Email, model.Password))
                {
                    AppUser user = UserManager.FindByName(model.Email);
                    //可以在这里将用户所属的role或者Claim添加到此
                    ClaimsIdentity claims = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, user.UserName)
                    ,new Claim(ClaimTypes.NameIdentifier,user.Id)
                    ,new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider",user.Id)},
                        DefaultAuthenticationTypes.ApplicationCookie);
    
                    AuthenticationProperties properties = new AuthenticationProperties
                    {
                        IsPersistent = true
                    };
    
                    ClaimsPrincipal principal = new ClaimsPrincipal(claims);
                    //System.Threading.Thread.CurrentPrincipal = principal;
                    this.AuthenticationManager.SignIn(properties, new[] { claims });
    
                    return RedirectToLocal(returnUrl);
                }
                else
                {
                    ModelState.AddModelError("", "Invalid login attempt.");
                    return View(model);
                }
        }

    通过第三步首先会进入到Login的Get方法(这里没有贴出来,因为只是返回试图而已),然后用户通过输入正确的用户名和密码通过验证后,再为用户添加代码中所示的Claim,注意如果你的代码中加入了[ValidateAntiForgeryToken]特性,那么需要加入NameIdentifier,identityprovider两个声明,否则会报错。最后再调用SignIn方法(我没有仔细去看这个方法的代码,但是我认为这里应该至少会有对System.Threading.Thread.CurrentPrincipal进行赋值,确保验证通过)然后再次跳转到授权节点,再次执行第三步的代码,这是identity就不为空了,我这里重新构造了Identity,注意这里的AuthenticationType需要为“Bearer”,这是告诉OAuth生成Token的关键。最后再进行SignIn,此时会执行第二步中注册的Authorize Code生成方法,生成Code后会回跳到客户端的注册的RedirectUrl,客户端此时就可以拿着Code来换Token了,具体也就是执行第二步中注册的OnReceive方法

  5. 好了,整个服务器授权逻辑差不多就是这样了,当然我这里为了简单没有去实现Scope的Grant,如果要做可以在授权终结点里进行实现
  6. 下面来看看客户端怎么实现的,首选需要说明一点如果是.NET的WEB客户端可以使用DotNetOpenOAuth,通过Nuget就可以获取,比较简单,如果是其他手机设备等则可以通过ImplicitGrant模式通过javascript脚本来做,implicitgrant是authorize code的而一个简化版,省略了code交互这些步骤,直接从服务器拿Token,而且客户端是能直接看到Token的,但是authorize code客户端是看不到的,只有客户端所在的web server才能看到,所以也就更安全。
  7. 我在客户端自己稍微对DNOA做了下封装,代码有点长,所以就只看下几个核心方法

            public string[] scopes { get; set; }
            public string redirectUri { get; set; }
       public IAuthorizationState AuthorizationState = null;
            private string AuthorizationEndpoint, TokenEndpoint, ClientId, ClientKey;
            private AuthorizationServerDescription authServer = null;
    
            private WebServerClient oauthWebClient = null;
            public OAuthClient()
            {
    
                AuthorizationEndpoint = System.Configuration.ConfigurationManager.AppSettings.Get("AuthorizeEndpoint");
                TokenEndpoint = System.Configuration.ConfigurationManager.AppSettings.Get("TokenEndpoint");
                ClientKey = System.Configuration.ConfigurationManager.AppSettings.Get("ClientKey");
                ClientId = System.Configuration.ConfigurationManager.AppSettings.Get("ClientID");
                authServer = new AuthorizationServerDescription
                {
                    AuthorizationEndpoint = new Uri(AuthorizationEndpoint),
                    TokenEndpoint = new Uri(TokenEndpoint)
                };
    
                oauthWebClient = new WebServerClient(authServer, clientIdentifier: ClientId, clientSecret:ClientKey);
                AuthorizationState = new AuthorizationState();
    
            }

    比较重要的就是WebServerClient这个对象了,封装了对授权服务器访问的所有方法,对它初始化需要告诉它的授权和Token终结点,ClientID,ClientSecret(这两个值正常做法是授权服务器颁给的,和新浪,腾讯等一样)

  8. 初始化好了后就可以开始访问了,因为我们是使用authorize code,所以这里调用的

     oauthWebClient.RequestUserAuthorization(scopes, new Uri(redirectUri));

    传入你要访问资源的范围和回调的URL(通常就是发起的action),获得Code后就可以服务器会把code值作为参数写入到Request的querystring中,所以我们可以下面方式来检查code是否已经存在

    if (request.Url.AbsoluteUri.Contains(redirectUri))
                {
                    if (!string.IsNullOrEmpty(request.Params["code"]))
                    {
                        return true;
                    }
                }

    如果code存在就可以用code来换Token了,马上就会大功告成了。。

    public void GetAccessToken(HttpRequestBase Requst, out string token)
            {
                if (CheckCodeExist(Requst))
                {
                    AuthorizationState = oauthWebClient.ProcessUserAuthorization();
                    if (AuthorizationState != null)
                    {
                        if (!string.IsNullOrEmpty(AuthorizationState.AccessToken))
                        {
    
                            token = AuthorizationState.AccessToken;
    
                            return;
                        }
                    }
                }
               GetAuthrizationCode();
               token = null;
    
            }

    最关键的方法就是AuthorizationState = oauthWebClient.ProcessUserAuthorization();通过它我们就可以拿到token相关的信息了,例如AccessToken, RefreshToken,Token过期时间等

  9. 接下来就是用Token去访问资源服务器上的资源了,访问之前,我们先要对资源服务器同样要加入OWIN的支持,由于我采用的IISHost,需要加入下列组件microsoft.owin,micorsoft.owin.host.systemweb, microsoft.owin.security,microsoft.owin.oauth,通过Nuget获取既可,不做多介绍,这时应该会有Startup.cs文件自动加进来,我们在里面加上下面代码:

     public partial class Startup
        {
            public void Configuration(IAppBuilder app)
            {
                app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions());
            }
        }

    这里就是使用Bearer验证,具体逻辑是拿到请求头里的Bearer验证带来的Token,如果token是合法的,那么OWIN对token进行解析反序列化出来对等的claim,并且通过验证,还可以进一步根据claim来判断当前用户是否有权限来相应的资源,如下(用户必须是Admin角色,否则会401):

            [Authorize]
            [HttpGet]
            [Route("api/car/protect")]
            public string ProtectedResource()
            {
                ClaimsPrincipal principal = Thread.CurrentPrincipal as ClaimsPrincipal;
                var isInRole= principal.IsInRole("Admin");
                return "you have the right now to access me!";
    
            }

    另外需要注意点的是,如果你需要实现单点登录之类的功能或者是访问多台资源服务器,那么需要保证不同的资源服务器使用的都是相同加密解密机制,这个可以通过在web.config里设置machiekey来实现,关于machine key这里就不说了

  10. 回到刚才9步中说到的客户端访问资源,很简单,我是通过httpclient做了个简单的访问实现,具体如下,关键就是将token以Bearer方式添加到验证头,就不多复述了

                string msg="";
                using (HttpClient httpClient = new HttpClient())
                {
                    httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
                   httpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
                    if (method.ToUpper() == "POST")
                    {
                        StringContent content = new StringContent(jsonStr);
                        content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
                        foreach (var address in resourceAddress)
                        {
                            var responseMsg = httpClient.PostAsync(address, content).Result;
                            if (responseMsg.IsSuccessStatusCode)
                            {
                                msg += responseMsg.Content.ReadAsStringAsync().Result; ;
                            }
                        }
    
                    }
                    else
                    {
                        foreach (var address in resourceAddress)
                        {
                            var responseMsg = httpClient.GetAsync(address).Result;
                            if (responseMsg.IsSuccessStatusCode)
                            {
                                msg += responseMsg.Content.ReadAsStringAsync().Result; ;
                            }
                        }
                    }

现在整个授权服务器,资源服务器,客户端基本都实现了。

第一次写博,原以为最多一个小时可以搞定,没想到快四个小时了,所以没时间来检查以及弄排版样式啥的了,写博客还真不是容易的事啊。

时间: 2024-08-26 04:06:11

基于OWIN+DotNetOpenOAuth实现OAuth2.0的相关文章

使用Owin中间件搭建OAuth2.0认证授权服务器

前言 这里主要总结下本人最近半个月关于搭建OAuth2.0服务器工作的经验.至于为何需要OAuth2.0.为何是Owin.什么是Owin等问题,不再赘述.我假定读者是使用Asp.Net,并需要搭建OAuth2.0服务器,对于涉及的Asp.Net Identity(Claims Based Authentication).Owin.OAuth2.0等知识点已有基本了解.若不了解,请先参考以下文章: MVC5 - ASP.NET Identity登录原理 - Claims-based认证和OWIN

基于Apache OLTU的OAuth2.0授权解决方案

Apache OLTU实现了OAuth 2.0的规范,是一种可靠的Java授权解决方案.但是,官方文档实在是太惨不忍睹了.本文参考了开涛的OAuth 2.0集成Shiro文章.模拟了OAuth2.0的认证流程. 技术框架:Spring+SpringMVC+Apache OLTU+AmazonUI. 界面效果: 代码地址:https://github.com/favccxx/FavOAuth2

ASP.NET没有魔法——ASP.NET MVC使用Oauth2.0实现身份验证

原文:ASP.NET没有魔法--ASP.NET MVC使用Oauth2.0实现身份验证 随着软件的不断发展,出现了更多的身份验证使用场景,除了典型的服务器与客户端之间的身份验证外还有,如服务与服务之间的(如微服务架构).服务器与多种客户端的(如PC.移动.Web等),甚至还有需要以服务的形式开放给第三方的,身份验证这一功能已经演化为一个服务,很多大型应用中都有自己的身份验证服务器甚至集群,所以普通的身份验证方式已经不能满足需求. 在.Net领域中也有一些开源的身份验证服务器组件,如Identit

使用微服务架构思想,设计部署OAuth2.0授权认证框架

1,授权认证与微服务架构 1.1,由不同团队合作引发的授权认证问题 去年的时候,公司开发一款新产品,但人手不够,将B/S系统的Web开发外包,外包团队使用Vue.js框架,调用我们的WebAPI,但是这些WebAPI并不在一台服务器上,甚至可能是第三方提供的WebAPI.同时处于系统安全的架构设计,后端WebAPI是不能直接暴露在外面的:另一方面,我们这个新产品还有一个C/S系统,C端登录的时候,要求统一到B/S端登录,可以从C端无障碍的访问任意B/S端的页面,也可以调用B/S系统的一些API,

springboot+spring security +oauth2.0 demo搭建(password模式)(认证授权端与资源服务端分离的形式)

项目security_simple(认证授权项目) 1.新建springboot项目 这儿选择springboot版本我选择的是2.0.6 点击finish后完成项目的创建 2.引入maven依赖  下面是我引入的依赖 1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi=&q

基于OAuth2.0协议 第三方登录与数据同步验证设计

前段时间,公司跟别的公司签订合作伙伴,搞了一个第三方登录与数据共享同步,是基于OAuth2.0协议,现在空闲了,做一下笔记. 到github下载一个OAuth2.0的PHP类库(当然,你也可以自己写一个^-^,但个人觉得没必要造轮子),有写好Mysql与Mongodb的Demo,参考一下,然后嵌套自己的业务代码,下面是客户端与服务端的交互流程: +-----------+ +-----------+| | 带client_id的URL请求获取code | || | ---------------

基于OAuth2.0协议的QQ第三方授权登录iOS代码分析

简要说明: 授权登录已经成为注册方式里的主流,目前授权登录方式主要SSO跳转授权登录和OAuth2.0两种,前者好处无需用户再次输入密码就可以直接授权成功,但前提是必须用户手机端安装了该软件,比如QQ,后者的优势就是是否安装无关紧要,是一个HTML的页面呈现,麻烦就在于要输入用户名和密码,这就非常不爽了,但是有时候偏偏必须这么做,理由嘛,自行想想就好,接下来我们就看看如果利用OAuth2.0的方式来做QQ授权登录,如果想了解QQ的SSO授权登录,可以看我(博客主页)之前的博客:基于第三方QQ授权

基于oauth2.0实现应用的第三方登录

OAuth2 OAuth2所涉及到的对象主要有以下四个: Client 第三方应用,我们的应用就是一个Client Resource Owner 资源所有者,即用户 Authorization Server 授权服务器,即提供第三方登录服务的服务器,如Github Resource Server 拥有资源信息的服务器,通常和授权服务器属于同一应用 OAuth2的基本流程为: 第三方应用请求用户授权. 用户同意授权,并返回一个凭证(code) 第三方应用通过第二步的凭证(code)向授权服务器请求

基于OWin的Web服务器Katana发布版本3

当 ASP.NET 首次在 2002 年发布时,时代有所不同. 那时,Internet 仍处于起步阶段,大约有 5.69 亿用户,每个用户平均每天访问 Internet 的时间为 46 分钟,大约有 3 百万个网站. 仅仅在 10 年之后,相同的测量指标揭示,大约有 22.7 亿个 Internet 用户,每个用户平均每天访问 Internet 的时间为 4 小时,大约有 5.55 亿个网站.伴随着网络应用程序开发的不断演进,ASP.NET也伴随着产生了新的技术,比如ASP.NET MVC和AS