前言
翻譯類文章,原文來自 Token Based Authentication using ASP.NET Web API 2, Owin, and Identity,沒有逐字逐句翻譯,大概意思表達了。
創(chuàng)建認證服務(wù)
- 新建WebAPI項目
Framework 4.6,新建ASP.NET應(yīng)用程序,選擇“Empty”吁津,“Web API”,“No Authentication”堕扶。 - 引入Nuget包管理
使用Nuget包管理器安裝用于Owin服務(wù)器的類庫碍脏,打開Nuget Package Manger控制臺梭依,使用如下命令安裝:
Install-Package Microsoft.AspNet.WebApi.Owin -Version 5.1.2
Install-Package Microsoft.Owin.Host.SystemWeb -Version 2.1.0
- 添加OWIN啟動類
using Microsoft.Owin;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
[assembly: OwinStartup(typeof(AuthenServer.Startup))]
namespace AuthenServer
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
WebApiConfig.Register(config);
app.UseWebApi(config);
}
}
}
Startup類將會在Owin服務(wù)啟動的時候被調(diào)用,請注意“assembly”特性標(biāo)注了將會被調(diào)用的類典尾。
方法中的代碼役拴,在基本的WebAPI腳手架中,位于Global,ascx文件中急黎,用于注冊API路由及相關(guān)運行參數(shù)扎狱。
在步驟一中,通常VS已經(jīng)生成了一個“WebApiConfig”類勃教,如果還沒有淤击,我們可以現(xiàn)在添加一個,默認將此類放在"App_Start"文件夾中故源。代碼如下:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();
jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
}
}
- 刪除“Global.ascx”文件污抬。
- 添加ASP.NET Identity身份識別系統(tǒng)
為簡(FU)單(ZA)起見,我們直接使用微軟的ASP.NET Identity身份識別系統(tǒng)绳军,先引入Identity組件包:
Install-Package Microsoft.AspNet.Identity.Owin -Version 2.0.1
Install-Package Microsoft.AspNet.Identity.EntityFramework -Version 2.0.1
添加一個用于連接SQL Server數(shù)據(jù)庫的上下文AuthenContext類印机,以及一個UserViewModel實體類。
public class AuthenContext: IdentityDbContext<IdentityUser>
{
public AuthenContext(): base("AuthenContext")
{
}
}
-------------------------------------------------------------------------
public class UserViewModel
{
[Required]
[Display(Name = "用戶名")]
public string UserName { get; set; }
[Required]
[StringLength(100, ErrorMessage = "{0} 要求最少 {2} 位字符長度门驾。", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "密碼")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "確認密碼")]
[Compare("Password", ErrorMessage = "兩次輸入的密碼不一致射赛。")]
public string ConfirmPassword { get; set; }
}
在Web.config中添加數(shù)據(jù)庫連接字符串配置項
<connectionStrings>
<add name="AuthenContext" connectionString="Data Source=(localDB)\v11.0;Initial Catalog=OAuth;Integrated Security=SSPI;" providerName="System.Data.SqlClient" />
</connectionStrings>
需要注意的是,name值要和上面AuthenContext類的構(gòu)造函數(shù)中使用的參數(shù)值一致奶是,都是“AuthenContext”楣责,數(shù)據(jù)庫我們使用LocalDB,數(shù)據(jù)庫名OAuth聂沙,數(shù)據(jù)庫文件放在哪兒沒有說明秆麸,系統(tǒng)會自動存放在Windows當(dāng)前登錄用戶的用戶文件夾中。有關(guān)連接字符串的詳細說明及汉,可以查閱 EF 6.0 入門系列 - 數(shù)據(jù)庫連接 沮趣。
- 創(chuàng)建用戶身份識別倉儲類
第一步我們實現(xiàn)“RegisterUser”及“FindUser”兩個方法。ASP.NET Identity System將會幫助我們對密碼進行HASH編碼等一系列復(fù)雜工作坷随,我們只需要簡單的將前端傳入的用戶密碼等參數(shù)提供給UserManager類房铭。
public class AuthenRepository : IDisposable
{
private AuthenContext _context;
private UserManager<IdentityUser> _userManager;
public AuthenRepository()
{
_context= new AuthenContext();
_userManager= new UserManager<IdentityUser>(new UserStore<IdentityUser>(_context));
}
public async Task<IdentityResult> RegisterUser(UserViewModel userModel)
{
IdentityUser user = new IdentityUser
{
UserName = userModel.UserName
};
var result = await _userManager.CreateAsync(user, userModel.Password);
return result;
}
public async Task<IdentityUser> FindUser(string userName, string password)
{
IdentityUser user = await _userManager.FindAsync(userName, password);
return user;
}
public void Dispose()
{
_context.Dispose();
_userManager.Dispose();
}
}
- 添加AccountAPIController
實現(xiàn)一個Register方法,返回成功或者失敗温眉。
[RoutePrefix("api/Account")]
public class AccountAPIController : ApiController
{
private AuthenRepository _repository = null;
public AccountAPIController()
{
_repository = new AuthenRepository();
}
// POST api/Account/Register
[AllowAnonymous]
[Route("Register")]
public async Task<IHttpActionResult> Register(UserViewModel userModel)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await _repository.RegisterUser(userModel);
IHttpActionResult errorResult = GetErrorResult(result);
if (errorResult != null)
{
return errorResult;
}
return Ok();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_repository.Dispose();
}
base.Dispose(disposing);
}
private IHttpActionResult GetErrorResult(IdentityResult result)
{
if (result == null)
{
return InternalServerError();
}
if (!result.Succeeded)
{
if (result.Errors != null)
{
foreach (string error in result.Errors)
{
ModelState.AddModelError("", error);
}
}
if (ModelState.IsValid)
{
// No ModelState errors are available to send, so just return an empty BadRequest.
return BadRequest();
}
return BadRequest(ModelState);
}
return null;
}
}
上面代碼中育叁,實現(xiàn)了一個地址為“api/account/register”的Web API方法,可以通過使用HTTP POST方法發(fā)送如下的JSON對象來實現(xiàn)新用戶注冊芍殖。
{
"userName": "Taiser",
"password": "SuperPass",
"confirmPassword": "SuperPass"
}
運行此Web應(yīng)用程序,并打開POSTMAN谴蔑,發(fā)送一個HTTP POST請求到 “http://localhost:port/api/account/register”豌骏,運氣好的話龟梦,你將會收到HTTP 200狀態(tài)碼,同時窃躲,在Web.config連接字符串指向的數(shù)據(jù)庫將被創(chuàng)建(第一次運行)计贰,同時發(fā)送的JSON對象中的用戶將會被添加到數(shù)據(jù)表“AspNetUsers”中。
如果你沒有修改JSON字串中的用戶名蒂窒,直接再次按下“Send”按鈕躁倒,將會出現(xiàn)如下情況:
- 添加一個受保護的API
項目右鍵添加一個空的API控制器OrdersAPIController,我們將確保此控制權(quán)只會向已認證用戶返回訂單信息洒琢,為簡單起見秧秉,演示代碼直接使用靜態(tài)數(shù)據(jù)。
[RoutePrefix("api/Orders")]
public class OrdersAPIController : ApiController
{
[Authorize]
[Route("")]
public IHttpActionResult Get()
{
return Ok(Order.CreateOrders());
}
}
#region Helpers
public class Order
{
public int OrderID { get; set; }
public string CustomerName { get; set; }
public string ShipperCity { get; set; }
public Boolean IsShipped { get; set; }
public static List<Order> CreateOrders()
{
List<Order> OrderList = new List<Order>
{
new Order {OrderID = 10248, CustomerName = "Taiseer Joudeh", ShipperCity = "Amman", IsShipped = true },
new Order {OrderID = 10249, CustomerName = "Ahmad Hasan", ShipperCity = "Dubai", IsShipped = false},
new Order {OrderID = 10250,CustomerName = "Tamer Yaser", ShipperCity = "Jeddah", IsShipped = false },
new Order {OrderID = 10251,CustomerName = "Lina Majed", ShipperCity = "Abu Dhabi", IsShipped = false},
new Order {OrderID = 10252,CustomerName = "Yasmeen Rami", ShipperCity = "Kuwait", IsShipped = true}
};
return OrderList;
}
}
#endregion
需要注意的是我們在“Get”方法前面添加了“Authorize”特性衰抑,這樣一來象迎,當(dāng)你向“http://localhost:port/api/orders”發(fā)送HTTP GET請求時,將會收到401 未授權(quán)狀態(tài)碼呛踊,因為此時砾淌,你發(fā)送的請求中并未包含任何驗證信息。
- 添加OAuth Berrer Tokens(承載令牌)生成代碼
第一步谭网,引入如下OAuth包
Install-Package Microsoft.Owin.Security.OAuth -Version 2.1.0
第二步汪厨,在Startup類中添加“ConfigureOAuth”以配置API使用OAuth驗證流程。
public class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureOAuth(app);
// 此處為原有代碼愉择,省略......
}
public void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new SimpleAuthorizationServerProvider()
};
// 令牌生成
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
“OAuthAuthorizationServerOptions” 配置
- 令牌生成URL為”http://localhost:port/token”劫乱。
- 令牌的過期時間為24小時,也就是說薄辅,用戶在24小時候要拂,還使用之前的令牌,他的請求將會被拒絕站楚,收到401狀態(tài)碼脱惰。
- 用戶申請令牌的時候,使用 “SimpleAuthorizationServerProvider” 進行用戶驗證窿春。
- 實現(xiàn)SimpleAuthorizationServerProvider”類
public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
using (AuthRepository _repo = new AuthRepository())
{
IdentityUser user = await _repo.FindUser(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
}
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("role", "user"));
context.Validated(identity);
}
}
“SimpleAuthorizationServerProvider” 覆蓋基類 “OAuthAuthorizationServerProvider“ 的兩個方法拉一,分別是 “ValidateClientAuthentication” 和 “GrantResourceOwnerCredentials”。
第一個方法職責(zé)是驗證連接客戶端旧乞, The first method is responsible for validating the “Client”, 在我們的例子中蔚润,我們只有一個客戶端,所以我們總是返回驗證成功尺栖。
第二個方法 “GrantResourceOwnerCredentials” 職責(zé)是驗證用戶名和密碼, 使用前面編寫的 “AuthenRepository” 類方法 “FindUser” 來驗證用戶名和密碼的有效性嫡纠。
如果憑據(jù)有效,我們將創(chuàng)建“ClaimsIdentity”類,并將認證類型傳遞給它(在我們的例子中是“bearer token”)除盏,然后我們將在簽名的令牌中添加兩個聲明 “sub” 及 “role”叉橱。您可以在這里添加其他不同的聲明,但令牌大小會因此增加者蠕。
當(dāng)我們調(diào)用“context.Validated(identity)”時窃祝,令牌將由系統(tǒng)自動生成。
如果需要允許在令牌中間件提供商上使用CORS (跨源資源共享 Cross-Origin Resource Sharing)踱侣,我們需要向Owin上下文添加標(biāo)題“Access-Control-Allow-Origin”粪小,如果您忘記了這一點,那么當(dāng)您嘗試從瀏覽器調(diào)用它時抡句,生成令牌將失敗探膊。 這不允許CORS的令牌中間件提供程序不是ASP.NET Web API,我們將在下一步添加玉转。 這句話貌似是作者的筆誤突想,感覺最開始的 Not that應(yīng)該是Note that。
- 在ASP.NET Web API中配置允許CORS
第一步還是引入Nuget包
Install-Package Microsoft.Owin.Cors -Version 2.1.0
第二步究抓,打開 “Startup” 類猾担,在 “Configuration” 方法中添加一行代碼:
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
ConfigureOAuth(app);
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
- 測試后端API
假設(shè)您在下面的步驟中注冊了密碼為“SuperPass”的用戶名“Taiseer”,我們將使用相同的用戶名來生成令牌刺下,因此绑嘹,為了測試這一點,打開您最喜歡的REST客戶端應(yīng)用程序橘茉,發(fā)出HTTP請求來生成用戶“Taiseer”的令牌工腋。我個人偏好使用PostMan。
令牌申請
在Headers頁面畅卓,配置“content-type” 為“application/x-www-form-urlencoded”擅腰。(配圖中看不見,自己試一下翁潘,我就不再為此占用無謂的互聯(lián)網(wǎng)資源了)
在Body頁面趁冈,配置類型為x-www-form-urlencoded,并添加如同中的三個鍵值"grant_type"拜马、"username"渗勘、"password"。
下方的Raw是按下"Send"按鈕后俩莽,username和password沒有錯誤的話旺坠,令牌服務(wù)器返回的令牌,我們需要手工復(fù)制此令牌字符串扮超,在下一步獲取資源時作為Header提交取刃。
接下來蹋肮,使用申請的令牌來請求訂單數(shù)據(jù),URL為"http://localhost:23170/api/orders"璧疗,HTTP GET請求括尸,并在Header 加入“Authorization”鍵,內(nèi)容是“Bearer” 和剛剛申請到的令牌字符串(中間加一空格)病毡。
注意:在這兒,我們無需再傳遞用戶名和密碼屁柏。如下圖:
資源獲取
一切都正確的話啦膜,收到HTTP 狀態(tài)碼200以及所需的受保護數(shù)據(jù)。如果你修改令牌任意字符淌喻,將會收到HTTP 狀態(tài)碼401(未授權(quán))僧家。
到目前為止,我們建立的后端API已經(jīng)可以從任何一種前端應(yīng)用進行訪問贯莺,不管是Web襟衰,Winform或者是原生手機APP藏雏。