OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。

OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的 2 小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth 让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。


以上概念来自:https://zh.wikipedia.org/wiki/OAuth

OAuth 是什么?为什么要使用 OAuth?上面的概念已经很明确了,这里就不详细说明了。

阅读目录:

  • 运行流程和授权模式
  • 授权码模式(authorization code)
  • 简化模式(implicit grant type)
  • 密码模式(resource owner password credentials)
  • 客户端模式(Client Credentials Grant)

开源地址:https://github.com/yuezhongxin/OAuth2.Demo

1. 运行流程和授权模式

关于 OAuth 2.0 的运行流程(来自 RFC 6749):

这里我们模拟一个场景:用户听落网,但需要登录才能收藏期刊,然后用快捷登录方式,使用微博的账号和密码登录后,落网就可以访问到微博的账号信息等,并且在落网也已登录,最后用户就可以收藏期刊了。

结合上面的场景,详细说下 OAuth 2.0 的运行流程:

  • (A) 用户登录落网,落网询求用户的登录授权(真实操作是用户在落网登录)。
  • (B) 用户同意登录授权(真实操作是用户打开了快捷登录,用户输入了微博的账号和密码)。
  • (C) 由落网跳转到微博的授权页面,并请求授权(微博账号和密码在这里需要)。
  • (D) 微博验证用户输入的账号和密码,如果成功,则将 access_token 返回给落网。
  • (E) 落网拿到返回的 access_token,请求微博。
  • (F) 微博验证落网提供的 access_token,如果成功,则将微博的账户信息返回给落网。

图中的名词解释:

  • Client -> 落网
  • Resource Owner -> 用户
  • Authorization Server -> 微博授权服务
  • Resource Server -> 微博资源服务

其实,我不是很理解 ABC 操作,我觉得 ABC 可以合成一个 C:落网打开微博的授权页面,用户输入微博的账号和密码,请求验证。

OAuth 2.0 四种授权模式:

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

下面我们使用 ASP.NET WebApi OWIN,分别实现上面的四种授权模式。

2. 授权码模式(authorization code)

简单解释:落网提供一些授权凭证,从微博授权服务获取到 authorization_code,然后根据 authorization_code,再获取到 access_token,落网需要请求微博授权服务两次。

第一次请求授权服务(获取 authorization_code),需要的参数:

  • grant_type:必选,授权模式,值为 "authorization_code"。
  • response_type:必选,授权类型,值固定为 "code"。
  • client_id:必选,客户端 ID。
  • redirect_uri:必选,重定向 URI,URL 中会包含 authorization_code。
  • scope:可选,申请的权限范围,比如微博授权服务值为 follow_app_official_microblog。
  • state:可选,客户端的当前状态,可以指定任意值,授权服务器会原封不动地返回这个值,比如微博授权服务值为 weibo。

第二次请求授权服务(获取 access_token),需要的参数:

  • grant_type:必选,授权模式,值为 "authorization_code"。
  • code:必选,授权码,值为上面请求返回的 authorization_code。
  • redirect_uri:必选,重定向 URI,必须和上面请求的 redirect_uri 值一样。
  • client_id:必选,客户端 ID。

第二次请求授权服务(获取 access_token),返回的参数:

  • access_token:访问令牌.
  • token_type:令牌类型,值一般为 "bearer"。
  • expires_in:过期时间,单位为秒。
  • refresh_token:更新令牌,用来获取下一次的访问令牌。
  • scope:权限范围。

ASP.NET WebApi OWIN 需要安装的程序包:

  • Owin
  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.Owin.Security.OAuth
  • Microsoft.Owin.Security.Cookies
  • Microsoft.AspNet.Identity.Owin

在项目中创建 Startup.cs 文件,添加如下代码:

  1. public partial class Startup
  2. {
  3. public void ConfigureAuth(IAppBuilder app)
  4. {
  5. var OAuthOptions = new OAuthAuthorizationServerOptions
  6. {
  7. AllowInsecureHttp = true,
  8. AuthenticationMode = AuthenticationMode.Active,
  9. TokenEndpointPath = new PathString("/token"), //获取 access_token 授权服务请求地址
  10. AuthorizeEndpointPath=new PathString("/authorize"), //获取 authorization_code 授权服务请求地址
  11. AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10), //access_token 过期时间
  12. Provider = new OpenAuthorizationServerProvider(), //access_token 相关授权服务
  13. AuthorizationCodeProvider = new OpenAuthorizationCodeProvider(), //authorization_code 授权服务
  14. RefreshTokenProvider = new OpenRefreshTokenProvider() //refresh_token 授权服务
  15. };
  16. app.UseOAuthBearerTokens(OAuthOptions); //表示 token_type 使用 bearer 方式
  17. }
  18. }

OpenAuthorizationServerProvider 示例代码:

  1. public class OpenAuthorizationServerProvider : OAuthAuthorizationServerProvider
  2. {
  3. /// <summary>
  4. /// 验证 client 信息
  5. /// </summary>
  6. public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
  7. {
  8. string clientId;
  9. string clientSecret;
  10. if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
  11. {
  12. context.TryGetFormCredentials(out clientId, out clientSecret);
  13. }
  14. if (clientId != "xishuai")
  15. {
  16. context.SetError("invalid_client", "client is not valid");
  17. return;
  18. }
  19. context.Validated();
  20. }
  21. /// <summary>
  22. /// 生成 authorization_code(authorization code 授权方式)、生成 access_token (implicit 授权模式)
  23. /// </summary>
  24. public override async Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
  25. {
  26. if (context.AuthorizeRequest.IsImplicitGrantType)
  27. {
  28. //implicit 授权方式
  29. var identity = new ClaimsIdentity("Bearer");
  30. context.OwinContext.Authentication.SignIn(identity);
  31. context.RequestCompleted();
  32. }
  33. else if (context.AuthorizeRequest.IsAuthorizationCodeGrantType)
  34. {
  35. //authorization code 授权方式
  36. var redirectUri = context.Request.Query["redirect_uri"];
  37. var clientId = context.Request.Query["client_id"];
  38. var identity = new ClaimsIdentity(new GenericIdentity(
  39. clientId, OAuthDefaults.AuthenticationType));
  40. var authorizeCodeContext = new AuthenticationTokenCreateContext(
  41. context.OwinContext,
  42. context.Options.AuthorizationCodeFormat,
  43. new AuthenticationTicket(
  44. identity,
  45. new AuthenticationProperties(new Dictionary<string, string>
  46. {
  47. {"client_id", clientId},
  48. {"redirect_uri", redirectUri}
  49. })
  50. {
  51. IssuedUtc = DateTimeOffset.UtcNow,
  52. ExpiresUtc = DateTimeOffset.UtcNow.Add(context.Options.AuthorizationCodeExpireTimeSpan)
  53. }));
  54. await context.Options.AuthorizationCodeProvider.CreateAsync(authorizeCodeContext);
  55. context.Response.Redirect(redirectUri + "?code=" + Uri.EscapeDataString(authorizeCodeContext.Token));
  56. context.RequestCompleted();
  57. }
  58. }
  59. /// <summary>
  60. /// 验证 authorization_code 的请求
  61. /// </summary>
  62. public override async Task ValidateAuthorizeRequest(OAuthValidateAuthorizeRequestContext context)
  63. {
  64. if (context.AuthorizeRequest.ClientId == "xishuai" &&
  65. (context.AuthorizeRequest.IsAuthorizationCodeGrantType || context.AuthorizeRequest.IsImplicitGrantType))
  66. {
  67. context.Validated();
  68. }
  69. else
  70. {
  71. context.Rejected();
  72. }
  73. }
  74. /// <summary>
  75. /// 验证 redirect_uri
  76. /// </summary>
  77. public override async Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
  78. {
  79. context.Validated(context.RedirectUri);
  80. }
  81. /// <summary>
  82. /// 验证 access_token 的请求
  83. /// </summary>
  84. public override async Task ValidateTokenRequest(OAuthValidateTokenRequestContext context)
  85. {
  86. if (context.TokenRequest.IsAuthorizationCodeGrantType || context.TokenRequest.IsRefreshTokenGrantType)
  87. {
  88. context.Validated();
  89. }
  90. else
  91. {
  92. context.Rejected();
  93. }
  94. }
  95. }

需要注意的是,ValidateClientAuthentication 并不需要对 clientSecret 进行验证,另外,AuthorizeEndpoint 只是生成 authorization_code,并没有生成 access_token,生成操作在 OpenAuthorizationCodeProvider 中的 Receive 方法。

OpenAuthorizationCodeProvider 示例代码:

  1. public class OpenAuthorizationCodeProvider : AuthenticationTokenProvider
  2. {
  3. private readonly ConcurrentDictionary<string, string> _authenticationCodes = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
  4. /// <summary>
  5. /// 生成 authorization_code
  6. /// </summary>
  7. public override void Create(AuthenticationTokenCreateContext context)
  8. {
  9. context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
  10. _authenticationCodes[context.Token] = context.SerializeTicket();
  11. }
  12. /// <summary>
  13. /// 由 authorization_code 解析成 access_token
  14. /// </summary>
  15. public override void Receive(AuthenticationTokenReceiveContext context)
  16. {
  17. string value;
  18. if (_authenticationCodes.TryRemove(context.Token, out value))
  19. {
  20. context.DeserializeTicket(value);
  21. }
  22. }
  23. }

上面 Create 方法是 await context.Options.AuthorizationCodeProvider.CreateAsync(authorizeCodeContext); 的重载方法。

OpenRefreshTokenProvider 示例代码:

  1. public class OpenRefreshTokenProvider : AuthenticationTokenProvider
  2. {
  3. private static ConcurrentDictionary<string, string> _refreshTokens = new ConcurrentDictionary<string, string>();
  4. /// <summary>
  5. /// 生成 refresh_token
  6. /// </summary>
  7. public override void Create(AuthenticationTokenCreateContext context)
  8. {
  9. context.Ticket.Properties.IssuedUtc = DateTime.UtcNow;
  10. context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(60);
  11. context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
  12. _refreshTokens[context.Token] = context.SerializeTicket();
  13. }
  14. /// <summary>
  15. /// 由 refresh_token 解析成 access_token
  16. /// </summary>
  17. public override void Receive(AuthenticationTokenReceiveContext context)
  18. {
  19. string value;
  20. if (_refreshTokens.TryRemove(context.Token, out value))
  21. {
  22. context.DeserializeTicket(value);
  23. }
  24. }
  25. }

refresh_token 的作用就是,在 access_token 过期的时候,不需要再通过一些凭证申请 access_token,而是直接通过 refresh_token 就可以重新申请 access_token。

另外,需要一个 api 来接受 authorization_code(来自 redirect_uri 的回调跳转),实现代码如下:

  1. public class CodesController : ApiController
  2. {
  3. [HttpGet]
  4. [Route("api/authorization_code")]
  5. public HttpResponseMessage Get(string code)
  6. {
  7. return new HttpResponseMessage()
  8. {
  9. Content = new StringContent(code, Encoding.UTF8, "text/plain")
  10. };
  11. }
  12. }

