2014年2月14日 星期五

AspNet Identity 玩弄手札 第二彈 Social Login Facebook 登入

One ASP.NET 表示:天下大勢,分久必合,合久必分

AspNet Identity 表示:我也不過是被 One ASP.NET 玩弄於股掌之間的一個犧牲者

老疆表示:程式設計師用一些想到的點子來做一些無聊事情,這樣瞭不瞭

第一次聽到 AspNet Identity 是 老疆 的影片

http://www.youtube.com/watch?v=G2JC4KWTqXM&list=PLSvKFCIcFiosRfhmjcSCozircig3dRRrR&index=1

(11:15開始)

老疆的演講都很有梗

而且梗都是模仿不來的

別人說都不好笑 但是聽他講卻又有種獨特的幽默

我想這是一種 Aura









這次來玩 Social Login

為啥要 Social Login

也沒啥為什麼 因為流行

客戶說 OO 的網站可以用 XX 登入 我們的網站也要以

但是 social website 滿天下

每個都自成一套 API

而且 document 絕不會有中文

所以程式設計師總是永無止境的在熟悉 API 的用法

等到好像有點懂的時候 可能 API 又改版了 ._.




Micro$oft 發現了這個現象

所以就把它包裝起來 放到 AspNet Identity 裡面

好讓它更顯強大




首先是出場率最高的 Facebook 登入

在以往要做到這項功能

可能要先征服這頁

https://developers.facebook.com/docs/facebook-login/login-flow-for-web/



有個名詞兒叫 OAuth

OAuth(開放授權)是一個開放標準,允許用戶讓第三方應用訪問該用戶在某一網站上存儲的私密的資源(如照片,視頻,聯繫人列表),而無需將用戶名和密碼提供給第三方應用。

http://zh.wikipedia.org/wiki/OAuth




Facebook 登入 就是一種 OAuth

那不重要 因為大多數的人其實都不 Care 名詞解釋這回事

馬上來玩玩 就對了

但是玩之前必須要先申請 Facebook 應用程式

取得應用程式 ID (appId) 跟 應用程式密鑰 (appSecret)



申請過程沒啥好講的

但是如果我們在古早就 自行挑戰過 Facebook 登入的話...

WTF 介面改了 長得不一樣了

要先到 設定 這頁 點選 +新增平台



然後選網站



網站 URL 填好之後 切記! 要按 儲存



最近發現一個坑 那就是 只要網站沒有設定網域 (例如 localhost 或者 IP 的形式)

Facebook 只會讓應用程式擁有者 的帳號登入

用其他的帳號 一律看到這個 不明所以的錯誤



如果用 手工 javascript 的 FB.login() 的話更慘 會直接空白卡死



2014/02/26 補充:

後來發現原來是沙盒在作亂

http://stackoverflow.com/questions/20706322/how-to-disable-sandbox-mode-for-app-in-new-facebook-developer

在這頁可以切換...




MotherF___er!! 不能按是哪招??


後來經高人指點 才知道原來要填 E-mail 之後才能按 (X 鬼知道唷) 見圖2




再來 回到 VS 2013 建立一個 MVC 專案

他預設很好心的就準備好一堆範例程式 讓我們熟悉 他新攪和進來的東東

然後打開 \App_Start\Startup.Auth.cs

我們會發現只要取消註解 填上 appId 跟 appSecret 就結束了



此時登入頁面 就會多了 Facebook 登入的按鈕



如果點了 就GG了 那是因為我們使用 網址使用 localhost 然後又用了 該死的 IE



否則 點了會跳這個 不管問什麼都一定會按確定 的頁面



這時會回來網站問你...唉唷欸欸耶耶名字可以自己取耶呵呵呵呵呵呵



如果覺得有下底線的 username 很潮 我們會毫無意外的 獲得紅字



因為預設只接受英數字組合 中文也不行

修改 AccountController 的建構子 設定 AllowOnlyAlphanumericUserNames = false 就能解決

但是此時就沒任何規則 要加驗證要另外搞

public AccountController(UserManager<ApplicationUser> userManager)
{
    UserManager = userManager;
 
    UserManager.UserValidator = new UserValidator<ApplicationUser>(UserManager)
    {
        AllowOnlyAlphanumericUserNames = false
    };
}

然後就能快樂的登入了 可喜可賀



什麼? 這麼快? 快得令人覺得有些空虛 -.-

因為大部分都被黑箱了呀

好奇怎麼實作的話 就 reflector 催落起吧

AspNet Identity 玩弄手札 第一彈 Code First 自訂 profile data

