OAuth 2 從入門到精通(一) - 身份認證服務(wù)器

前言

翻譯類文章,原文來自 Token Based Authentication using ASP.NET Web API 2, Owin, and Identity,沒有逐字逐句翻譯,大概意思表達了。

創(chuàng)建認證服務(wù)

  1. 新建WebAPI項目
    Framework 4.6,新建ASP.NET應(yīng)用程序,選擇“Empty”吁津,“Web API”,“No Authentication”堕扶。
  2. 引入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
  1. 添加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();
        }
    }
  1. 刪除“Global.ascx”文件污抬。
  2. 添加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ù)庫連接 沮趣。

  1. 創(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();
    }
}
  1. 添加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)如下情況:
請求無效

  1. 添加一個受保護的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ā)送的請求中并未包含任何驗證信息。

  1. 添加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” 進行用戶驗證窿春。
  1. 實現(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。

  1. 在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);
   }
  1. 測試后端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藏雏。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市肌稻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌匕荸,老刑警劉巖爹谭,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異榛搔,居然都是意外死亡诺凡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門践惑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來腹泌,“玉大人,你說我怎么就攤上這事尔觉×垢ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵穷娱,是天一觀的道長绑蔫。 經(jīng)常有香客問我,道長泵额,這世上最難降的妖魔是什么配深? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮嫁盲,結(jié)果婚禮上篓叶,老公的妹妹穿的比我還像新娘烈掠。我一直安慰自己,他們只是感情好缸托,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布左敌。 她就那樣靜靜地躺著,像睡著了一般俐镐。 火紅的嫁衣襯著肌膚如雪矫限。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天佩抹,我揣著相機與錄音叼风,去河邊找鬼。 笑死棍苹,一個胖子當(dāng)著我的面吹牛无宿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播枢里,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼孽鸡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了栏豺?” 一聲冷哼從身側(cè)響起彬碱,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎冰悠,沒想到半個月后堡妒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡溉卓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年皮迟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片桑寨。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡伏尼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出尉尾,到底是詐尸還是另有隱情爆阶,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布沙咏,位于F島的核電站辨图,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏肢藐。R本人自食惡果不足惜故河,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吆豹。 院中可真熱鬧鱼的,春花似錦理盆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至宙橱,卻和暖如春姨俩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背师郑。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工哼勇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人呕乎。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像陨晶,于是被迫代替她去往敵國和親猬仁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理先誉,服務(wù)發(fā)現(xiàn)湿刽,斷路器,智...
    卡卡羅2017閱讀 134,654評論 18 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法褐耳,類相關(guān)的語法诈闺,內(nèi)部類的語法,繼承相關(guān)的語法铃芦,異常的語法雅镊,線程的語...
    子非魚_t_閱讀 31,625評論 18 399
  • 1、不安全的隨機數(shù)生成刃滓,在CSRF TOKEN生成仁烹、password reset token生成等,會造成toke...
    nightmare丿閱讀 3,694評論 0 1
  • 一. Java基礎(chǔ)部分.................................................
    wy_sure閱讀 3,811評論 0 11
  • 睡眠是維持人類生命不可或缺的一部分,但現(xiàn)代人的睡眠質(zhì)量堪憂砰诵。以前人們改善睡眠基本靠藥物治療征唬,但隨著科技的日新月異,...
    琴音花語閱讀 225評論 0 2