是一个关于授权(Authorization)的开放网络标准,目前的版本是2.0版。注意是Authorization(授权),而不是Authentication(认证)。用来做Authentication(认证)的标准叫做。
一、OAuth2.0理论普及
1、OAuth2.0中的角色说明:
资源拥有者(resource owner):能授权访问受保护资源的一个实体,可以是一个人,那我们称之为最终用户;如新浪微博用户zhangsan;
资源服务器(resource server):存储受保护资源,客户端通过access token请求资源,资源服务器响应受保护资源给客户端;存储着用户zhangsan的微博等信息。
授权服务器(authorization server):成功验证资源拥有者并获取授权之后,授权服务器颁发授权令牌(Access Token)给客户端。
客户端(client):如新浪微博客户端weico、微格等第三方应用,也可以是它自己的官方应用;其本身不存储资源,而是资源拥有者授权通过后,使用它的授权(授权令牌)访问受保护资源,然后客户端把相应的数据展示出来/提交到服务器。“客户端”术语不代表任何特定实现(如应用运行在一台服务器、桌面、手机或其他设备)。
2、OAuth2.0客户端的授权模式:
2.1、Oautho2.0为客户端定义了4种授权模式:
1)授权码模式
2)简化模式
3)密码模式
4)客户端模式
2.2、授权码模式:
授权码模式是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。
授权码模式的认证流程:
(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器首先生成一个授权码,并返回给用户,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
注意:(C)和(D)中两个重定向URI是不一样的,(C)中的重定向URI是用来核对的,这个是服务器事先指定并保存在数据库里面。而(D)中的重定向URI指的是生成access_token的url。
3、选择合适的OAuth模式打造自己的webApi认证服务
场景:你自己实现了一套webApi,想供自己的客户端调用,又想做认证。
这种场景下你应该选择模式3或者4,特别是当你的的客户端是js+html应该选择3,当你的客户端是移动端(ios应用之类)可以选择3,也可以选择4。
密码模式(resource owner password credentials)的流程:
我在另一篇博客中
《》,详细介绍了各种微服务身份认证的技术方案。
如果觉得以上理论信息意犹未尽的话,请继续关注鄙人博客,或者搜索相关的资料,做进一步的研究。
下面就以授权码授权模式为例,进行代码的实践。
二、OAuth2.0实践
这里是以Asp.Net mvc5为例,具体步骤如下:
首先引用Owin OAuth相关的类库。
Microsoft.AspNet.Identity;Microsoft.Owin;Microsoft.Owin.Security.Cookies;Microsoft.Owin.Security.Infrastructure;Microsoft.Owin.Security.OAuth;
添加Owin启动类,代码如下:
using System;using System.Collections.Generic;using System.Threading.Tasks;using Microsoft.AspNet.Identity;using Microsoft.Owin;using Microsoft.Owin.Security.Cookies;using Microsoft.Owin.Security.Infrastructure;using Microsoft.Owin.Security.OAuth;using Owin;using System.Security.Claims;using System.Collections.Concurrent;/*=================================================================================================** Title:XXXXXXXX* Author:李朝强* Description:模块描述* CreatedBy:lichaoqiang.com* CreatedOn:2017-8-4 11:16:42* ModifyBy:暂无...* ModifyOn:2017-8-4 11:16:42* Blog:http://www.lichaoqiang.com* Mark:**================================================================================================*/[assembly: OwinStartup(typeof(OAuthCode.Startup))]namespace OAuthCode{ ////// 应用程序启动类 /// public class Startup { ////// 用来存放临时授权码 线程安全 /// private readonly ConcurrentDictionary_authenticationCodes = new ConcurrentDictionary (StringComparer.Ordinal); /// /// 配置授权 /// /// public void Configuration(IAppBuilder app) { //创建OAuth授权服务器 app.UseOAuthAuthorizationServer(new Microsoft.Owin.Security.OAuth.OAuthAuthorizationServerOptions() { AllowInsecureHttp = true,//开启 AuthenticationType = "Bearer", AuthorizeEndpointPath = new PathString("/OAuth/Authorize"), TokenEndpointPath = new PathString("/OAuth/Token"), AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30), Provider = new OAuthAuthorizationServerProvider() { //授权码 authorization_code OnGrantAuthorizationCode = ctx => { if (ctx.Ticket != null && ctx.Ticket.Identity != null && ctx.Ticket.Identity.IsAuthenticated) { ctx.Validated(ctx.Ticket.Identity);// } return Task.FromResult(0); }, OnGrantRefreshToken = ctx => { if (ctx.Ticket != null && ctx.Ticket.Identity != null && ctx.Ticket.Identity.IsAuthenticated) { ctx.Validated(); } return Task.FromResult(0); }, //OnGrantResourceOwnerCredentials = (context) => //{ // context.Validated(context.Ticket.Identity); // return Task.FromResult(0); //}, OnValidateAuthorizeRequest = ctx => { ctx.Validated(); return Task.FromResult(ctx); }, //验证redirect_uri是否合法 OnValidateClientRedirectUri = context => { context.Validated(redirectUri: context.RedirectUri); return Task.FromResult(context); }, //用来验证请求中的client_id和client_secret OnValidateClientAuthentication = context => { string clientId; string clientSecret; //这是通过Basic或form的方式,获取client_id和client_secret if (context.TryGetBasicCredentials(out clientId, out clientSecret) || context.TryGetFormCredentials(out clientId, out clientSecret)) { context.Validated(clientId); } return Task.FromResult(context); }, OnAuthorizeEndpoint = context => { return Task.FromResult(context); }, OnTokenEndpoint = (context) => { return Task.FromResult(context); }, //OnGrantClientCredentials = (context) => //{ // context.Validated(); // return Task.FromResult(context); //} }, //Code授权 AuthorizationCodeProvider = new AuthenticationTokenProvider() { OnCreate = context => { context.SetToken(DateTime.Now.Ticks.ToString()); string token = context.Token; string ticket = context.SerializeTicket(); var redirect_uri = context.Request.Query["redirect_uri"]; context.Response.Redirect(string.Format("{0}?code={1}&state=1", redirect_uri, token)); _authenticationCodes[token] = ticket;//这里存放授权码 }, //当接收到code时 OnReceive = context => { string token = context.Token; string ticket; if (_authenticationCodes.TryRemove(token, out ticket)) { context.DeserializeTicket(ticket); } }, }, //(可选)访问令牌 AccessTokenProvider = new AuthenticationTokenProvider() { //创建访问令牌 OnCreate = (context) => { string token = context.SerializeTicket(); context.SetToken(token); }, //接收 OnReceive = (context) => { context.DeserializeTicket(context.Token); }, }, //刷新令牌 RefreshTokenProvider = new AuthenticationTokenProvider() { OnCreate = context => { context.SetToken(context.SerializeTicket()); }, OnReceive = context => { context.DeserializeTicket(context.Token); }, } }); //本地Cookie身份认证 app.UseCookieAuthentication(new CookieAuthenticationOptions() { LoginPath = new PathString("/Account/Login"), AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie }); } }}
以上是启动类的所有代码,你也可以在码云中获取。
接下来,我们需要一个登录授权页面,这里有两个控制器
AccountController及OAuthController,分别负责登录及认证授权。
using System;using System.Collections.Generic;using System.Linq;using System.Net.Http;using System.Security.Claims;using System.Web;using System.Web.Mvc;using Microsoft.AspNet.Identity;using Microsoft.Owin.Security;using OAuthCode.Models;namespace OAuthCode.Controllers{ public class AccountController : Controller { ////// /// public IAuthenticationManager AuthenticationManager { get { return HttpContext.GetOwinContext().Authentication; } } // // GET: /Account/ public ActionResult Index() { return View(); } ////// /// ///public ActionResult Login(string returnUrl) { ViewBag.returnUrl = Uri.EscapeDataString(returnUrl); return View(); } /// /// /// /// /// ///[HttpPost] public ActionResult Login(LoginViewModel model, string returnUrl) { string userId = "1"; //可以在这里将用户所属的role或者Claim添加到此 ClaimsIdentity claims = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, model.account) ,new Claim(ClaimTypes.NameIdentifier,userId) ,new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider",userId)}, 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 Redirect(returnUrl); } }}
以上是登录控制器有关的代码。
接下来,我们编写下OAuthController控制器相关的Action.
using System;using System.Collections.Generic;using System.Linq;using System.Net;using System.Net.Http;using System.Security.Claims;using System.Web;using System.Web.Http;using System.Web.Mvc;using Microsoft.AspNet.Identity;using Microsoft.Owin.Security;using System.Threading.Tasks;using DotNetOpenAuth.OAuth2;/******************************************************************************************************************* * * 说 明: (版本:Version1.0.0)* 作 者:李朝强* 日 期:2015/05/19* 修 改:* 参 考:http://my.oschina.net/lichaoqiang/* 备 注:暂无...* * * ***************************************************************************************************************/namespace OAuthCode.Controllers{ ////// /// public class OAuthController : Controller { ////// /// ///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(); 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 new EmptyResult(); } /// /// /// ///public async Task GetAccessToken() { #region 使用DotNetOpenOAuth获取访问令牌 //var authServer = new AuthorizationServerDescription //{ // AuthorizationEndpoint = new Uri("http://localhost:3335/OAuth/Authorize"), // TokenEndpoint = new Uri("http://localhost:3335/OAuth/Token"), //}; //var autoServerClient = new DotNetOpenAuth.OAuth2.WebServerClient(authServer, clientIdentifier: "fNm0EDIXbfuuDowUpAoq5GTEiywV8eg0TpiIVnV8", clientSecret: "clientSecret"); //var authorizationState = autoServerClient.ProcessUserAuthorization(); //if (authorizationState != null) //{ // if (!string.IsNullOrEmpty(authorizationState.AccessToken)) // { // var token = authorizationState.AccessToken; // } //} #endregion 使用DotNetOpenOAuth获取访问令牌 #region 根据授权码,获取访问令牌 模拟第三方回调地址 redirect_uri string strCode = Request.QueryString["code"];//访问令牌 if (string.IsNullOrEmpty(strCode) == false) { HttpClient client = new HttpClient(); HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, "http://localhost:3335/OAuth/Token"); Dictionary dict = new Dictionary (); dict["grant_type"] = "authorization_code"; dict["client_id"] = "fNm0EDIXbfuuDowUpAoq5GTEiywV8eg0TpiIVnV8"; dict["client_secret"] = "111111"; dict["code"] = strCode; dict["redirect_uri"] = "http://localhost:3335/OAuth/GetAccessToken"; dict["scope"] = "scope1"; message.Content = new FormUrlEncodedContent(dict); var response = await client.SendAsync(message); string strResponseText = await response.Content.ReadAsStringAsync(); return Content(strResponseText, "text/javascript"); } #endregion 根据授权码,获取访问令牌 return Content("invalid code!"); } }}
其中包含了认证及回调获取令牌的处理逻辑。
以上步骤中,注意的事项比较多,
认证终结点:/OAuth/Authorize
下面是我本地演示的认证地址:
http://localhost:3335//OAuth/Authorize?redirect_uri=http://localhost:3335/OAuth/GetAccessToken&client_id=fNm0EDIXbfuuDowUpAoq5GTEiywV8eg0TpiIVnV8&client_secret=111111&response_type=code&scope=scope1
授权码模式下需要注意以下事项:
注意:
认证的时候,response_type必须为code,scope可选参数。
授权(获取令牌的)的时候:grant_type必须为authorization_code
完成以上工作,接下来让我们进行尝试,首先,打开请求认证授权的地址,
http://localhost:3335//OAuth/Authorize?redirect_uri=http://localhost:3335/OAuth/GetAccessToken&client_id=fNm0EDIXbfuuDowUpAoq5GTEiywV8eg0TpiIVnV8&client_secret=111111&response_type=code&scope=scope1
它请求的是认证终结点,也就是颁发授权码的入口。
第一次,由于没有登录,于是会出现登录授权的界面。
这个过程就像,第三方登录,如QQ,点击QQ登录,会出现QQ的授权页面,这里只是省略了,可以根据实际情况进行定制。
我们点击登录授权
返回结果如下:
{"access_token":"p8Jd6YwYmBgDkyTt4zTBMWNzTRbZRAM30vO3gfOiqzEw_8dCft-emDrbCC4o6_DGHW2zX0HuQus_4GJ1mYio6meCGeNP4tyEz_la4_zP8vJPsWG0TyXIwzyZth0ioWJ9JJc453MXNMH7EPMevrRsYyQpPG387gEaQFia1Q3EL7EOV7_LIkpmmMyfHGuxaTbevCbekWqR8YJdpigFd4WSwOOlode_PwL23qtneu-ezE3YitFoRIicD4rLk62lCme5pc9gFHBo2d0hRjyu7sHbqiwotWISDm290ddkhlhGlS2cPNJKYJZeCXMb7EPOdTuWMWBoOO1tpFZUsWZDVsbsu2tf42O5SNvQwzNw_o-oDW3riDVwle6aW5IqwFDk2cBIXVU3_ewbNhx13r4HfoeyhzFqUBmOjmUcB2qaER5UaEDsNVf8d0-KukUPEW-MHwl42flCTB_qqFn7ZjiKOIbjZJbVlbVj8vDvzTYjrc3msjc","token_type":"bearer","expires_in":1799,"refresh_token":"bR4OmKey_ex9JJApDP8-3O1gFZ0X9yefaXGS95At5q8NDt_v8CIM825jJklg0hrMd-yvpK_9ZG_ev8jViK78G7XN6jmy882bZPZAgcKT4tf879rKtMR_m7v4SJQAy7Jf1WnDr-U_Ty5s8bAnTCZFj99kK-S0mSoeBbgyepk1Cvez0fsw60jovxH8q_DPJPFfFETGQKYmDWQ34T1MeBllgtfZ2_Ayp5Dd4RBewDQTb_c1-cgmXy4rE_rcHz751aRsYSvhHU07QnzpjtFO6oo0i3bjWD84SCxhoevXEm-5TgBLNeX_WGv1raazwgAoV_7lpbvbGgsVbEcjyAkx05j81wp9RU3NnaKxQOpstOFW3C7ER-jx9niGplwpFS_As7t2L3Z0_ww2XwS1LHyMDXEYU3UPSP3EA7aW12qoNpxfe1ep0Ky-4kc1tFf3qq9syIvTgmXhXjGqxD8m3PvZsxlpHV89RVqFrrbCkTqIH3gm9fw"}
项目源码:
RichCodeBox其中包含了代码JWT、客户端模式、授权码模式等。
另外,我们也可以自己定制OAuthAuthorizationServerProvider。我在JWT中有用到,以下代码只做了解代码如下:
using Microsoft.Owin.Security;using Microsoft.Owin.Security.DataHandler.Encoder;using Microsoft.Owin.Security.OAuth;using System;using System.Collections.Generic;using System.IdentityModel.Tokens;using System.Linq;using System.Security.Claims;using System.Threading.Tasks;using System.Web;namespace WebApplication3.OAuth{ ////// /// public class CustomAuthorizationServerProvider : OAuthAuthorizationServerProvider { ////// /// public CustomAuthorizationServerProvider() { } ////// /// private TokenValidationParameters _validationParameters; ////// /// /// public CustomAuthorizationServerProvider(TokenValidationParameters validationParameters) { _validationParameters = validationParameters; } ////// /// /// ///public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { var secret = TextEncodings.Base64Url.Decode("IxrAjDoa2FqElO7IhrSrUJELhUckePEPVpaePlS_Xaw");//秘钥 var username = context.UserName; var password = context.Password; string userid; if (!CheckCredential(username, password, out userid)) { context.SetError("invalid_grant", "The user name or password is incorrect"); context.Rejected();//拒绝访问 return Task.FromResult
另外,建议在使用Microsoft.Owin.Security.OAuth默认的AccessToken生成类时,在
配置文件中,添加machineKey的有配置,关于machineKey的生成工具,不在讨论范围。