如何基於Microsoft.Owin.Security.OAuth,使用Client Credentials
Grant授權方式給客戶端發放access token? Client Credentials
Grant的授權方式就是只驗證客戶端(Client),不驗證用戶(Resource Owner),只要客戶端通過驗證就發access
token。舉一個對應的應用場景例子,比如我們想提供一個“獲取網站首頁最新博文列表”的WebAPI給客戶端App調用。由於這個數據與用戶無關,所以不涉及用戶登錄與授權,不需要Resource
Owner的參與。但我們不想任何人都可以調用這個WebAPI,所以要對客戶端進行驗證,而使用OAuth中的 Client
Credentials Grant 授權方式可以很好地解決這個問題。
在App_Start文件夾下新增ApplicationDbInitializer,代碼如下:
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext>
{
protected override void Seed(ApplicationDbContext context)
{
InitializeIdentityForEF(context);
base.Seed(context);
}
//創建用戶名為admin@123.com,密碼為“Admin@123456”
public static void InitializeIdentityForEF(ApplicationDbContext dbContext)
{
var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
const string name = "admin@123.com";//用戶名
const string email = "admin@123.com";//郵箱
const string password = "Admin@123456";//密碼
//如果沒有admin@123.com用戶則創建該用戶
var user = userManager.FindByName(name);
if (user == null)
{
user = new ApplicationUser
{
UserName = name,
Email = email
};
var result = userManager.Create(user, password);
result = userManager.SetLockoutEnabled(user.Id, false);
}
}
}
修改Model文件夾下的IdentityModels.cs,添加斜體部分代碼,需添加命名空間:using System.Data.Entity;
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
// 在第一次啟動網站時初始化數據庫添加管理員用戶憑據到數據庫
Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
}
我把WebApi的Controller放到一個新建的文件夾APIControllers中,TestController的View的js的測試代碼
打開Startup.Auth.cs,以下代碼是Oauth相關的配置代碼
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
var OAuthOptions = new OAuthAuthorizationServerOptions
{
//獲取Token的路徑
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
//Token 過期時間,默認20分鍾
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
//在生產模式下設 AllowInsecureHttp = false
AllowInsecureHttp = true
};
app.UseOAuthBearerTokens(OAuthOptions);
}
}
使用Client Credentials Grant的授權方式( grant_type= client_credentials)獲取 Access Token,並以這個 Token 調用與用戶相關的 Web API。
我們需要修改部分代碼,修改ValidateClientAuthentication()方法,繼承實現GrantClientCredentials()方法。代碼如下
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId, clientSecret;
context.TryGetBasicCredentials(out clientId, out clientSecret);
if (clientId == "Mobile" && clientSecret == "Xiaomi")
{
context.Validated();
}
return Task.FromResult<object>(null);
}
public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
{
var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, "Xiaomi"));
var ticket = new AuthenticationTicket(oAuthIdentity, new AuthenticationProperties());
context.Validated(ticket);
return base.GrantClientCredentials(context);
}
}
在 ValidateClientAuthentication() 方法中獲取客戶端的 client_id 與 client_secret 進行驗證。在 GrantClientCredentials() 方法中對客戶端進行授權,授了權就能發 access token 。這樣,OAuth的ClientCredentials授權服務端代碼就完成了。在ASP.NET Web API中啟用OAuth的Access Token驗證非常簡單,只需在相應的Controller或Action加上[Authorize]標記,VS已生成部分代碼,詳細查看APIController文件夾下的ValuesController
下面我們在客戶端調用一下,添加TestController,生成Index的View,然后在View中添加如下
$(function () {
$("#clientCredentials").on("click", function () {
GetClientCredentialsAccessToken();
});
});
function GetClientCredentialsAccessToken() {
$("#clientResult").html("Requesting");
var clientId = "Mobile";
var clientSecret = "Xiaomi";
$.ajax({
url: "/Token",
type: "post",
data: { "grant_type": "client_credentials" },
dataType: "json",
headers: {
"Authorization": "Basic " + Base64_Encode(clientId + ":" + clientSecret)
},
success: function (data) {
var accessToken = data.access_token;
GetValues(accessToken);
}
});
}
function GetValues(accessToken) {
var html = "Token:" + accessToken + "<br/><br/>";
$.ajax({
url: "/api/Values",
type: "get",
dataType: "json",
headers: {
"Authorization": "Bearer " + accessToken
},
success: function (values) {
for (var i = 0; i < values.length; i++) {
html += "values[" + i + "] :" + values[i] + "<br/>";
}
$("#clientResult").html(html);
}
});
}
function Base64_Encode(str) {
var c1, c2, c3;
var base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var i = 0, len = str.length, string = '';
while (i < len) {
c1 = str.charCodeAt(i++) & 0xff;
if (i === len) {
string += base64EncodeChars.charAt(c1 >> 2);
string += base64EncodeChars.charAt((c1 & 0x3) << 4);
string += "==";
break;
}
c2 = str.charCodeAt(i++);
if (i === len) {
string += base64EncodeChars.charAt(c1 >> 2);
string += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
string += base64EncodeChars.charAt((c2 & 0xF) << 2);
string += "=";
break;
}
c3 = str.charCodeAt(i++);
string += base64EncodeChars.charAt(c1 >> 2);
string += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
string += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
string += base64EncodeChars.charAt(c3 & 0x3F);
}
return string;
}
測試結果:
Token:4iIu7HProfJaxRiklsl-ORRO3hdyrsu50pQc1Eh2-Q5lSWK8UJgz6719ZaeeULhwkMPpEFYfk6QDOOMEyFqULULk65Sb0JY29wskyZyQhKJ3_P-eSVQ2PlbKbjH9ZcziAZsVOiNLp8CfUqL5qWUq8ggVAa8KRcnlJ1DIVWnEu0XvTEDZaLDpFqqj2Cex2CX7TmTgfs07RUBdx5_3WDavNA
Ps:
傳遞clientId與clientSecret有兩種方式,上例使用BasicAuthentication,服務端使用TryGetBasicCredentials();另外一種方式是普通From的,把參數放到Ajax的data中,如:
{“clientId”: id ,” clientSecret”:”secret”, "grant_type":"client_credentials"}
對應服務端使用TryGetFormCredentials()獲取clientId和clientSecret;
推薦使用Basic Authentication方式;
使用Resource Owner Password Credentials Grant 的授權方式( grant_type=password )獲取 Access Token,並以這個 Token 調用與用戶相關的 Web API。
Resource Owner Password Credentials Grant 授權方式(需要驗證登錄用戶)
因為我們剛開始時已經初始化EF,添加了一個用戶信息。ApplicationOAuthProvider.cs 的GrantResourceOwnerCredentials()方法(VS幫我們自動生成了),已經實現了先關的代碼
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
//調用后台的登錄服務驗證用戶名與密碼
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "用戶名或密碼不正確。");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager, OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager, CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
添加一個測試用的Controller
public class UsersController : ApiController
{
[Authorize]
public string GetCurrent()
{
return User.Identity.Name;
//這里可以調用后台用戶服務,獲取用戶相關數所,或者驗證用戶權限進行相應的操作
}
}
在Test的index.cshtml 中新增測試的代碼,如下
function GetResourceOwnerCredentialsAccessToken() {
$("#resourceOwnerresult").html("Requesting");
var clientId = "Mobile";
var clientSecret = "Xiaomi";
$.ajax({
url: "/Token",
type: "post",
data: {
"grant_type": "password",
"username": "admin@123.com",
"password": "Admin@123456"
},
dataType: "json",
headers: {
"Authorization": "Basic " + Base64_Encode(clientId + ":" + clientSecret)
},
success: function (data) {
var accessToken = data.access_token;
GetCurrentUserName(accessToken);
}
});
}
function GetCurrentUserName(accessToken) {
var html = "Token:" + accessToken + "<br/><br/>";
$.ajax({
url: "/api/User",
type: "get",
dataType: "text",
headers: {
"Authorization": "Bearer " + accessToken
},
success: function (userName) {
html += "CurrentUserName:" + userName + "<br/>";
$("#resourceOwnerresult").html(html);
}
});
}
測試結果如下
Token:Cvct6BAKix_xLNEEOfidpEG0ymJihOSjdACazP2R2tJSB3TKVnxicgQK27DzDrICUC4A7vITqhkhBRsT5cRgiow--VkbiR4we3yQ54tc6B_W8KRrdGabjase_gpmFv8oYUPGLpI82acDpcZPzCkmgLLwAq8qfkmlK7iHm5tLM6-NRR8tgfEeOVBljHq4smIXw_eVuces3sRQm-PXTD4xmp05JdrJ9zFeRb_SAN0ADqDJfJxk1nNooCtdJyeHB6r1S2D81H6P7bhRK_edneWdkX5QCNBHL8b39UKnnk0ywza6vXcWct4RaATBYOw20iNu0XR6JRx5opP9vqqC2ag8Ux6s3GHl-vAZTaYuwunmWyY0FyJJWpjNnFpPo-pkxZaK1XJxgGPpSV-JJjEZLarnq9O57hQGfbVLCd3KtWuJflo5rMnfkAz2nXlcd3gAgjIhipAIlpsG72StzN0qBL8Ml2XvV9Re1Z8U4QtrE7tzjkE
CurrentUserName:"admin@123.com"
至此,使用WebApi 的兩種授權方式發放Token和兩種授權方式區別,以及使用Token調用受保護的api已經介紹完了,Oauth其實還有一個refresh token,refresh token 是專用於刷新 access token 的 token。一是因為 access token 是有過期時間的,到了過期時間這個 access token 就失效,需要刷新;二是因為一個 access token 會關聯一定的用戶權限,如果用戶授權更改了,這個 access token 需要被刷新以關聯新的權限。
這個就不單獨介紹了,有興趣的可以自己研究下。
Asp.Net 高級技術群 89336052,群共享有源碼
感謝:晨風的指導和教育,提供demo和文章,我只是一個發布文章的戰五渣!