OWIN的全稱是Open Web Interface For .Net, 是MS在VS2013期間引入的全新的概念, 網(wǎng)上已經(jīng)有不少的關于它的信息, 這里我就談下我自己的理解:
- OWIN****是一種規(guī)范和標準, ****不代表特定技術. MS最新出現(xiàn)的一些新的技術, 比如Kanata, Identity, SignalR, 它們只是基于OWIN的不同實現(xiàn).
- OWIN****的核心理念是解耦,協(xié)作和開放---這和MS以前的風格大相徑庭牍鞠,值得引起大家的注意殿遂。
- OWIN****是MS****未來Web****開發(fā)的方向贩汉,想跟著MS路線繼續(xù)開發(fā)Web應用误辑,OWIN是大勢所趨库正。
在這篇博文中杨拐,我們將以 OAuth 的** Resource Owner Password Credentials Grant **的授權方式( grant_type=password )獲取 Access Token,并以這個 Token 調(diào)用與用戶相關的 Web API闸盔。
Resource Owner Password Credentials 這種模式要求用戶提供用戶名和密碼來交換訪問令牌(access_token)。該模式僅用于非常值得信任的用戶琳省,例如API提供者本人所寫的移動應用迎吵。雖然用戶也要求提供密碼,但并不需要存儲在設備上针贬。因為初始驗證之后击费,只需將OAuth的令牌記錄下來即可。如果用戶希望取消授權桦他,因為其真實密碼并沒有被記錄蔫巩,因此無需修改密碼就可以立即取消授權。token本身也只是得到有限的授權,因此相比最傳統(tǒng)的username/password授權批幌,該模式依然更為安全础锐。
對應的應用場景是:為自家的網(wǎng)站開發(fā)手機 App(非第三方 App),只需用戶在 App 上登錄荧缘,無需用戶對 App 所能訪問的數(shù)據(jù)進行授權皆警。
基本流程
-
A. 向用戶索要認證信息
首先,我們必須得讓用戶將認證信息提供給應用程序截粗。對于應用方來說信姓,如果用戶處于不可信的網(wǎng)絡中時,除了需要輸入用戶名和密碼外绸罗,還需要用戶提供一個安全令牌作為用戶的第三個輸入意推。 -
B. 交換訪問令牌
這里的訪問令牌交換過程與授權碼類型的驗證授權(authorization code)很相似。我們要做的就是向認證服務器提交一個POST請求并在其中提供相應的認證和客戶信息珊蟀。
所需的POST參數(shù):
**grant_type 該模式下為"password" **
scope 業(yè)務訪問控制范圍菊值,是一個可選參數(shù)
client_id 應用注冊時獲得的客戶id
client_secret 應用注冊時獲得的客戶密鑰
username 用戶的用戶名
password 用戶的密碼
POST https://xxx.com/token HTTP/1.1Content-type:application/x-www-form-urlencodedAuthorization Basic Base64(clientId:clientSecret)username=irving&password=123456&grant_type=password
-
C. 刷新Token
1).accesstoken 是有過期時間的,到了過期時間這個 access token 就失效育灸,需要刷新腻窒。
2).如果accesstoken會關聯(lián)一定的用戶權限,如果用戶授權更改了磅崭,這個accesstoken需要被刷新以關聯(lián)新的權限儿子。
3).為什么要專門用一個 token 去更新 accesstoken 呢?如果沒有 refreshtoken砸喻,也可以刷新 accesstoken柔逼,但每次刷新都要用戶輸入登錄用戶名與密碼,客戶端直接用 refreshtoken 去更新 accesstoken割岛,無需用戶進行額外的操作愉适。
POST http://localhost:19923/tokenContent-Type: Application/x-www-form-
urlencodedAuthorization Basic Base64(clientId:clientSecret)username=irving&password=123456&grant_type=refresh_token
備注:
有了前面相關token,服務調(diào)用也很簡單
GET https://xxx.com/api/v1/account/profile HTTP/1.1Content-type:application/x-www-form-urlencodedAuthorization Authorization: Bearer {THE TOKEN}
WebApi Startup
get access_token
在 C# 中用 HttpClient 實現(xiàn)一個簡單的客戶端蜂桶,代碼如下:
using System;
using System.Collections.Generic;
using System.Net.Http;
namespace Tdf.OAuthClientTest
{
public class OAuthClientTest
{
private HttpClient _httpClient;
public OAuthClientTest()
{
_httpClient = new HttpClient();
_httpClient.Timeout = TimeSpan.FromMinutes(30);
_httpClient.BaseAddress = new Uri("http://localhost:13719");
}
public void Get_Accesss_Token_By_Client_Credentials_Grant()
{
var parameters = new Dictionary<string, string>();
parameters.Add("UserName", "Bobby");
parameters.Add("Password", "123");
parameters.Add("grant_type", "password");
Console.WriteLine(_httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters))
.Result.Content.ReadAsStringAsync().Result);
}
}
}
這樣儡毕,運行客戶端程序就可以拿到 Access Token 了也切。
refresh_token
在 C# 中用 HttpClient 實現(xiàn)一個簡單的客戶端扑媚,代碼如下:
using System;
using System.Collections.Generic;
using System.Net.Http;
namespace Tdf.OAuthClientTest
{
public class OAuthClientTest
{
private HttpClient _httpClient;
public OAuthClientTest()
{
_httpClient = new HttpClient();
_httpClient.Timeout = TimeSpan.FromMinutes(30);
_httpClient.BaseAddress = new Uri("http://localhost:13719");
}
public void Get_Accesss_Token_By_Client_Credentials_Grant()
{
var parameters = new Dictionary<string, string>();
// refresh_token
parameters.Add("grant_type", "refresh_token");
parameters.Add("refresh_token", "DAB1FE2B-2F84-4534-A620-F6B9B474B503");
Console.WriteLine(_httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters))
.Result.Content.ReadAsStringAsync().Result);
}
}
}
這樣,運行客戶端程序就可以拿到 Access Token 了雷恃。
OWIN WEBAPI
默認Startup.Auth.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Google;
using Microsoft.Owin.Security.OAuth;
using Owin;
using Ems.Web.Providers;
using Ems.Web.Models;
namespace Ems.Web
{
/// <summary>
/// Startup
/// </summary>
public partial class Startup
{
/// <summary>
/// OAuthOptions
/// </summary>
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
/// <summary>
/// PublicClientId
/// </summary>
public static string PublicClientId { get; private set; }
/// <summary>
/// ConfigureAuth
/// 有關配置身份驗證的詳細信息疆股,請訪問 http://go.microsoft.com/fwlink/?LinkId=301864
/// </summary>
/// <param name="app"></param>
public void ConfigureAuth(IAppBuilder app)
{
// 將數(shù)據(jù)庫上下文和用戶管理器配置為對每個請求使用單個實例
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
// 使應用程序可以使用 Cookie 來存儲已登錄用戶的信息
// 并使用 Cookie 來臨時存儲有關使用第三方登錄提供程序登錄的用戶的信息
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// 針對基于 OAuth 的流配置應用程序
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
//在生產(chǎn)模式下設 AllowInsecureHttp = false
AllowInsecureHttp = true
};
// 使應用程序可以使用不記名令牌來驗證用戶身份
app.UseOAuthBearerTokens(OAuthOptions);
// 取消注釋以下行可允許使用第三方登錄提供程序登錄
//app.UseMicrosoftAccountAuthentication(
// clientId: "",
// clientSecret: "");
//app.UseTwitterAuthentication(
// consumerKey: "",
// consumerSecret: "");
//app.UseFacebookAuthentication(
// appId: "",
// appSecret: "");
//app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
//{
// ClientId = "",
// ClientSecret = ""
//});
}
}
}
Startup.cs
using Microsoft.Owin;
using Microsoft.Owin.Security.OAuth;
using Microsoft.Practices.Unity;
using Owin;
using System;
using System.Web.Http;
using Tdf.Application.Act.UserMgr;
using Tdf.Utils.Config;
using Tdf.WebApi.Providers;
using Unity.WebApi;
[assembly: OwinStartup(typeof(Tdf.WebApi.Startup))]
namespace Tdf.WebApi
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
// 有關如何配置應用程序的詳細信息,請訪問 http://go.microsoft.com/fwlink/?LinkID=316888
var config = new HttpConfiguration();
WebApiConfig.Register(config);
IUnityContainer container = UnityConfig.GetConfiguredContainer();
config.DependencyResolver = new UnityDependencyResolver(container);
ConfigureOAuth(app, container);
// 這一行代碼必須放在ConfiureOAuth(app)之后
// Microsoft.AspNet.WebApi.Owin
app.UseWebApi(config);
}
public void ConfigureOAuth(IAppBuilder app, IUnityContainer container)
{
IUserAppService userService = container.Resolve<IUserAppService>();
OAuthAuthorizationServerOptions oAuthServerOptions = new OAuthAuthorizationServerOptions()
{
#if DEBUG
AllowInsecureHttp = true,
#endif
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(double.Parse(ConfigHelper.GetValue("TokenExpireMinute", "120"))),
RefreshTokenProvider = new TdfRefreshTokenProvider(userService),
Provider = new TdfAuthorizationServerProvider(userService)
};
// Token Generation
app.UseOAuthAuthorizationServer(oAuthServerOptions);
var opts = new OAuthBearerAuthenticationOptions()
{
Provider = new TdfOAuthBearerProvider("Token")
};
app.UseOAuthBearerAuthentication(opts);
}
}
}
TdfAuthorizationServerProvider.cs
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OAuth;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Tdf.Application.Act.UserMgr;
using Tdf.Application.Act.UserMgr.Dtos;
using Tdf.Domain.Act.Entities;
namespace Tdf.WebApi.Providers
{
/// <summary>
/// Resource Owner Password Credentials Grant 授權
/// </summary>
public class TdfAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
/// <summary>
/// Password Grant 授權服務
/// </summary>
readonly IUserAppService _userService;
/// <summary>
/// 構造函數(shù)
/// </summary>
/// <param name="userService">Password Grant 授權服務</param>
public TdfAuthorizationServerProvider(IUserAppService userService)
{
this._userService = userService;
}
/// <summary>
/// Resource Owner Password Credentials Grant 的授權方式倒槐;
/// 驗證用戶名與密碼 [Resource Owner Password Credentials Grant[username與password]|grant_type=password&username=irving&password=654321]
/// 重載 OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials() 方法即可
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var user = await _userService.Login(new LoginInput() { UserName = context.UserName, Password = context.Password });
var userInfo = user.Result as User;
if (user.ErrCode != 0 || userInfo == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
else
{
// 驗證context.UserName與context.Password
// 調(diào)用后臺的登錄服務驗證用戶名與密碼
var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, userInfo.Id.ToString()));
oAuthIdentity.AddClaim(new Claim("UserId", userInfo.Id.ToString()));
oAuthIdentity.AddClaim(new Claim("UserName", userInfo.UserName));
var props = new AuthenticationProperties(new Dictionary<string, string> {
{ "user_id", userInfo.Id.ToString() },
{ "user_name", userInfo.UserName }
});
var ticket = new AuthenticationTicket(oAuthIdentity, props);
context.Validated(ticket);
await base.GrantResourceOwnerCredentials(context);
}
}
/// <summary>
/// 把Context中的屬性加入到token中
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
/// <summary>
/// 驗證客戶端 [Authorization Basic Base64(clientId:clientSecret)|Authorization: Basic 5zsd8ewF0MqapsWmDwFmQmeF0Mf2gJkW]
/// 對third party application 認證旬痹,
/// 為third party application頒發(fā)appKey和appSecrect,在此省略了頒發(fā)appKey和appSecrect的環(huán)節(jié),
/// 認為所有的third party application都是合法的
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// 表示所有允許此third party application請求
context.Validated();
return Task.FromResult<object>(null);
}
}
}
TdfRefreshTokenProvider.cs
using Microsoft.Owin.Security.Infrastructure;
using System;
using System.Threading.Tasks;
using Tdf.Application.Act.UserMgr;
using Tdf.Domain.Act.Entities;
using Tdf.Utils.Config;
using Tdf.Utils.GuidHelper;
namespace Tdf.WebApi.Providers
{
/// <summary>
/// 刷新Token
/// 生成與驗證Token
/// </summary>
public class TdfRefreshTokenProvider : AuthenticationTokenProvider
{
/// <summary>
/// 授權服務
/// </summary>
private IUserAppService _userService;
/// <summary>
/// 構造函數(shù)
/// </summary>
/// <param name="userService">授權服務</param>
public TdfRefreshTokenProvider(IUserAppService userService)
{
this._userService = userService;
}
/// <summary>
/// 創(chuàng)建refreshToken
/// </summary>
/// <param name="context">上下文</param>
/// <returns></returns>
public override async Task CreateAsync(AuthenticationTokenCreateContext context)
{
if (string.IsNullOrEmpty(context.Ticket.Identity.Name)) return;
var refreshTokenLifeTime = ConfigHelper.GetValue("TokenExpireMinute", "120");
if (string.IsNullOrEmpty(refreshTokenLifeTime)) return;
// generate access token
var refreshTokenId = new RegularGuidGenerator().Create();
context.Ticket.Properties.IssuedUtc = DateTime.UtcNow;
context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddMinutes(double.Parse(refreshTokenLifeTime));
var refreshToken = new RefreshToken()
{
Id = refreshTokenId,
UserId = new Guid(context.Ticket.Identity.Name),
IssuedUtc = DateTime.Parse(context.Ticket.Properties.IssuedUtc.ToString()),
ExpiresUtc = DateTime.Parse(context.Ticket.Properties.ExpiresUtc.ToString()),
ProtectedTicket = context.SerializeTicket()
};
// Token沒有過期的情況強行刷新两残,刪除老的Token保存新的Token
var jsonMsg = await _userService.SaveTokenAsync(refreshToken);
if (jsonMsg.ErrCode == 0)
{
context.SetToken(refreshTokenId.ToString());
}
}
/// <summary>
/// 刷新refreshToken[刷新access token時永毅,refresh token也會重新生成]
/// </summary>
/// <param name="context">上下文</param>
/// <returns></returns>
public override async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
var jsonMsg = await _userService.GetToken(context.Token);
if (jsonMsg.ErrCode == 0)
{
var refreshToken = jsonMsg.Result as RefreshToken;
if (refreshToken != null)
{
context.DeserializeTicket(refreshToken.ProtectedTicket);
await _userService.RemoveToken(context.Token);
}
}
}
}
}
TdfOAuthBearerProvider.cs
using Microsoft.Owin.Security.OAuth;
using System.Threading.Tasks;
namespace Tdf.WebApi.Providers
{
public class TdfOAuthBearerProvider : OAuthBearerAuthenticationProvider
{
readonly string _name;
public TdfOAuthBearerProvider(string name)
{
_name = name;
}
public override Task RequestToken(OAuthRequestTokenContext context)
{
var value = context.Request.Query.Get(_name);
if (!string.IsNullOrEmpty(value))
{
context.Token = value;
}
return Task.FromResult<object>(null);
}
}
}
結合 ASP.NET 現(xiàn)有的安全機制,借助 OWIN 的威力人弓,Microsoft.Owin.Security.OAuth 的確讓開發(fā)基于 OAuth 的 Web API 變得更簡單沼死。