OAuth是一個關於授權(Authorization)的開放網絡標準,目前的版本是2.0版。註意是Authorization(授權),而不是Authentication(認證)。用來做Authentication(認證)的標準叫做openid connect。
一、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
{
/// <summary>
/// 應用程序啟動類
/// </summary>
public class Startup
{
/// <summary>
/// 用來存放臨時授權碼 線程安全
/// </summary>
private readonly ConcurrentDictionary<string, string> _authenticationCodes = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
/// <summary>
/// 配置授權
/// </summary>
/// <param name="app"></param>
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
{
/// <summary>
///
/// </summary>
public IAuthenticationManager AuthenticationManager
{
get { return HttpContext.GetOwinContext().Authentication; }
}
//
// GET: /Account/
public ActionResult Index()
{
return View();
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public ActionResult Login(string returnUrl)
{
ViewBag.returnUrl = Uri.EscapeDataString(returnUrl);
return View();
}
/// <summary>
///
/// </summary>
/// <param name="model"></param>
/// <param name="returnUrl"></param>
/// <returns></returns>
[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
{
/// <summary>
/// <![CDATA[驗證授權]]>
/// </summary>
public class OAuthController : Controller
{
/// <summary>
/// <!--授權-->
/// </summary>
/// <returns></returns>
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();
}
/// <summary>
///<![CDATA[獲取訪問令牌]]>
/// </summary>
/// <returns></returns>
public async Task<ActionResult> 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<string, string> dict = new Dictionary<string, string>();
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"}
項目源碼:https://gitee.com/lichaoqiang/RichCodeBox
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
{
/// <summary>
///
/// </summary>
public class CustomAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
/// <summary>
///
/// </summary>
public CustomAuthorizationServerProvider()
{
}
/// <summary>
///
/// </summary>
private TokenValidationParameters _validationParameters;
/// <summary>
///
/// </summary>
/// <param name="validationParameters"></param>
public CustomAuthorizationServerProvider(TokenValidationParameters validationParameters)
{
_validationParameters = validationParameters;
}
/// <summary>
/// <![CDATA[授權資源擁有者證書]]>
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
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<object>(context);
}
var ticket = new AuthenticationTicket(SetClaimsIdentity(context, userid, username), new AuthenticationProperties());
context.Validated(ticket);
return Task.FromResult<object>(context);
}
/// <summary>
/// <![CDATA[驗證客戶端授權]]>
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string client_id;
string client_secret;
context.TryGetFormCredentials(out client_id, out client_secret);
//驗證clientid
if (string.IsNullOrEmpty(context.ClientId) ||
context.ClientId != Constants.Const.OAuth2.CLIENT_ID)
{
context.SetError("ClientId is incorrect!");
context.Rejected();
}
//正常
else
{
context.Validated();
}
return Task.FromResult<object>(context);
}
/// <summary>
///
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
return Task.FromResult<object>(context);
}
/// <summary>
/// 驗證客戶端重定向URL
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (string.IsNullOrEmpty(context.ClientId) ||
context.ClientId != Constants.Const.OAuth2.CLIENT_ID)
{
context.SetError("client_id is incorrect!");
context.Rejected();
}
if (string.IsNullOrEmpty(context.RedirectUri))
{
context.SetError("redirect_uri is null!");
context.Rejected();
}
else
{
context.Validated(context.RedirectUri);
}
return Task.FromResult<object>(0);
}
/// <summary>
/// 驗證令牌
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override Task ValidateTokenRequest(OAuthValidateTokenRequestContext context)
{
context.Validated();
return Task.FromResult<object>(context);
}
#region 私有方法
/// <summary>
/// 設置聲明
/// </summary>
/// <param name="context"></param>
/// <param name="userid"></param>
/// <param name="usercode"></param>
/// <returns></returns>
private static ClaimsIdentity SetClaimsIdentity(OAuthGrantResourceOwnerCredentialsContext context, string userid, string usercode)
{
var identity = new ClaimsIdentity("JWT");
identity.AddClaim(new Claim(JwtRegisteredClaimNames.Sub, userid));
identity.AddClaim(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()));
identity.AddClaim(new Claim(ClaimTypes.Name, usercode));
identity.AddClaim(new Claim(ClaimTypes.Role, "1"));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, usercode));
return identity;
}
/// <summary>
/// 檢測用戶憑證
/// </summary>
/// <param name="usernme"></param>
/// <param name="password"></param>
/// <param name="userid"></param>
/// <returns></returns>
private static bool CheckCredential(string usernme, string password, out string userid)
{
var success = false;
// 用戶名和密碼驗證
if (usernme == "admin" && password == "admin")
{
userid = "1";
success = true;
}
else
{
userid = string.Empty;
}
return success;
}
#endregion 私有方法
}
}
另外,建議在使用Microsoft.Owin.Security.OAuth默認的AccessToken生成類時,在
配置文件中,添加machineKey的有配置,關於machineKey的生成工具,不在討論範圍。
Tags: 授權 客戶端 模式 資源 服務器 用戶
文章來源: