一、前言
從上一篇關於資源密碼憑證模式中,通過使用client_id和client_secret以及用戶名密碼通過應用Client(客戶端)直接獲取,從而請求獲取受保護的資源,但是這種方式存在client可能存了用戶密碼這不安全性問題,所以需要做到client是高可信的應用。因此,我們可以考慮通過其他方式來解決這個問題。
我們通過Oauth2.0的簡化授權模式了解到,可以使用這種方式來解決這個問題,讓用戶自己在IdentityServer服務器進行登錄驗證,客戶端不需要知道用戶的密碼,從而實現用戶密碼的安全性。
所以在這一篇中,我們將通過多種授權模式中的簡化授權模式進行說明,主要針對介紹IdentityServer保護API的資源,簡化授權訪問API資源。
二、初識
有些 Web 應用是純前端應用,沒有后端,必須將令牌儲存在前端。RFC 6749 就規定了這種方式,允許直接向前端頒發令牌。這種方式沒有授權碼這個中間步驟,所以稱為(授權碼)"簡化"(implicit)。
簡化模式(implicit grant type)不通過第三方應用程序的服務器,直接在瀏覽器中向認證服務器申請令牌,跳過了"授權碼"這個步驟(授權碼模式后續會說明)。所有步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端不需要認證。
這種方式把令牌直接傳給前端,是很不安全的。因此,只能用於一些安全要求不高的場景,並且令牌的有效期必須非常短,通常就是會話期間(session)有效,瀏覽器關掉,令牌就失效了。
2.1 適用范圍
這種模式的使用場景是基於瀏覽器的應用
這種模式基於安全性考慮,建議把token時效設置短一些, 不支持refresh token
2.2 授權流程:
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI --->| |
| User- | | Authorization |
| Agent -|----(B)-- User authenticates -->| Server |
| | | |
| |<---(C)--- Redirection URI ----<| |
| | with Access Token +---------------+
| | in Fragment
| | +---------------+
| |----(D)--- Redirection URI ---->| Web-Hosted |
| | without Fragment | Client |
| | | Resource |
| (F) |<---(E)------- Script ---------<| |
| | +---------------+
+-|--------+
| |
(A) (G) Access Token
| |
^ v
+---------+
| |
| Client |
| |
+---------+
簡化授權流程描述
(A)客戶端攜帶客戶端標識以及重定向URI到授權服務器;
(B)用戶確認是否要授權給客戶端;
(C)授權服務器得到許可后,跳轉到指定的重定向地址,並將令牌也包含在了里面;
(D)客戶端不攜帶上次獲取到的包含令牌的片段,去請求資源服務器;
(E)資源服務器會向瀏覽器返回一個腳本;
(F)瀏覽器會根據上一步返回的腳本,去提取在C步驟中獲取到的令牌;
(G)瀏覽器將令牌推送給客戶端。
2.2.1 過程詳解
訪問令牌請求
參數 | 是否必須 | 含義 |
---|---|---|
response_type | 必需 | 表示授權類型,此處的值固定為"token" |
client_id | 必需 | 客戶端ID |
redirect_uri | 可選 | 表示重定向的URI |
scope | 可選 | 表示授權范圍。 |
state | 可選 | 表示隨機字符串 |
(1)資源服務器生成授權URL並將用戶重定向到授權服務器
(用戶的操作:用戶訪問https://resourcesServer/index.html跳轉到登錄地址,選擇授權服務器方式登錄)
在授權開始之前,它首先生成state參數(隨機字符串)。client端將需要存儲這個(cookie,會話或其他方式),以便在下一步中使用。
第一步,A 網站提供一個鏈接,要求用戶跳轉到 B 網站,授權用戶數據給 A 網站使用。
https://oauth2Server/oauth2/default/v1/authorize?
response_type=token
&client_id=${clientId}
&redirect_uri=https://resourcesServer/implicit.html
&scope=授權范圍
&state=隨機字符串
生成的授權URL如上所述(如上),請求這個地址后重定向訪問授權服務器,其中 response_type參數為token,表示直接返回令牌。
(2)驗證授權服務器登陸狀態
(用戶的操作:如果未登陸用賬號 User,密碼12345登陸https://oauth2Server/login,如果已登陸授權服務器不需要此步驟)
如果未登陸賬號,自動跳轉到授權服務器登陸地址,登陸授權服務器以后用戶被重定向client端
https://resourcesServer/implicit.html
如已提前登陸授權服務器或授權服務器登陸會話還存在自動重定向到client端
https://resourcesServer/implicit.html
(3)驗證狀態參數
(用戶的操作:無需操作)
用戶被重定向回客戶機,URL中現在有一個片段包含訪問令牌以及一些其他信息。
用戶跳轉到 B 網站,登錄后同意給予 A 網站授權。這時,B 網站就會跳回redirect_uri
參數指定的跳轉網址,並且把令牌作為 URL 參數,傳給 A 網站。
https://resourcesServer/authorization-code.html
\#access_token=&token_type=Bearer&expires_in=3600&scope=photo&state=隨機字符串
其中,token參數就是令牌,A網站因此直接在前端拿到令牌。
注意,令牌的位置是 URL 錨點(fragment),而不是查詢字符串(querystring),這是因為 OAuth 2.0 允許跳轉網址是 HTTP 協議,因此存在"中間人攻擊"的風險,而瀏覽器跳轉時,錨點不會發到服務器,就減少了泄漏令牌的風險。
用戶使用這個令牌訪問資源服務器,當令牌失效時使用刷新令牌去換取新的令牌
三、實踐
在示例實踐中,我們將創建一個授權訪問服務,定義一個MVC客戶端,MVC客戶端通過IdentityServer上請求訪問令牌,並使用它來訪問API。
3.1 搭建 Authorization Server 服務
搭建認證授權服務
3.1.1 安裝Nuget包
IdentityServer4
程序包
3.1.2 配置內容
建立配置內容文件Config.cs
public static class Config
{
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("Implicit_scope1")
};
public static IEnumerable<ApiResource> ApiResources =>
new ApiResource[]
{
new ApiResource("api1","api1")
{
Scopes={ "Implicit_scope1" },
ApiSecrets={new Secret("apipwd".Sha256())} //api密鑰
}
};
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "Implicit_client",
ClientName = "Implicit Auth",
AllowedGrantTypes = GrantTypes.Implicit,
RedirectUris ={
"http://localhost:5002/signin-oidc", //跳轉登錄到的客戶端的地址
},
PostLogoutRedirectUris ={
"http://localhost:5002/signout-callback-oidc",//跳轉登出到的客戶端的地址
},
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"Implicit_scope1"
},
// 是否需要同意授權 (默認是false)
RequireConsent=true
},
};
}
RedirectUris
: 登錄成功回調處理的客戶端地址,處理回調返回的數據,可以有多個。
PostLogoutRedirectUris
:跳轉登出到的客戶端的地址。這兩個都是配置的客戶端的地址,且是identityserver4組件里面封裝好的地址,作用分別是登錄,注銷的回調
因為是簡化授權的方式,所以我們通過代碼的方式來創建幾個測試用戶。
新建測試用戶文件TestUsers.cs
public class TestUsers
{
public static List<TestUser> Users
{
get
{
var address = new
{
street_address = "One Hacker Way",
locality = "Heidelberg",
postal_code = 69118,
country = "Germany"
};
return new List<TestUser>
{
new TestUser
{
SubjectId = "1",
Username = "i3yuan",
Password = "123456",
Claims =
{
new Claim(JwtClaimTypes.Name, "i3yuan Smith"),
new Claim(JwtClaimTypes.GivenName, "i3yuan"),
new Claim(JwtClaimTypes.FamilyName, "Smith"),
new Claim(JwtClaimTypes.Email, "i3yuan@email.com"),
new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
new Claim(JwtClaimTypes.WebSite, "http://i3yuan.top"),
new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json)
}
}
};
}
}
}
返回一個TestUser的集合。
通過以上添加好配置和測試用戶后,我們需要將用戶注冊到IdentityServer4服務中,接下來繼續介紹。
3.1.3 注冊服務
在startup.cs中ConfigureServices方法添加如下代碼:
public void ConfigureServices(IServiceCollection services)
{
var builder = services.AddIdentityServer()
.AddTestUsers(TestUsers.Users); //添加測試用戶
// in-memory, code config
builder.AddInMemoryIdentityResources(Config.IdentityResources);
builder.AddInMemoryApiScopes(Config.ApiScopes);
builder.AddInMemoryApiResources(Config.ApiResources);
builder.AddInMemoryClients(Config.Clients);
// not recommended for production - you need to store your key material somewhere secure
builder.AddDeveloperSigningCredential();
}
3.1.4 配置管道
在startup.cs中Configure方法添加如下代碼:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseIdentityServer();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
以上內容是快速搭建簡易IdentityServer項目服務的方式。
這搭建 Authorization Server 服務跟上一篇資源密碼憑證模式有何不同之處呢?
- 在Config中配置客戶端(client)中定義了一個
AllowedGrantTypes
的屬性,這個屬性決定了Client可以被哪種模式被訪問,GrantTypes.Implicit為簡化授權。所以在本文中我們需要添加一個Client用於支持簡化授權(implicit)。- 簡化授權不通過第三方應用程序的服務器,直接在瀏覽器中向認證服務器申請令牌,所有步驟在瀏覽器中完成,所以需要配置對應的回調地址和登出地址。這也是不同於之前的資源所有者憑證模式。
3.2 搭建MVC 客戶端
實現對客戶端認證授權訪問資源
3.2.1 快速搭建一個MVC項目
3.2.2 安裝Nuget包
IdentityServer4.AccessTokenValidation 包
3.2.3 注冊服務
要將對 OpenID Connect 身份認證的支持添加到MVC應用程序中。
在startup.cs中ConfigureServices方法添加如下代碼:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthorization();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:5001";
options.RequireHttpsMetadata = false;
options.ClientId = "Implicit_client";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
});
}
AddAuthentication
注入添加認證授權,當需要用戶登錄時,使用cookie
來本地登錄用戶(通過“Cookies”作為DefaultScheme
),並將DefaultChallengeScheme
設置為“oidc”,使用
AddCookie
添加可以處理 cookie 的處理程序。因為簡化模式的實現是就是
OpenID Connect
,所以在AddOpenIdConnect
用於配置執行OpenID Connect
協議的處理程序。Authority
表明之前搭建的 IdentityServer 授權服務地址。然后我們通過ClientId
。識別這個客戶端。SaveTokens
用於在 cookie 中保留來自IdentityServer 的令牌。
3.2.4 配置管道
然后要確保認證服務執行對每個請求的驗證,加入UseAuthentication
和UseAuthorization
到Configure
中,在startup.cs中Configure方法添加如下代碼:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
UseAuthentication將身份驗證中間件添加到管道中;
UseAuthorization 將啟動授權中間件添加到管道中,以便在每次調用主機時執行身份驗證授權功能。
3.2.5 添加授權
在HomeController控制器並添加[Authorize]
特性到其中一個方法。在進行請求的時候,需進行認證授權通過后,才能進行訪問。
[Authorize]
public IActionResult Privacy()
{
ViewData["Message"] = "Secure page.";
return View();
}
還要修改主視圖以顯示用戶的Claim以及cookie屬性。
@using Microsoft.AspNetCore.Authentication
<h2>Claims</h2>
<dl>
@foreach (var claim in User.Claims)
{
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>
}
</dl>
<h2>Properties</h2>
<dl>
@foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
{
<dt>@prop.Key</dt>
<dd>@prop.Value</dd>
}
</dl>
訪問 Privacy 頁面,跳轉到認證服務地址,進行賬號密碼登錄,Logout 用於用戶的注銷操作。
3.3 效果
3.3.1 項目測試
四、問題
4.1 SameSite策略
在Chrome瀏覽器中,進行認證授權的時候,用戶登錄之后,無法跳轉到原網頁,還是停留在登錄頁中,可以看控制台就發現上圖的效果。
最后查找資料發現,是Google將於2020年2月份發布Chrome 80版本。本次發布將推進Google的“漸進改良Cookie”策略,打造一個更為安全和保障用戶隱私的網絡環境。所以本次更新可能導致瀏覽器無法向服務端發送Cookie。如果你有多個不同域名的應用,部分用戶很有可能出現會話時常被打斷的情況,還有部分用戶可能無法正常登出系統。
所以我們需要解決這個問題:
方法一:將域名升級為 HTTPS
方法二:使用代碼修改 SameSite 設置
新增 SameSiteCookiesServiceCollectionExtensions 類 (可以下載源碼查看)
private const SameSiteMode Unspecified = (SameSiteMode)(-1);
改為
private const SameSiteMode Unspecified = SameSiteMode.Lax;
如果沒有域名或內網環境,可以使用該方法,在 Startup 添加引用。
public IServiceProvider ConfigureServices(IServiceCollection services)
{
...
services.ConfigureNonBreakingSameSiteCookies();
...
五、總結
- 本篇主要闡述以簡化授權,編寫一個MVC客戶端,並通過客戶端以瀏覽器的形式請求IdentityServer上請求獲取訪問令牌,從而訪問資源。
- 簡化模式解決了客戶端模式用戶身份驗證和授權的問題,也解決了上一篇中資源所有者密碼憑證授權面臨的用戶密碼暴露的問題,是基於瀏覽器的應用。但由於token攜帶在url中,安全性方面不能保證,建議把token時效設置短一些
- 在后續會對在安全性方面做得更好的模式進行說明,數據庫持久化問題,以及如何應用在API資源服務器中和配置在客戶端中,會進一步說明。
- 如果有不對的或不理解的地方,希望大家可以多多指正,提出問題,一起討論,不斷學習,共同進步。
- 項目地址