基本上面代码已经实现了,单元测试代码如下:

  1. public class OAuthClientTest
  2. {
  3. private const string HOST_ADDRESS = "http://localhost:8001";
  4. private IDisposable _webApp;
  5. private static HttpClient _httpClient;
  6. public OAuthClientTest()
  7. {
  8. _webApp = WebApp.Start<Startup>(HOST_ADDRESS);
  9. Console.WriteLine("Web API started!");
  10. _httpClient = new HttpClient();
  11. _httpClient.BaseAddress = new Uri(HOST_ADDRESS);
  12. Console.WriteLine("HttpClient started!");
  13. }
  14. private static async Task<TokenResponse> GetToken(string grantType, string refreshToken = null, string userName = null, string password = null, string authorizationCode = null)
  15. {
  16. var clientId = "xishuai";
  17. var clientSecret = "123";
  18. var parameters = new Dictionary<string, string>();
  19. parameters.Add("grant_type", grantType);
  20. if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password))
  21. {
  22. parameters.Add("username", userName);
  23. parameters.Add("password", password);
  24. }
  25. if (!string.IsNullOrEmpty(authorizationCode))
  26. {
  27. parameters.Add("code", authorizationCode);
  28. parameters.Add("redirect_uri", "http://localhost:8001/api/authorization_code"); //和获取 authorization_code 的 redirect_uri 必须一致,不然会报错
  29. }
  30. if (!string.IsNullOrEmpty(refreshToken))
  31. {
  32. parameters.Add("refresh_token", refreshToken);
  33. }
  34. _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
  35. "Basic",
  36. Convert.ToBase64String(Encoding.ASCII.GetBytes(clientId + ":" + clientSecret)));
  37. var response = await _httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters));
  38. var responseValue = await response.Content.ReadAsStringAsync();
  39. if (response.StatusCode != HttpStatusCode.OK)
  40. {
  41. Console.WriteLine(response.StatusCode);
  42. Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
  43. return null;
  44. }
  45. return await response.Content.ReadAsAsync<TokenResponse>();
  46. }
  47. private static async Task<string> GetAuthorizationCode()
  48. {
  49. var clientId = "xishuai";
  50. var response = await _httpClient.GetAsync($"/authorize?grant_type=authorization_code&response_type=code&client_id={clientId}&redirect_uri={HttpUtility.UrlEncode("http://localhost:8001/api/authorization_code")}");
  51. var authorizationCode = await response.Content.ReadAsStringAsync();
  52. if (response.StatusCode != HttpStatusCode.OK)
  53. {
  54. Console.WriteLine(response.StatusCode);
  55. Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
  56. return null;
  57. }
  58. return authorizationCode;
  59. }
  60. [Fact]
  61. public async Task OAuth_AuthorizationCode_Test()
  62. {
  63. var authorizationCode = GetAuthorizationCode().Result; //获取 authorization_code
  64. var tokenResponse = GetToken("authorization_code", null, null, null, authorizationCode).Result; //根据 authorization_code 获取 access_token
  65. _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
  66. var response = await _httpClient.GetAsync($"/api/values");
  67. if (response.StatusCode != HttpStatusCode.OK)
  68. {
  69. Console.WriteLine(response.StatusCode);
  70. Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
  71. }
  72. Console.WriteLine(await response.Content.ReadAsStringAsync());
  73. Assert.Equal(HttpStatusCode.OK, response.StatusCode);
  74. Thread.Sleep(10000);
  75. var tokenResponseTwo = GetToken("refresh_token", tokenResponse.RefreshToken).Result; //根据 refresh_token 获取 access_token
  76. _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponseTwo.AccessToken);
  77. var responseTwo = await _httpClient.GetAsync($"/api/values");
  78. Assert.Equal(HttpStatusCode.OK, responseTwo.StatusCode);
  79. }
  80. }

Startup 配置的 access_token 过期时间是 10s,线程休眠 10s,是为了测试 refresh_token。

上面单元测试代码,执行成功,当然也可以用 Postman 模拟请求测试。

3. 简化模式(implicit grant type)

简单解释:授权码模式的简化版,省略 authorization_code,并且 access_token 以 URL 参数返回(比如 #token=xxxx)。

请求授权服务(只有一次),需要的参数:

  • response_type:必选,授权类型,值固定为 "token"。
  • client_id:必选,客户端 ID。
  • redirect_uri:必选,重定向 URI,URL 中会包含 access_token。
  • scope:可选,申请的权限范围,比如微博授权服务值为 follow_app_official_microblog。
  • state:可选,客户端的当前状态,可以指定任意值,授权服务器会原封不动地返回这个值,比如微博授权服务值为 weibo。

需要注意的是,简化模式请求参数并不需要 grant_type,并且可以用 http get 直接请求。

Startup 代码:

  1. public partial class Startup
  2. {
  3. public void ConfigureAuth(IAppBuilder app)
  4. {
  5. var OAuthOptions = new OAuthAuthorizationServerOptions
  6. {
  7. AllowInsecureHttp = true,
  8. AuthenticationMode = AuthenticationMode.Active,
  9. TokenEndpointPath = new PathString("/token"), //获取 access_token 授权服务请求地址
  10. AuthorizeEndpointPath=new PathString("/authorize"), //获取 authorization_code 授权服务请求地址
  11. AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10), //access_token 过期时间
  12. Provider = new OpenAuthorizationServerProvider(), //access_token 相关授权服务
  13. RefreshTokenProvider = new OpenRefreshTokenProvider() //refresh_token 授权服务
  14. };
  15. app.UseOAuthBearerTokens(OAuthOptions); //表示 token_type 使用 bearer 方式
  16. }
  17. }

OpenRefreshTokenProvider、OpenAuthorizationServerProvider 的代码就不贴了,和上面授权码模式一样,只不过在 OpenAuthorizationServerProvider 的 AuthorizeEndpoint 方法中有 IsImplicitGrantType 判断,示例代码:

  1. var identity = new ClaimsIdentity("Bearer");
  2. context.OwinContext.Authentication.SignIn(identity);
  3. context.RequestCompleted();

这段代码执行会直接回调 redirect_uri,并附上 access_token,接受示例代码:

  1. [HttpGet]
  2. [Route("api/access_token")]
  3. public HttpResponseMessage GetToken()
  4. {
  5. var url = Request.RequestUri;
  6. return new HttpResponseMessage()
  7. {
  8. Content = new StringContent("", Encoding.UTF8, "text/plain")
  9. };
  10. }

单元测试代码:

  1. [Fact]
  2. public async Task OAuth_Implicit_Test()
  3. {
  4. var clientId = "xishuai";
  5. var tokenResponse = await _httpClient.GetAsync($"/authorize?response_type=token&client_id={clientId}&redirect_uri={HttpUtility.UrlEncode("http://localhost:8001/api/access_token")}");
  6. //redirect_uri: http://localhost:8001/api/access_token#access_token=AQAAANCMnd8BFdERjHoAwE_Cl-sBAAAAfoPB4HZ0PUe-X6h0UUs2q42&token_type=bearer&expires_in=10
  7. var accessToken = "";//get form redirect_uri
  8. _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
  9. var response = await _httpClient.GetAsync($"/api/values");
  10. if (response.StatusCode != HttpStatusCode.OK)
  11. {
  12. Console.WriteLine(response.StatusCode);
  13. Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
  14. }
  15. Console.WriteLine(await response.Content.ReadAsStringAsync());
  16. Assert.Equal(HttpStatusCode.OK, response.StatusCode);
  17. }

回调 redirect_uri 中的 access_token 参数值,因为在 URL 的 # 后,后端不好获取到,所以这里的单元测试只是示例,并不能执行成功,建议使用 Poastman 进行测试。

4. 密码模式(resource owner password credentials)

简单解释:在一开始叙述的 OAuth 授权流程的时候,其实就是密码模式,落网发起授权请求,用户在微博的授权页面填写账号和密码,验证成功则返回 access_token,所以,在此过程中,用户填写的账号和密码,和落网没有半毛钱关系,不会存在账户信息被第三方窃取问题。

请求授权服务(只有一次),需要的参数:

  • grant_type:必选,授权模式,值固定为 "password"。
  • username:必选,用户名。
  • password:必选,用户密码。
  • scope:可选,申请的权限范围,比如微博授权服务值为 follow_app_official_microblog。

Startup 代码:

  1. public partial class Startup
  2. {
  3. public void ConfigureAuth(IAppBuilder app)
  4. {
  5. var OAuthOptions = new OAuthAuthorizationServerOptions
  6. {
  7. AllowInsecureHttp = true,
  8. AuthenticationMode = AuthenticationMode.Active,
  9. TokenEndpointPath = new PathString("/token"), //获取 access_token 授权服务请求地址
  10. AuthorizeEndpointPath=new PathString("/authorize"), //获取 authorization_code 授权服务请求地址
  11. AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10), //access_token 过期时间
  12. Provider = new OpenAuthorizationServerProvider(), //access_token 相关授权服务
  13. RefreshTokenProvider = new OpenRefreshTokenProvider() //refresh_token 授权服务
  14. };
  15. app.UseOAuthBearerTokens(OAuthOptions); //表示 token_type 使用 bearer 方式
  16. }
  17. }

OpenAuthorizationServerProvider 示例代码:

  1. public class OpenAuthorizationServerProvider : OAuthAuthorizationServerProvider
  2. {
  3. /// <summary>
  4. /// 验证 client 信息
  5. /// </summary>
  6. public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
  7. {
  8. string clientId;
  9. string clientSecret;
  10. if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
  11. {
  12. context.TryGetFormCredentials(out clientId, out clientSecret);
  13. }
  14. if (clientId != "xishuai")
  15. {
  16. context.SetError("invalid_client", "client is not valid");
  17. return;
  18. }
  19. context.Validated();
  20. }
  21. /// <summary>
  22. /// 生成 access_token(resource owner password credentials 授权方式)
  23. /// </summary>
  24. public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
  25. {
  26. if (string.IsNullOrEmpty(context.UserName))
  27. {
  28. context.SetError("invalid_username", "username is not valid");
  29. return;
  30. }
  31. if (string.IsNullOrEmpty(context.Password))
  32. {
  33. context.SetError("invalid_password", "password is not valid");
  34. return;
  35. }
  36. if (context.UserName != "xishuai" || context.Password != "123")
  37. {
  38. context.SetError("invalid_identity", "username or password is not valid");
  39. return;
  40. }
  41. var OAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
  42. OAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
  43. context.Validated(OAuthIdentity);
  44. }
  45. }

GrantResourceOwnerCredentials 内部可以调用外部服务,以进行对用户账户信息的验证。

单元测试代码:

  1. [Fact]
  2. public async Task OAuth_Password_Test()
  3. {
  4. var tokenResponse = GetToken("password", null, "xishuai", "123").Result; //获取 access_token
  5. _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
  6. var response = await _httpClient.GetAsync($"/api/values");
  7. if (response.StatusCode != HttpStatusCode.OK)
  8. {
  9. Console.WriteLine(response.StatusCode);
  10. Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
  11. }
  12. Console.WriteLine(await response.Content.ReadAsStringAsync());
  13. Assert.Equal(HttpStatusCode.OK, response.StatusCode);
  14. Thread.Sleep(10000);
  15. var tokenResponseTwo = GetToken("refresh_token", tokenResponse.RefreshToken).Result;
  16. _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponseTwo.AccessToken);
  17. var responseTwo = await _httpClient.GetAsync($"/api/values");
  18. Assert.Equal(HttpStatusCode.OK, responseTwo.StatusCode);
  19. }

5. 客户端模式(Client Credentials Grant)

简单解释:顾名思义,客户端模式就是客户端直接向授权服务发起请求,和用户没什么关系,也就是说落网直接向微博提交授权请求,此类的请求不包含用户信息,一般用作应用程序直接的交互等。

请求授权服务(只有一次),需要的参数:

  • grant_type:必选,授权模式,值固定为 "client_credentials"。
  • client_id:必选,客户端 ID。
  • client_secret:必选,客户端密码。
  • scope:可选,申请的权限范围,比如微博授权服务值为 follow_app_official_microblog。

Startup 代码:

  1. public partial class Startup
  2. {
  3. public void ConfigureAuth(IAppBuilder app)
  4. {
  5. var OAuthOptions = new OAuthAuthorizationServerOptions
  6. {
  7. AllowInsecureHttp = true,
  8. AuthenticationMode = AuthenticationMode.Active,
  9. TokenEndpointPath = new PathString("/token"), //获取 access_token 授权服务请求地址
  10. AuthorizeEndpointPath=new PathString("/authorize"), //获取 authorization_code 授权服务请求地址
  11. AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10), //access_token 过期时间
  12. Provider = new OpenAuthorizationServerProvider(), //access_token 相关授权服务
  13. RefreshTokenProvider = new OpenRefreshTokenProvider() //refresh_token 授权服务
  14. };
  15. app.UseOAuthBearerTokens(OAuthOptions); //表示 token_type 使用 bearer 方式
  16. }
  17. }

