博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
OAuth2.0授权码模式理论+实践
阅读量:7144 次
发布时间:2019-06-29

本文共 21068 字,大约阅读时间需要 70 分钟。

hot3.png

是一个关于授权(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

我在另一篇博客中

《》,详细介绍了各种微服务身份认证的技术方案。

如果觉得以上理论信息意犹未尽的话,请继续关注鄙人博客,或者搜索相关的资料,做进一步的研究。

下面就以授权码授权模式为例,进行代码的实践。

二、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(context); } var ticket = new AuthenticationTicket(SetClaimsIdentity(context, userid, username), new AuthenticationProperties()); context.Validated(ticket); return Task.FromResult(context); } /// /// /// /// ///
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(context); } /// /// /// /// ///
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context) { return Task.FromResult(context); } /// /// 验证客户端重定向URL /// /// ///
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(0); } /// /// 验证令牌 /// /// ///
public override Task ValidateTokenRequest(OAuthValidateTokenRequestContext context) { context.Validated(); return Task.FromResult(context); } #region 私有方法 /// /// 设置声明 /// /// /// /// ///
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; } /// /// 检测用户凭证 /// /// /// /// ///
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的生成工具,不在讨论范围。

转载于:https://my.oschina.net/lichaoqiang/blog/1510730

你可能感兴趣的文章
android开发,assets下面的资源文件不会变化/改动
查看>>
Kinect开发学习笔记之(一)Kinect介绍和应用
查看>>
[LeetCode] 3Sum Closest 最近三数之和
查看>>
如何解决严重拖延症
查看>>
C#操作 iis启用父目录
查看>>
JS正则表达式验证数字非常全
查看>>
Android AES加密算法,现在实际上
查看>>
机器学习中的正则化和范数规则化
查看>>
C#修改文件或文件夹的权限,为指定用户、用户组添加完全控制权限
查看>>
Datazen自定义地图
查看>>
Calculate CRC32 as in STM32 hardware (EWARM v.5.50 and later)
查看>>
Android Sdk 国内镜像下载地址
查看>>
strcmp的源码实现
查看>>
Java多线程7:死锁
查看>>
概率图形模型(PGM)学习笔记(四)-贝叶斯网络-伯努利贝叶斯-贝叶斯多项式...
查看>>
worker_pool的例子
查看>>
Android 插件化
查看>>
Yii2的深入学习--自动加载机制
查看>>
sqlserver修改增删改字段
查看>>
设计模式介绍、分类、原则
查看>>