One ASP.NET 表示:天下大勢,分久必合,合久必分

AspNet Identity 表示:我也不過是被 One ASP.NET 玩弄於股掌之間的一個犧牲者

老疆表示:程式設計師用一些點子來做一些無聊事情,這樣瞭不瞭

第一次聽到 AspNet Identity 是 老疆 的影片

http://www.youtube.com/watch?v=G2JC4KWTqXM&list=PLSvKFCIcFiosRfhmjcSCozircig3dRRrR&index=1

(11:15開始)

老疆的演講都很有梗

而且梗都是模仿不來的

別人說都不好笑 但是聽他講卻又有種獨特的幽默

我想這是一種 Aura









首先來玩 Micro$oft 最愛表演的戲碼 Code First + 自訂 profile data 神奇搞定會員登入功能

打開 VS 2013 建立一個 MVC 專案

他預設很好心的就準備好一堆範例程式 讓我們熟悉 他新攪和進來的東東

其實馬上就能執行了 可以註冊 登入 都沒問題



因為是 Code First 所以第一次呼叫 DbContext 的時候 會自動建立好 DB 跟 TABLE



以往要擴充 profile data 十分麻煩 有多麻煩我也不太瞭

因為相傳 業界公司 沒有人在用 都是自幹 會員機制 而我也不過是其中一人

現在他表明了 You can add profile data for the user by adding more properties to your ApplicationUser class

在這邊加屬性就搞定了

// You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
public class ApplicationUser : IdentityUser
{
    public string Email { getset; }
 
    public int Point { getset; }
 
    public bool Gender { getset; }
 
    public DateTime Birthday { getset; }
}

原先的 RegisterViewModel 也順手加上 以利欄位驗證

public class RegisterViewModel
{
    [Required]
    [Display(Name = "使用者名稱")]
    public string UserName { getset; }
 
    [Required]
    [StringLength(100,
        ErrorMessage = "{0} 的長度至少必須為 {2} 個字元。",
        MinimumLength = 6
    )]
    [DataType(DataType.Password)]
    [Display(Name = "密碼")]
    public string Password { getset; }
 
    [DataType(DataType.Password)]
    [Display(Name = "確認密碼")]
    [Compare("Password"ErrorMessage = "密碼和確認密碼不相符。")]
    public string ConfirmPassword { getset; }
 
    [Required]
    [EmailAddress]
    [Display(Name = "E - mail")]
    public string Email { getset; }
 
    [Required]
    [Display(Name = "點數")]
    public int Point { getset; }
 
    [Required]
    [Display(Name = "性別")]
    public bool Gender { getset; }
 
    [DataType(DataType.Date)]
    [Display(Name = "生日")]
    public DateTime Birthday { getset; }
}

AccountController 的 Register 也要從 ViewModel 把值帶入 ApplicationUser

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser()
        {
            UserName = model.UserName,
            Email = model.Email,
            Point = model.Point,
            Gender = model.Gender,
            Birthday = model.Birthday
        };
 
        var result = await UserManager.CreateAsync(usermodel.Password);
        if (result.Succeeded)
        {
            await SignInAsync(userisPersistentfalse);
            return RedirectToAction("Index""Home");
        }
        else
        {
            AddErrors(result);
        }
    }
 
    // 如果執行到這裡,發生某項失敗,則重新顯示表單
    return View(model);
}

如果你覺得欄位很多很給掰 可以使用一些 Mapping 或者 Injecter 的套件

像是 Value Injecte



只要屬性名稱一致 就能無腦的搞定

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser().InjectFrom(modelas ApplicationUser;
 
        var result = await UserManager.CreateAsync(usermodel.Password);
        if (result.Succeeded)
        {
            await SignInAsync(userisPersistentfalse);
            return RedirectToAction("Index""Home");
        }
        else
        {
            AddErrors(result);
        }
    }
 
    // 如果執行到這裡,發生某項失敗,則重新顯示表單
    return View(model);
}

最後當然別忘了 View 可不會自動生出 HTML 也補上

<div class="form-group">
    @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
    <div class="col-md-10">
        @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
    </div>
</div>
<div class="form-group">
    @Html.LabelFor(m => m.Point, new { @class = "col-md-2 control-label" })
    <div class="col-md-10">
        @Html.TextBoxFor(m => m.Point, new { @class = "form-control" })
    </div>
</div>
<div class="form-group">
    @Html.LabelFor(m => m.Gender, new { @class = "col-md-2 control-label" })
    <div class="col-md-10">
        男
        @Html.RadioButtonFor(
            m => m.Gender, true, new { @id = "", @checked = "checked" })
        女
        @Html.RadioButtonFor(
            m => m.Gender, false, new { @id = "" })
    </div>
</div>
<div class="form-group">
    @Html.LabelFor(m => m.Birthday, new { @class = "col-md-2 control-label" })
    <div class="col-md-10">
        @Html.TextBoxFor(m => m.Birthday, new { @class = "form-control" })
    </div>
</div>



搞定 馬上開心執行 立馬報錯 -.-



因為先前執行過一次已經 產生過 DB 跟 TABLE

但是這次 Code First 發現 schema 被異動過 無法 ORM 就跳錯了

請透過 套件管理主控台 來更新他

我們可以在 檢視/其他視窗/套件管理主控台 找到這貨

啟用轉移:Enable-Migrations -ContextTypeName MvcSample.ApplicationDbContext

轉移版本:Add-Migration '版本名稱'

更新資料庫:Update-Database

出現 Seed 就表示成功啦



這下搞定了 資料乖乖進 DB 可喜可賀



2014年2月7日 星期五

關於 LINQ to Entities 的時候 MSSQL 相等比較 預設不區分大小寫的問題

其實這個故事並不是發生在使用 LINQ to Entities 的時候

而是在 SSMS 敲 SQL 的時候

當時在 try 舊 DB 資料轉移新 DB 的整合的方法

說時遲那時快

這鬼才 DB 的 account 欄位的資料 竟然有在重複的 但是 key 不同

雖然登入是靠 SSO 的方式 但是 account 相同應該還是無法辨識才對

除了那些真正相同的 account 之外 忽然發現 大小寫不同 但是字串相同 也會被判斷成相同

估狗了一下才發現 原來 MSSQL 預設是沒在管大小寫的

跟啥 定序 之類的鬼東西有關

解法自然也有

1.直接改那個 定序 的設定

2.加上關鍵字 COLLATE Chinese_Taiwan_Stroke_CS_AS

參考







這種 MSSQL 才有的專屬天使

LINQ to Entities 應該絕對是會無視它的

立馬玩一下 果真如此

走訪 估狗 跟 stackoverflow 之後 解法約有二

1. .ToList() 之後再玩一次 因為這時候後就是 C# 物件 就認得大小寫

但是資料多的話肯定是 GG

2. 用之前的 T - SQL 方法 有很多鬼法子可以讓 LINQ to Entities 也吃 T - SQL

但是就跟 ORM 精神說掰掰



目前的選擇是 2 因為 1 讓人覺得實在多此一舉

但是要搞這種家醜不得外揚的東東

通常都會包成一個方法

有朝一日科技進步 或者 牛人出世 之際 再來改做法




雖然理想上我想要包成這種感覺

var q = db.user.UpperOrLowerCaseWhere(x => x.account == "amy");

讓人有種還是 LINQ 的錯覺

但是實際上有許多難點

1. 可能要反射來反射去 我才有辦法抓到 "account" 這個欄位名稱

2. 浪打返回布林 那我也無法抓到 "amy" -.-

3. 最後還是靠 T - SQL 作亂 如果在這行後面在加其他 LINQ 方法 肯定也是 GG





最後選擇擴充 IQueryable<T> 用法如下

var q = db.user.AsQueryable().UpperOrLowerCaseEquals(db"account""amy");

Where() 會轉成 IQueryable<T> 所以不必加 AsQueryable()

var q = db.user
    .Where(x => x.email.Length > 0)
    .UpperOrLowerCaseEquals(db"account""amy");

Extension 大概長這樣

    public static class IQueryableExtension
    {
        public static IEnumerable<TEntity> UpperOrLowerCaseEquals<TEntity>(
            this IQueryable<TEntity> query,
            DbContext db,
            string columnName,
            string keyword
        ) where TEntity : class
        {
            string sql = query.ToString();
            
            IObjectContextAdapter adapter = db as IObjectContextAdapter;
 
            ObjectContext objectContext = adapter.ObjectContext;
 
            var q = objectContext.ExecuteStoreQuery<TEntity>(
                string.Format(@"
                    SELECT * FROM ( {0} ) AS [table] 
                    WHERE [{1}] COLLATE Chinese_Taiwan_Stroke_CS_AS = @keyword ",
                    sql,
                    columnName
                ),
                new SqlParameter("@keyword"keyword)
            );
 
            return q.ToList();
        }
    }




命名為 UpperOrLowerCaseEquals 而非 UpperOrLowerCaseWhere

則是限制只用在相等比較

WHERE 要搞定的東東或許太複雜 ˊ_>ˋ



最後...牛人快出世吧