OpenAuthorizationServerProvider 示例代码:

  1. public class OpenAuthorizationServerProvider : OAuthAuthorizationServerProvider
  2. {
  3. /// <summary>
  4. /// 验证 client 信息
  5. /// </summary>
  6. public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
  7. {
  8. string clientId;
  9. string clientSecret;
  10. if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
  11. {
  12. context.TryGetFormCredentials(out clientId, out clientSecret);
  13. }
  14. if (clientId != "xishuai" || clientSecret != "123")
  15. {
  16. context.SetError("invalid_client", "client or clientSecret is not valid");
  17. return;
  18. }
  19. context.Validated();
  20. }
  21. /// <summary>
  22. /// 生成 access_token(client credentials 授权方式)
  23. /// </summary>
  24. public override async Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
  25. {
  26. var identity = new ClaimsIdentity(new GenericIdentity(
  27. context.ClientId, OAuthDefaults.AuthenticationType),
  28. context.Scope.Select(x => new Claim("urn:oauth:scope", x)));
  29. context.Validated(identity);
  30. }
  31. }

和其他授权模式不同,客户端授权模式需要对 client_secret 进行验证(ValidateClientAuthentication)。

单元测试代码:

  1. [Fact]
  2. public async Task OAuth_ClientCredentials_Test()
  3. {
  4. var tokenResponse = GetToken("client_credentials").Result; //获取 access_token
  5. _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
  6. var response = await _httpClient.GetAsync($"/api/values");
  7. if (response.StatusCode != HttpStatusCode.OK)
  8. {
  9. Console.WriteLine(response.StatusCode);
  10. Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
  11. }
  12. Console.WriteLine(await response.Content.ReadAsStringAsync());
  13. Assert.Equal(HttpStatusCode.OK, response.StatusCode);
  14. Thread.Sleep(10000);
  15. var tokenResponseTwo = GetToken("refresh_token", tokenResponse.RefreshToken).Result;
  16. _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponseTwo.AccessToken);
  17. var responseTwo = await _httpClient.GetAsync($"/api/values");
  18. Assert.Equal(HttpStatusCode.OK, responseTwo.StatusCode);
  19. }

除了上面四种授权模式之外,还有一种就是更新令牌(refresh token),单元测试代码中已经体现了,需要额外的两个参数:

  • grant_type:必选,授权模式,值固定为 "refresh_token"。
  • refresh_token:必选,授权返回的 refresh_token。

最后,总结下四种授权模式的应用场景:

  • 授权码模式(authorization code):引入 authorization_code,可以增加系统的安全性,和客户端应用场景差不多,但一般用于 Server 端。
  • 简化模式(implicit):无需 Server 端的介入,前端可以直接完成,一般用于前端操作。
  • 密码模式(resource owner password credentials):和用户账户相关,一般用于第三方登录。
  • 客户端模式(client credentials):和用户无关,一般用于应用程序和 api 之间的交互场景,比如落网开放出 api,供第三方开发者进行调用数据等。

开源地址:https://github.com/yuezhongxin/OAuth2.Demo

参考资料:

