前言
哈嘍~~~ 大家週一好!夏天到了,大家舒服了沒有,熟話說,打敗你的不是天真,是天真熱!?
過去的一週裡,發生了兩件事跟大家分享下:
①、有兩個小夥伴給我提供了 Working Online 的工作,簡單說了說,感覺應該不太適合,至少我不適合,前期雙方試探的成分太多了,我是不喜歡,同時也建議正在找OnLine工作的小夥伴需多多考慮可行性。
②、我決定開始《微講堂》了,具體可以參考右側公告欄,因為有些基礎比較薄弱的小夥伴,單單提供思路還是無法入門,所以提供線上手把手教學吧,這個我是完全無所謂,看你心情吧。
這幾天通過晚上對 IdentityServer4 的學習和研究,發現這個就是一個“大坑”(不是說功能不好,是裡邊有很多很多的內容需要學習),之前看官網, 關於 IdentityServer4 的教程,洋洋灑灑就過去了,感覺還挺簡單,發現要真是落地到專案裡了,自我感覺又有了壓迫感,文末結語中,我簡單的說了幾點問題,大家可以慢慢往下走,不過知識嘛,無外乎就是自己開心學習 和 自己學習掙錢,這兩個心理,加油吧。
當然平時工作之餘,還是要照顧下前後端分離專案的一些東西的,基礎不能丟,主要是三塊地方做了修改,這裡簡單的列一下,就不單獨的寫文章了,希望一直在看第一個專案的小夥伴,有緣可以看到吧,不過,就算是看不到也沒事兒,遇到了自然就知道了:
1、Blog.Vue 首頁的閃屏處理;// 知名博主@張飛洪提出的問題,不知道我是否修改對了;http://core-dotnet.com:8077
2、Blog.Admin 後臺框架調整優化;// ①登入頁樣式改版,②Tabs 導航條優化,③相容手機螢幕等;http://core-dotnet.com:2364
3、Blog.Core 後端專案增加 Wiki 頁;// 為了讓剛接觸框架的小夥伴能快速一覽,特地在 Github 上,建立了 Wiki ,只不過現在才打了個目錄,內容慢慢填,如果還有其他的不足之處,歡迎提建議;https://github.com/anjoy8/Blog.Core/wiki
突然轉話題,上次我們們第一次對專案進行持久化操作《三║ 詳解授權持久化 & 使用者資料遷移》,不知道小夥伴都看了多少,這裡再把幾個重要問題提一下,希望不要忘記了才好:
1、Ids4 一共用到了幾個上下文,分別的用處是什麼? 2、在遷移中,資料庫生成了多少表,各個模組又是幹什麼的? 3、Ids4 的良好擴充套件性,體現在哪裡?豐富性又體現在哪裡? 4、ApplicationUser 類是起到什麼作用的?
如果腦子裡有些東西,那就恭喜了,如果第一次看,或者完全不知道我在說什麼的話,請看上一集,今天會說說我在研究的過程中,遇到的兩個 Flag ?,也就是兩個問題,希望有心的小夥伴,可以幫忙思考下,歡迎找我討論,廢話不多說,開車,馬上講解今天的內容!??
零、今天要實現綠色的部分
(知識結構圖,注意這是我自己的講解結構,和Ids4知識圖解無關)
一、使用者資料處理 —— Identity
我們們在上篇文章中,簡單的將 IdentityServer4 的結構進行持久化處理,並把前後端專案中的使用者資料進行遷移處理,最後修改了登入頁的樣式,基本滿足了登入和登出的操作,作為一個授權服務中心,僅僅只有登入是完全解決不了什麼問題的,至少應該對使用者資料進行常規操作處理,比如 CURD 等基礎操作。
正好,我們使用了 NetCore 自帶的 Identity 機制,可以幫助我們做一部分工作,因為它自己也封裝了一些方法,我們可以根據他們的方法,實當的做些擴充套件,從而達到相應的目的,具體有哪些操作,請往下看:
1、使用者資料展示(有許可權)
既然有資料處理,肯定得有展示出來,當然,這個不是一定的,只是做下處理,如果你擔心會有資料安全問題的話,要麼不顯示資料,要麼只顯示無關痛癢的兩列,甚至可以直接加上許可權,只有超級管理員或者技術人員可以看到就行。我這裡僅僅是加了個登入許可權,只有登入的使用者才能看的到:
// 注入使用者管理 private readonly UserManager<ApplicationUser> _userManager; [HttpGet] [Route("account/users")] [Authorize]//可以自定義規則 public IActionResult Users(string returnUrl = null) { ViewData["ReturnUrl"] = returnUrl; var users = _userManager.Users.Where(d => !d.tdIsDelete).OrderBy(d => d.UserName).ToList();//Identity 已經對內部的一些方法做了封裝,直接使用即可,如果你對 Net 自帶的 Identity 使用過的話,應該很容易上手。 return View(users); }
注意下上邊的紅色標註的地方,下文會說到為啥這裡用到了 isDelete 。
我們簡單的對 User 頁面做了授權處理,必須登入狀態下才能有權訪問,如果是沒有登入,會直接跳轉到登入頁面:
(帶許可權的使用者展示頁)
2、註冊
關於註冊其實我們之前已經說過了,為什麼呢,因為我們在之前匯入使用者資料的時候,就已經用到了這個方法,只不過這裡單拎出來了,但是這裡有一個問題需要我們好好的思考思考,那就是角色的獲取!這裡就是我下邊要說的第一個“Flag”?,為什麼重要呢,不知道現在讀的你是否使用過 IdentityServer4 ,我也這幾天在考慮這個問題,授權中心肯定需要有使用者管理的,那很自然的,就會出現 “ 區分控制 ” 的問題,這裡簡單說下會出現的兩個情況:
1、前臺展示專案:如果我們的vue 專案,是一個前臺網站,比如 電商類 的或者 Blog.Vue 這樣的,很簡單,我們只需要在 api 上加上 [Authorize] 這個無具體規則的授權特性就行,大家先不要往下看,先停一分鐘想一想是不是這個情況。商城嘛,只需要使用者登入一下就可以購買了,我們不需要特地的區分商城使用者有什麼區別,有什麼三六九等,大家都是一樣,登入了,就可以任何操作,無論是買東西,還是寫文章,亦或者投票等等;
2、後臺管理專案:但是!還有另一種情況,那就是後臺管理,一個對使用者身份要求特別嚴格的一個系統,我們肯定不能僅僅在 api 介面地址上,加上 [Authorize] 這個簡單的特性就完事兒了,就比如我們的 Blog.Admin 專案,肯定需要一套複雜的授權策略機制,那就不得不用到使用者的角色資訊,或者其他的模組資訊,這就是我上邊說的 “區分控制”;(至於是基於角色的策略,還是模組化,我還在考慮中,目前先嚐試角色管理)
3、猜想:你是不是想說使用基於角色+策略授權的 Hybrid Flow 混合模式?彆著急,以後的問題會說到,這裡提出這個問題,就是向給大家一個思路的過程。
如果是第二種情況的話,我們在使用者註冊的時候,就需要帶上 “角色” 這個資訊,比如我這裡先預設是一個 test 系統測試管理員的角色(這個暫時這麼處理,後期我會再深入研究下,是不是這個模式,或者如果正再看的你很懂的話,歡迎指導下,不勝感激!),當然,如果你的專案不需要對使用者的許可權進行劃分,就比如我上邊的第一種情況,電商類,部落格類,只要不是後臺管理這種的前臺系統,都很簡單,只需要在 api 上加上 [Authorize] ,然後授權中心是不需要角色這個概念的。
我們學術討論嘛,當然是從複雜的著手,就把角色給考慮進去了,現在先寫死一個角色,我們以後的文章中會進一步討論這個複雜的情況:
[HttpPost] [Route("account/register")] [ValidateAntiForgeryToken] public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null, string rName = "AdminTest") { ViewData["ReturnUrl"] = returnUrl; IdentityResult result = new IdentityResult(); // 模型校驗 if (ModelState.IsValid) { // 判斷使用者名稱是否存在,說明:如果是DDD設計思想,這中查重應該是寫在領域模型的。 var userItem = _userManager.FindByNameAsync(model.LoginName).Result; if (userItem == null) { // 轉成我們的實體模型,說明:這種多個實體轉換,可以使用 Dto var user = new ApplicationUser { Email = model.Email, UserName = model.LoginName, LoginName = model.RealName, sex = model.Sex, age = model.Birth.Year - DateTime.Now.Year, birth = model.Birth, addr = "", tdIsDelete = false }; // 建立使用者,注意密碼的規範,比如必須有大小寫字母+數字+符號 result = await _userManager.CreateAsync(user, model.Password); if (result.Succeeded) { // 使用者新增成功後,就需要新增宣告瞭,看自己需要多少吧,可以自定義擴充套件 result = await _userManager.AddClaimsAsync(user, new Claim[]{ // 這個 Name ,就是 Jwt 的唯一名字,也是頁面裡展示的名稱,比如是“測試賬號”,而不是登入名的“test1” new Claim(JwtClaimTypes.Name, model.RealName), new Claim(JwtClaimTypes.Email, model.Email), // 是否需要進行 Email 郵件驗證 new Claim(JwtClaimTypes.EmailVerified, "false", ClaimValueTypes.Boolean), // 這裡就是角色宣告 new Claim(JwtClaimTypes.Role, rName) }); if (result.Succeeded) { // 新增成功,可以直接登入,這個就比如是我們的部落格專案或者電商專案,我們在授權中心註冊成功後,直接登入了,跳轉到前臺了。 //await _signInManager.SignInAsync(user, isPersistent: false); return RedirectToLocal(returnUrl); } } } else { ModelState.AddModelError(string.Empty, $"{userItem?.UserName} already exists"); } // 收集全部異常資料,返回前臺 AddErrors(result); } return View(model); }
上邊的就是註冊的主要程式碼,大家可以自己任意的擴充套件,然後重要的部分,我已經標紅,也寫上了詳細的註釋,特別簡單,都能看懂。
這一 Part 都很平常,最重要的一個問題還是那個角色這一塊,希望讀到這裡的都能看懂,想一想到底你的專案裡需不需要這樣的 Claim,不懂的歡迎來討論。
3、更新 與 邏輯刪除(有許可權)
上邊我們們說到了展示和新增,那下邊就是說到更新了(這個操作我帶上了最高的許可權,必須是超級管理員才能操作 [Authorize(Roles = "SuperAdmin")] ),你會問,為啥要把刪除和更新放到一起呢?其實我個人感覺邏輯是一樣的,平時開發肯定也都知道,邏輯刪除其實就是把“是否刪除” 這個欄位設定成 True 就行了,但是真的是這樣麼,我們慢慢往下看。
首先更新使用者這個很簡單的,我就不多說什麼了,具體的可以看看程式碼,主要的邏輯就是平時的三步走:
1、查詢出當前人Model;
2、用檢視模型修改Model;
3、執行更新操作 _userManager.UpdateAsync(userItem); // 這裡要說下就是,Identity 自帶了很多擴充套件方法,大家需要自己好好的研究下,從而達到自己的相應目的。
更新說完了,下邊說說刪除,刪除其實本身就有兩種情況:
1、邏輯刪除,很自然,就是將資料更新下狀態,比如我們可以用上邊的方法,把當前操作人的 IsDeleted=True 即可,很簡單;
2、物理刪除,這個還是需要好好研究研究,我在官方的程式碼裡,沒有找到如何物理刪除的方法,可能還是需要開發者自己定義擴充套件吧;
這就是我說的第二個 “Flag”? ,需要好好的思考思考,如果你已經忘了第一個 Flag 的話,請向上看,使用者註冊章節裡的角色問題。
(更新 & 刪除 有許可權 動圖)
4、重置密碼
這個是目前為止稍微複雜一點的,需用用到流程,首先看動圖吧:
(重置/更新密碼 動圖)
這個過程其實很簡單,也是專案中必須使用到的功能,我相信任何一個網站,必須要用到這個重置和找回密碼的功能吧,當然生產環境很複雜,可能需要郵箱或者手機等來處理動態連結,我這裡只是提供一個思路,總結來說,流程說明如下:
1、輸入當時註冊郵箱;
2、獲取包含動態 Code 的安全連結(可通過發郵件的形式);
3、根據安全連結,設定新密碼;
4、重新登入;
核心程式碼(節選):
// 1、判斷郵箱 var user = await _userManager.FindByEmailAsync(model.Email); // 2、生成重置密碼回撥連結 var code = await _userManager.GeneratePasswordResetTokenAsync(user); var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme); var ResetPassword = $"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>"; // 3、重置密碼 var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
5、其他情況處理?
通過上邊的簡單說明,AccountController 這個控制器的內容, 我們們說完了,是不是就沒有問題了呢,不是!我們要研究,就要研究透徹,大家肯定注意到了這個專案中,基本都說到了,但是在核心的快速啟動資料夾 Quickstart 中,還有幾個控制器沒有說到:
不光如此,在平時的開發中,我們還會遇到下邊這幾個業務邏輯操作:
1、如何找回註冊郵箱? 2、如何通過傳送郵件,從而達到郵件確認的目的? 3、如何實現FaceBook、Google登入? 4、如何更新使用者的角色等Claims?
5、如何重新整理 Token ?
上邊紅框中的那幾個控制器都是什麼意思?
下邊四條業務邏輯又該如何實現?
當前專案是不是還有其他不為我們知道的祕密?以後的章節再慢慢展開,請關注。
不過我們既然已經完成使用者的基本操作,我們就先停下上邊的疑惑問題,往下走走,看看 IdentityServer4 到底是如何通過 OpenID Connect 來操作的。
二、簡單授權模式 —— Implicit Flow OpenID
0、OpenID Connect授權模式
OPID 認證流程主要是由 OAuth2 的五種授權流程延伸而來的,它有以下 3 種:
- Authorization Code Flow(授權碼模式):基於OAuth2的授權碼來換取Id Token和Access Token。
- Implicit Flow(簡化模式):基於OAuth2的Implicit流程獲取Id Token和Access Token。
- Hybrid Flow:混合Authorization Code Flow+Implici Flow獲取Id Token和Access Token。
注:OpenID Connect 為什麼沒有基於OAuth2的Resource Owner Password Credentials Grant和Client Credentials Grant擴充套件,Resource Owner Password Credentials Grant是需要應用提供賬號密碼的,賬號密碼都有了在獲取Id Token意義不大。Client Credentials Grant沒有使用者的參與所以獲取Id Token 也沒意義。這也能反映授權和認證的差異,以及只使用OAuth2來做身份認證的事情是遠遠不夠的,也是不合適的。
1、概念
簡化模式用於獲取訪問令牌(但它不支援令牌的重新整理,之所以所以稱為簡化模式,和授權碼模式比少了獲取授權碼的步驟),並對執行特定重定向URI的公共客戶端進行優化,而這一些列操作通常會使用指令碼語言在瀏覽器中完成,令牌對訪問者是可見的,且客戶端也不需要驗證。
簡化模式,主要有下邊三個特點:
1、用於“公共”客戶端;
2、客戶端應用直接從瀏覽器訪問資源;
3、沒有顯式的客戶端身份認證;
2、結構圖
為了配合大家理解,我這裡有兩個場景,大家腦子裡先有個畫面,然後往下看四個角色和流程圖:
場景一:部落格園登入,需要獲取騰訊的某一個QQ使用者的頭像和暱稱等資源;
場景二:前後端分離,Vue 專案需要獲取 Core 專案的 當前test1賬號的 資料;
首先先理解下四個角色:
1、Resource Owner(資源擁有者) —— 資源所有者,就比如我們授權登入中的,QQ使用者,他才是資源的擁有者。3143422472 / test1賬號
2、Resource Server(資源伺服器) —— 資源伺服器,用來儲存使用者資源(頭像,暱稱等)的伺服器,比如騰訊QQ。騰訊QQ伺服器 / Blog.Core
3、Client(客戶端) —— 第三方客戶端,比如部落格園;https://www.cnblogs.com / Blog.Vue
4、Authorization Server(授權伺服器)—— 授權伺服器,用來作為認證第三方平臺的服務,比如騰訊的QQ互聯平臺。https://graph.qq.com/oauth2.0/show?whic...... / Blog.Idp
然後我們們看看具體的流程是怎樣的:
(流程1:參考網上畫的,可能不是很明瞭)
(流程2:自己根據官網圖片做了下修改)
Tips:Web-Hosted Client Resource 伺服器相當於是一個儲存 accessToken 的地方,通常指瀏覽器中的儲存(cookie、localStorage、SessionStorge、js變數等),一般這個頁面是看不到的,而且一般情況是和 Client 客戶端寫在一起的,當然也有分開的。
步驟解析:
-
客戶端攜帶客戶端標識以及重定向URI到授權伺服器;
-
使用者確認是否要授權給客戶端;
-
授權伺服器得到許可後,跳轉到指定的重定向地址,並將令牌也包含在了裡面;
-
客戶端不攜帶上次獲取到的包含令牌的片段,去請求資源伺服器;
-
資源伺服器會向瀏覽器返回一個指令碼;
-
瀏覽器會根據上一步返回的指令碼,去提取在C步驟中獲取到的令牌;
-
瀏覽器將令牌推送給客戶端。
(A步驟)中需要用到的引數,注意在這裡要使用"application/x-www-form-urlencoded"格式:
-
response_type 必選項,此值必須為"token"
-
client_id 必選項
-
redirect_uri 可選項
-
scope 可選項
-
state 建議選項
例如:
(C步驟)中返回的引數包含:
-
access_token 必選項
-
token_type 必選項
-
expires_in 建議選項
-
scope 可選項
-
state 必選項
例如:
上邊我們簡單的說了說 Implicit Flow 模式的相關知識點,不知道大家有沒有一點點感覺,如果不是很懂,正好感覺配合著下邊的程式碼研究下,二者結合會更好。
三、前後端專案授權聯調
因為我們用到了前後端分離專案,所以一定是要三方處理,如果你現在使用的是 MVC 模式的話,我們以後的章節也會說到 授權碼授權模式(Authorization Code Flow),這裡先把簡化模式調通了:
1、授權服務端 —— Implicit(Blog.Idp)
這個配置很簡單,在 Blog.Idp 專案中,大家別看是在 Config.cs 檔案裡,其實它已經在我們上一篇文章中,生成到了資料庫中,不懂的請回看上一篇文章
new Client { ClientId = "blogvuejs",//客戶端id ClientName = "Blog.Vue JavaScript Client", AllowedGrantTypes = GrantTypes.Implicit, AllowAccessTokensViaBrowser = true, RedirectUris = { "http://localhost:6688/callback" },//回撥頁面 PostLogoutRedirectUris = { "http://localhost:6688" }, AllowedCorsOrigins = { "http://localhost:6688" }, // 允許的前端獲取的作用域 AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "roles", "blog.core.api" } }
2、資源服務端 —— Bearer(Blog.Core)
這裡的配置是在 Blog.Core 我們的資源伺服器中,在啟動檔案 Startup.cs 中,大家自行檢視,注意如果使用這個的話,請把 Jwt 認證給註釋掉:
services.AddAuthentication("Bearer") .AddIdentityServerAuthentication(options => { options.Authority = "http://localhost:5002";//授權伺服器地址 options.RequireHttpsMetadata = false;//是否Https options.ApiName = "blog.core.api";//我們在 Blog.Idp 中配置的資源伺服器名 });
新增過程中,可能會需要引用擴充套件包 : IdentityServer4.AccessTokenValidation 這都是小問題,大家自行檢查即可。
3、請求客戶端 —— Oidc(Blog.Vue)
上邊我們已經在兩個服務端做好了配置,客戶端如何處理,這個地方才是今天的重頭戲,無論是什麼客戶端,JS 或者 Vue、React、Ng 等等前端框架,都需要用到 oidc-client 這個外掛庫:
1、安裝
執行命令:npm install oidc-client --save
2、封裝
注意這個是一個js庫,我們就像之前將 SignalR 那樣,直接使用就行,不用在 main.js 中引用,但是還是需要先例項化一個使用者管理類 ApplicationUserManager 並配置建構函式,請注意這些引數都要和 Blog.Idp 授權伺服器配置一致。
在 src 資料夾下 新建 Auth 資料夾,並新增 applicationusermanager.js 來封裝我們的連線管理:
import { UserManager } from 'oidc-client' class ApplicationUserManager extends UserManager { constructor () { super({ authority: 'http://localhost:5002',// 授權服務中心地址 client_id: 'blogvuejs',// 客戶端 id redirect_uri: 'http://localhost:6688/callback',// 登入回撥地址 response_type: 'id_token token', scope: 'openid profile roles blog.core.api',// 作用域也要一一匹配 post_logout_redirect_uri: 'http://localhost:6688' //登出後回撥地址 }) } async login () { await this.signinRedirect() return this.getUser() } async logout () { return this.signoutRedirect() } }
同時為了配合其他頁面使用,我們封裝幾個常用的方法,在 Auth 資料夾下,新建 UserAuth.js 來封裝使用者的一些基本資訊:
import applicationUserManager from "./applicationusermanager"; const userAuth = { data() { return { user: { name: "", isAuthenticated: false } }; }, methods: { async refreshUserInfo() {//獲取使用者資訊 const user = await applicationUserManager.getUser(); if (user) { this.user.name = user.profile.name; this.user.isAuthenticated = true; } else { this.user.name = ""; this.user.isAuthenticated = false; } } }, async created() { await this.refreshUserInfo(); } }; export default userAuth;
3、發起 登入/登出 請求
我們封裝好了方法,下邊就是直接設計業務邏輯了,過程很簡單,在 App.vue 元件中:
1、每次路由跳轉需非同步獲取使用者資料;
2、發起非同步登入請求;
3、發起非同步登出請求;
import applicationUserManager from "./Auth/applicationusermanager"; import userAuth from "./Auth/UserAuth"; export default { name: "app", mixins: [userAuth], data: function() { return {}; }, watch: { $route: async function(to, from) { //這裡使用Id4授權認證,用Jwt,請刪之; // await this.refreshUserInfo(); } }, methods: { async login() { try { await applicationUserManager.login(); } catch (error) { console.log(error); this.$root.$emit("show-snackbar", { message: error }); } }, async logout() { try { await applicationUserManager.logout(); this.$store.commit("saveToken", ""); } catch (error) { console.log(error); this.$root.$emit("show-snackbar", { message: error }); } } } };
4、回撥
在上邊的使用者管理配置中,我們用到了一個回撥頁面,這個很重要,因為我們在登入成功後,需要調整到客戶端,並且需要將資訊給儲存下來,就是上邊流程圖中,我們說到的 客戶端資源
具體怎麼寫的,很簡單,在 views 檢視頁面資料夾下,新建一個 LoginCallbackView.vue 頁面:
import applicationUserManager from '../Auth/applicationusermanager' export default { async created () { try { // 核心的就是這裡了 await applicationUserManager.signinRedirectCallback() let user = await applicationUserManager.getUser() // 將 token 儲存在客戶端 this.$store.commit("saveToken", user.access_token); // 調整首頁 this.$router.push({name: 'home'}) } catch (e) { console.log(e) this.$root.$emit('show-snackbar', { message: e }) } } }
四、結語
本文還是延續上篇文章的快速講解的風格,簡單連貫的把使用者管理和前後端聯調的內容通了一遍,總結一下:
1、分析了使用者是否需要角色等策略的緣由;
2、實現了對使用者的基本操作——CURD+重置密碼;
3、授權專案中還遺留了一片未知的知識塊,亟待探索;
4、實現了客戶端、資源伺服器、授權伺服器的第一次聯調;
5、重點講解了五大模式中的 Implicit Flow 簡化模式的概念和應用場景;
6、同時也把 Hybrid Flow 混合模式給引申出來,因為它基於 角色+策略 的授權;
當然,通過這一篇的學習,又開拓出了更多的未知領域,IdentityServer4 沒有我們想想的那麼難,但是肯定也不是一個 Demo 就能說的完的簡單,
如何解決文章中提到的,打算提到的,未提到的各種問題呢,請持續關注吧。
五、Github && Gitee
https://github.com/anjoy8/Blog.IdentityServer