ASP.NET WebApi OWIN 实现 OAuth 2.0的更多相关文章

  1. ASP.NET WebApi OWIN 实现 OAuth 2.0(自定义获取 Token)

    相关文章:ASP.NET WebApi OWIN 实现 OAuth 2.0 之前的项目实现,Token 放在请求头的 Headers 里面,类似于这样: Accept: application/jso ...

  2. [转]ASP.NET WebApi OWIN 实现 OAuth 2.0

    OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用. OAuth 允许用户提供一个令牌, ...

  3. NET WebApi OWIN 实现 OAuth 2.0

    NET WebApi OWIN 实现 OAuth 2.0 OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和 ...

  4. asp.net权限认证:OWIN实现OAuth 2.0 之简化模式(Implicit)

    asp.net权限认证系列 asp.net权限认证:Forms认证 asp.net权限认证:HTTP基本认证(http basic) asp.net权限认证:Windows认证 asp.net权限认证 ...

  5. asp.net权限认证:OWIN实现OAuth 2.0 之客户端模式(Client Credential)

    asp.net权限认证系列 asp.net权限认证:Forms认证 asp.net权限认证:HTTP基本认证(http basic) asp.net权限认证:Windows认证 asp.net权限认证 ...

  6. asp.net权限认证:OWIN实现OAuth 2.0 之密码模式(Resource Owner Password Credential)

    asp.net权限认证系列 asp.net权限认证:Forms认证 asp.net权限认证:HTTP基本认证(http basic) asp.net权限认证:Windows认证 asp.net权限认证 ...

  7. asp.net权限认证:OWIN实现OAuth 2.0 之授权码模式(Authorization Code)

    asp.net权限认证系列 asp.net权限认证:Forms认证 asp.net权限认证:HTTP基本认证(http basic) asp.net权限认证:Windows认证 asp.net权限认证 ...

  8. 基于ASP.NET WebAPI OWIN实现Self-Host项目实战

    引用 寄宿ASP.NET Web API 不一定需要IIS 的支持,我们可以采用Self Host 的方式使用任意类型的应用程序(控制台.Windows Forms 应用.WPF 应用甚至是Windo ...

  9. Asp.net webapi Owin Get request form data

    var formData = await context.Request.ReadFormAsync() as IEnumerable<KeyValuePair<string, strin ...

随机推荐

  1. IE6、7下html标签间存在空白符,导致渲染后占用多余空白位置的原因及解决方法

    直接上图:原因:该div包含的内容是靠后台进行print操作,输出的.如果没有输出任何内容,浏览器会默认给该空白区域添加空白符.在IE6.7下,浏览器解析渲染时,会认为空白符也是占位置的,默认其具有字 ...

  2. 可爱的豆子——使用Beans思想让Python代码更易维护

    title: 可爱的豆子--使用Beans思想让Python代码更易维护 toc: false comments: true date: 2016-06-19 21:43:33 tags: [Pyth ...

  3. .NetCore中的日志(1)日志组件解析

    .NetCore中的日志(1)日志组件解析 0x00 问题的产生 日志记录功能在开发中很常用,可以记录程序运行的细节,也可以记录用户的行为.在之前开发时我一般都是用自己写的小工具来记录日志,输出目标包 ...

  4. Linux CentOS 配置Tomcat环境

    一.下载Tomcat 下载Tomcat方式也有两种,可以参考我的前一篇博文Linux CentOS配置JDK环境,这边就不再赘述. 二.在Linux处理Tomcat包 1.创建tomcat文件夹 mk ...

  5. 关于Layer弹出框初探

    layer至今仍作为layui的代表作,她的受众广泛并非偶然,而是这五年多的坚持,不断完善和维护.不断建设和提升社区服务,使得猿们纷纷自发传播,乃至于成为今天的Layui最强劲的源动力.目前,laye ...

  6. 一个表缺失索引发的CPU资源瓶颈案例

    背景 近几日,公司的应用团队反应业务系统突然变慢了,之前是一直比较正常.后与业务部门沟通了解详情,得知最近生意比较好,同时也在做大的促销活动,使得业务数据处理的量出现较大的增长,最终系统在处理时出现瓶 ...

  7. WebGIS项目中利用mysql控制点库进行千万条数据坐标转换时的分表分区优化方案

    文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/ 1. 背景 项目中有1000万条历史案卷,为某地方坐标系数据,我们的真实 ...

  8. BPM配置故事之案例2-文本默认值

    Boss感觉方便了很多,然而采购部采购员阿海却还是有点意见,他跑来找小明. 阿海:现在申请都是我在提交,申请人和申请部门能不能不要每次都要填写啊,好麻烦的. 小明:没问题,这个简单. 小明在表单中把申 ...

  9. Android菜单项内容大全

    一.介绍: 菜单是许多应用中常见的用户界面组件. Android3.0版本以前,Android设备会提供一个专用"菜单"按钮呈现常用的一些用户操作, Android3.0版本以后, ...

  10. 学习笔记 :DrawText

    最近在做一个TStringGrid的自绘处理,在画文字处理上遇到了高度的计算问题.后来经过一段时间还是找到了一些方法: 1.使用TLabel 这个方法是有点绕路的,方法倒是简单,就是使用AutoSiz ...