術(shù)語http://www.tuicool.com/articles/2mQjIr
本文翻譯自IdentityServer教程茁计,如感覺有不好理解的地方料皇,請參考原文。
</br>
本教程將引導(dǎo)你建立一個基礎(chǔ)版的IdentityServer
星压。從簡單化角度践剂,本教程將合并IdentityServer
和Client
到同一個Web程序--這不是真實(shí)使用場景,但是可以讓你快速了解IdentityServer
的核心概念租幕。
請從此處here獲取完整代碼.
第一節(jié) - MVC 認(rèn)證和授權(quán)
第一節(jié)我們將創(chuàng)建一個簡單的MVC程序舷手,并通過IdentityServer
添加認(rèn)證。然后仔細(xì)了解聲明(claims)
,聲明轉(zhuǎn)換
和授權(quán)
劲绪。
創(chuàng)建Web應(yīng)用程序
在Visual Studio 2015中創(chuàng)建一個標(biāo)準(zhǔn)的MVC應(yīng)用并且設(shè)置認(rèn)證方式為“無認(rèn)證"男窟。
通過屬性框把項(xiàng)目切換到SSL:
重要
不要忘記在項(xiàng)目屬性框中更新啟動URL(https://localhost:44387/)。
添加 IdentityServer
IdentityServer
基于 OWIN/Katana 并且通過 Nuget 分發(fā). 用下面的命令在程序包管理器控制臺
添加對應(yīng)的包到剛剛創(chuàng)建的的WEB應(yīng)用中:
install-package Microsoft.Owin.Host.Systemweb
install-package IdentityServer3
配置IdentityServer - Clients
添加一個Clients類贾富,使IdentityServer
知道支持的Client(客戶)的基本信息:
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
Enabled = true,
ClientName = "MVC Client",
ClientId = "mvc",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"https://localhost:44387/"
},
AllowAccessToAllScopes = true
}
};
}
}
注意 當(dāng)前客戶可以訪問所有的范圍(Scope) (通過AllowAccessToAllScopes
設(shè)置).在生產(chǎn)環(huán)境需要對它限制.后面有更詳細(xì)解釋歉眷。
配置 IdentityServer - Users
下面我們會加一些用戶到IdentityServer
--這里我們直接硬編碼一些用戶,生產(chǎn)環(huán)境應(yīng)該從其他數(shù)據(jù)源中獲取颤枪。IdentityServer
提供對ASP.net Identity和MemberShipReboot的直接支持汗捡。
public static class Users
{
public static List<InMemoryUser> Get()
{
return new List<InMemoryUser>
{
new InMemoryUser
{
Username = "bob",
Password = "secret",
Subject = "1",
Claims = new[]
{
new Claim(Constants.ClaimTypes.GivenName, "Bob"),
new Claim(Constants.ClaimTypes.FamilyName, "Smith")
}
}
};
}
}
添加Startup
IdentityServer
通過startup類來配置。在Startup類中畏纲,我們提供了客戶扇住,用戶,范圍盗胀,簽名證書和其它配置信息艘蹋。生產(chǎn)環(huán)境應(yīng)該從Windows certificates store或者類似的源加載證書。簡單起見我們直接把證書加到項(xiàng)目中票灰,你可以從這里直接下載.添加到工程中女阀,并且設(shè)置為始終復(fù)制
.
關(guān)于如何從Azure WebSites裝載證書宅荤,請看這里.
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = new IdentityServerServiceFactory()
.UseInMemoryUsers(Users.Get())
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(StandardScopes.All)
});
});
}
X509Certificate2 LoadCertificate()
{
return new X509Certificate2(
string.Format(@"{0}\bin\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
}
}
我們已經(jīng)添加好全功能的IdentityServer
, 可以通過發(fā)現(xiàn)端點(diǎn)(discovery endpoint)了解配置情況
https://localhost:44387/identity/.well-known/openid-configuration
RAMMFAR
最后,不要忘記在web.config中添加RAMMFAR支持浸策,否則有些內(nèi)嵌的資源無法在IIS中正常加載:
<system.webServer>
<modules runAllManagedModulesForAllRequests="true" />
</system.webServer>
添加和配置OpenID Connect 認(rèn)證中間件
支持OIDC認(rèn)證需要另外兩個nuget 程序包:
install-package Microsoft.Owin.Security.Cookies
install-package Microsoft.Owin.Security.OpenIdConnect
在startup.cs中使用缺省值配置cookie中間件
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
同樣在startup.cs中配置OpenID Connect中間件冯键,指向我們內(nèi)嵌的IdentityServer
并使用我們前面配置的客戶信息:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44387/identity",
ClientId = "mvc",
RedirectUri = "https://localhost:44387/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies"
});
添加被保護(hù)的資源和現(xiàn)實(shí)聲明
使用IdentityServer
是為了保護(hù)一些資源(頁面,API)的訪問, 本教程中庸汗,我們通過全局授權(quán)過濾器惫确,簡單保護(hù)Home
控制器的About
頁面,并且顯示哪一個聲明(用戶)在訪問蚯舱。
[Authorize]
public ActionResult About()
{
return View((User as ClaimsPrincipal).Claims);
}
對應(yīng)的View(About.cshtml)修改如下:
@model IEnumerable<System.Security.Claims.Claim>
<dl>
@foreach (var claim in Model)
{
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>
}
</dl>
認(rèn)證和聲明
經(jīng)過上面的設(shè)置后雕薪,在例子程序的主頁上單擊關(guān)于
鏈接將激活認(rèn)證機(jī)制,例子程序?qū)@示一個登陸界面--用前面硬編碼的用戶(bob)和密碼(secret)登陸后-- 會發(fā)回一個token到主程序晓淀,OpenID connect中間件驗(yàn)證token,提取聲明信息盏档,然后把聲明信息傳給cookie中間件設(shè)置認(rèn)證cookie凶掰。如下圖用戶現(xiàn)在登陸啦。
譯者注此處需要使用https://localhost:44837/來訪問蜈亩,不能使用http懦窘,否則會反復(fù)認(rèn)證。
添加角色聲明和范圍
接下來稚配,我們將添加角色聲明來進(jìn)行授權(quán)畅涂。
現(xiàn)在我們離開OIDC的標(biāo)準(zhǔn)范圍--讓我們定義一個包含角色聲明的角色范圍并且添加到標(biāo)準(zhǔn)范圍中。
public static class Scopes
{
public static IEnumerable<Scope> Get()
{
var scopes = new List<Scope>
{
new Scope
{
Enabled = true,
Name = "roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
}
};
scopes.AddRange(StandardScopes.All);
return scopes;
}
}
在Startup
中使用新定義的Scope:
Factory = new IdentityServerServiceFactory()
.UseInMemoryUsers(Users.Get())
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get()),
然后我們添加一些角色聲明給硬編碼的Bob:
public static class Users
{
public static IEnumerable<InMemoryUser> Get()
{
return new[]
{
new InMemoryUser
{
Username = "bob",
Password = "secret",
Subject = "1",
Claims = new[]
{
new Claim(Constants.ClaimTypes.GivenName, "Bob"),
new Claim(Constants.ClaimTypes.FamilyName, "Smith"),
new Claim(Constants.ClaimTypes.Role, "Geek"),
new Claim(Constants.ClaimTypes.Role, "Foo")
}
}
};
}
}
修改中間件配置請求角色信息
OIDC中間件默認(rèn)只要求兩個Scopes:openid
和profile
-- 這就是為什么IdentityServer包括主題(subject)和名字聲明〉来ǎ現(xiàn)在我們加上roles
范圍:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44319/identity",
ClientId = "mvc",
Scope = "openid profile roles",
RedirectUri = "https://localhost:44319/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies"
});
修改編譯成功后午衰,訪問關(guān)于
頁面,這是就能看到角色聲明啦冒萄。
Claims transformation
仔細(xì)檢查關(guān)于
頁面上的聲明信息臊岸,有兩點(diǎn)引起我們的注意:
- 有些聲明帶有很長的類型名
- 很多的聲明信息我們并不需要.
長類型名是由微軟的JWT handler試圖把聲明類型映射到.Net的ClaimTypes
類型上。我們可以通過下面的代碼關(guān)閉這個功能(在Startup
類里面)尊流。關(guān)閉這個功能后帅戒,對于跨域訪問會有些問題,--例子中不會有問題崖技,但是大部分oauth2服務(wù)會跨域的--,所以我們要調(diào)整反跨站點(diǎn)請求偽造
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
修改后逻住,聲明看起來簡潔多了:
長長的聲明名稱沒了,但是還有很多底層的協(xié)議用的聲明迎献,我們并不需要瞎访。把原始的聲明轉(zhuǎn)換成程序需要的聲明叫做聲明轉(zhuǎn)換
。在這個過程中忿晕,我們拿到傳入的全部聲明装诡,選擇那些聲明需要以及從數(shù)據(jù)源中獲取更多的聲明信息以便程序使用银受。
OIDC中間件有一個通知機(jī)制讓我們做聲明轉(zhuǎn)換
,轉(zhuǎn)換后的聲明會保存到cookie中。
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44319/identity",
ClientId = "mvc",
Scope = "openid profile roles",
RedirectUri = "https://localhost:44319/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
var id = n.AuthenticationTicket.Identity;
// we want to keep first name, last name, subject and roles
var givenName = id.FindFirst(Constants.ClaimTypes.GivenName);
var familyName = id.FindFirst(Constants.ClaimTypes.FamilyName);
var sub = id.FindFirst(Constants.ClaimTypes.Subject);
var roles = id.FindAll(Constants.ClaimTypes.Role);
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
id.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role);
nid.AddClaim(givenName);
nid.AddClaim(familyName);
nid.AddClaim(sub);
nid.AddClaims(roles);
// add some other app specific claim
nid.AddClaim(new Claim("app_specific", "some data"));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
return Task.FromResult(0);
}
}
});
最終的聲明信息看起來簡單多了:
授權(quán)
好了鸦采,我們現(xiàn)在認(rèn)證了用戶宾巍,也有了用戶的一些信息,現(xiàn)在我們要加上一些簡單的授權(quán)規(guī)則了渔伯。
MVC有一個內(nèi)置的特性[Authorize]
顶霞,用于標(biāo)識需要認(rèn)證的頁面(webapi),通過這個特性,我們也可以標(biāo)識一些角色需求锣吼。
我們不建議使用這個特性选浑,它把業(yè)務(wù)邏輯和權(quán)限策略混在一起啦。分離業(yè)務(wù)邏輯和權(quán)限策略可以讓代碼更清晰玄叠,有更好的可測試性古徒。(關(guān)于這一點(diǎn),請閱讀 參考文件).
資源授權(quán)
添加下面的程序包獲得新的授權(quán)架構(gòu)和特性:
install-package Thinktecture.IdentityModel.Owin.ResourceAuthorization.Mvc
然后我們在Home
的Contact
操作上添加特性读恃,標(biāo)記這個操作會讀取
(read) 聯(lián)系人詳情
(contactDetaisl)資源:
[ResourceAuthorize("Read", "ContactDetails")]
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
注意這個特性不是標(biāo)記誰允許讀聯(lián)系人信息 --- 具體的授權(quán)管理(操作隧膘、資源和誰能夠操作這些資源)從應(yīng)用中分離到AuthorizationManager
啦。
public class AuthorizationManager : ResourceAuthorizationManager
{
public override Task<bool> CheckAccessAsync(ResourceAuthorizationContext context)
{
switch (context.Resource.First().Value)
{
case "ContactDetails":
return AuthorizeContactDetails(context);
default:
return Nok();
}
}
private Task<bool> AuthorizeContactDetails(ResourceAuthorizationContext context)
{
switch (context.Action.First().Value)
{
case "Read":
return Eval(context.Principal.HasClaim("role", "Geek"));
case "Write":
return Eval(context.Principal.HasClaim("role", "Operator"));
default:
return Nok();
}
}
}
最后我們把授權(quán)管理放到Startup
的OWIN的管道中去:
app.UseResourceAuthorization(new AuthorizationManager());
調(diào)試下這個例子寺惫,單步跟隨代碼疹吃,了解一下整個認(rèn)證授權(quán)過程。
角色授權(quán)
注意西雀,如果你選擇角色授權(quán)[Authorize(Roles = "Foo,Bar")]
, 當(dāng)前用戶被認(rèn)證但未授權(quán)角色的時候萨驶,有可能會進(jìn)入一種重定向死循環(huán)。因?yàn)?code>Authroize特性發(fā)現(xiàn)訪問用戶被認(rèn)證但未授權(quán)艇肴, 會讓操作返回401未授權(quán)腔呜,401未授權(quán)會重定向到認(rèn)證服務(wù)器(IdentityServer), 而認(rèn)證服務(wù)器又會認(rèn)證這個用戶并重定向回去再悼。噢哦育谬,重定向死循環(huán)開始啦。帮哈。膛檀。。
這個問題可以通過重載Authrize
特性的 HandleUnauthorizedRequest
方法來解決娘侍。代碼如下:
// 定制授權(quán)特性 Customized authorization attribute:
public class AuthAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.User.Identity.IsAuthenticated)
{
// 403 我們知道你是誰咖刃,但是你無權(quán)訪問
filterContext.Result = new HttpStatusCodeResult(System.Net.HttpStatusCode.Forbidden);
}
else
{
// 401 你是誰?請登陸后再試
filterContext.Result = new HttpUnauthorizedResult();
}
}
}
// Usage:
[Auth(Roles = "Geek")]
public ActionResult About()
{
// ...
}
更多的授權(quán)和拒絕訪問場景
讓我們在Home
控制器上加一個新的操作憾筏,進(jìn)一步探索授權(quán)過程:
[ResourceAuthorize("Write", "ContactDetails")]
public ActionResult UpdateContact()
{
ViewBag.Message = "Update your contact details!";
return View();
}
當(dāng)你訪問 /home/updatecontact
URL 你會看到禁止頁面嚎杨。
實(shí)際上會有不同的響應(yīng),如果你已經(jīng)登陸氧腰,那么你會看到上述的禁止頁面枫浙,否則刨肃,你會被重定向到登陸頁面。這是認(rèn)證授權(quán)設(shè)計好的流程(進(jìn)一步學(xué)習(xí)請看這里).
可以通過檢查403
狀態(tài)碼來處理禁止情況---我們提供了一個現(xiàn)成的filter箩帚,代碼如下:
[ResourceAuthorize("Write", "ContactDetails")]
[HandleForbidden]
public ActionResult UpdateContact()
{
ViewBag.Message = "Update your contact details!";
return View();
}
HandleForbidden
filter(可以作用在全局)會把未授權(quán)的訪問(403
)重定向到一個特殊的視圖----默認(rèn)會得到一個禁止
視圖真友。
譯者注需要在view的shared目錄下創(chuàng)建一個Forbidden.cshtml視圖,否則會報告404錯誤紧帕。
更好的辦法是使用authroization manager
進(jìn)行精細(xì)控制:
[HandleForbidden]
public ActionResult UpdateContact()
{
if (!HttpContext.CheckAccess("Write", "ContactDetails", "some more data"))
{
// either 401 or 403 based on authentication state
return this.AccessDenied();
}
ViewBag.Message = "Update your contact details!";
return View();
}
增加登出功能
在操作中調(diào)用katana認(rèn)證的Signout
方法就可以登出盔然,非常簡單明了。
public ActionResult Logout()
{
Request.GetOwinContext().Authentication.SignOut();
return Redirect("/");
}
Signout
方法調(diào)用 IdentityServer
的 endsession 的方法是嗜,這個方法會清除認(rèn)證cookie和結(jié)束當(dāng)前session愈案。
一般來說,登出時應(yīng)該關(guān)閉瀏覽器鹅搪,清除所有的會話數(shù)據(jù)站绪。有一些應(yīng)用則希望用戶登出的時候,以一個匿名用戶繼續(xù)留在網(wǎng)站丽柿。通過簡單幾步就可以達(dá)到這個目的崇众,首先注冊一個有效的URL作為登出完成后的地址。這個在MVC應(yīng)用的Client中定義: (注意新的 PostLogoutRedirectUris
設(shè)置):
new Client
{
Enabled = true,
ClientName = "MVC Client",
ClientId = "mvc",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"https://localhost:44387/"
},
PostLogoutRedirectUris = new List<string>
{
"https://localhost:44387/"
}
}
接下來航厚,客戶端需要把登陸時得到的token 發(fā)送給登出方法,以便我們重定向到正確的URL上(不是垃圾郵件地址或者釣魚地址)锰蓬。之前的代碼我們丟棄了這個token幔睬,現(xiàn)在我們要改變聲明轉(zhuǎn)換邏輯來保存它。
這個需要通過在SecurityTokenValidated
通知里面增加一行代碼來實(shí)現(xiàn)芹扭。
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
最后一步麻顶,我們通過OIDC中間件的通知機(jī)制,登出時把id_token發(fā)送到identityServer舱卡。
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
經(jīng)過上面的改變以后辅肾,IdentityServer
將顯示一個鏈接給用戶,通過這個鏈接可以回到最初的Web應(yīng)用:
Tip 在 IdentityServerOptions
上面轮锥,有一個 AuthenticationOptions
對象. 這個對象有一個屬性EnablePostSignOutAutoRedirect
. 和你想的一樣矫钓, 把他設(shè)置為true,登出時會自動重定向舍杜,不需要用戶點(diǎn)擊鏈接新娜。
增加Google賬號認(rèn)證
現(xiàn)在我們要提供第三方認(rèn)證功能,首先添加一個katana認(rèn)證中間件給IdentityServer
--這里我們使用Google既绩。
在Google登記我們的IdentityServer
我們必須在Google的開發(fā)者控制面板中登記我們的IdentityServer
概龄,按照下面的流程一步步做就好。
打開新的瀏覽器饲握,轉(zhuǎn)到下面的鏈接:
https://console.developers.google.com
創(chuàng)建一個新項(xiàng)目
啟用Google+ API
在同意
界面填寫郵件地址和產(chǎn)品名稱
創(chuàng)建應(yīng)用程序
單擊創(chuàng)建用戶ID后私杜,你會得到一個客戶id(Client id)和客戶密鑰(client secret).保存好他們蚕键,在配置google認(rèn)證中間件的時候我們需要用到他們。
增加Google認(rèn)證中間件
通過Nuget程序包管理器控制臺添加Google中間件:
install-package Microsoft.Owin.Security.Google
配置中間件
在Startup
中添加下述方法:
注意:需要用剛才的客戶Id和密鑰替換代碼中的...
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions
{
AuthenticationType = "Google",
Caption = "Sign-in with Google",
SignInAsAuthenticationType = signInAsType,
ClientId = "...",
ClientSecret = "..."
});
}
然后我們把IdentityServer
的認(rèn)證選項(xiàng)指向這個方法
Next we point our IdentityServer options class to this method:
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = new IdentityServerServiceFactory()
.UseInMemoryUsers(Users.Get())
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get()),
AuthenticationOptions = new IdentityServer3.Core.Configuration.AuthenticationOptions
{
IdentityProviders = ConfigureIdentityProviders
}
});
代碼修改完成衰粹,下一次用戶登錄時锣光,你會看到右邊有一個"Sign-in with google"的選項(xiàng):
注意使用Google登陸沒有角色聲明信息,因?yàn)間oogle本身沒有角色概念寄猩。 在接受第三方認(rèn)證時嫉晶,需要考慮第三方給的聲明信息可能不全。
第二節(jié)- WebAPI支持
這一節(jié)田篇,我們將增加一個Web API項(xiàng)目到解決方案中去替废。這個API由IdentityServer
保護(hù)。我們的MVC應(yīng)用會使用可信子系統(tǒng)
和代理認(rèn)證方法
來調(diào)用這個API泊柬。
添加 Web API 項(xiàng)目
最簡單的增加API項(xiàng)目的方式是添加一個空Web項(xiàng)目
通過Nuget增加WebAPI和Katana的支持:
install-package Microsoft.Owin.Host.SystemWeb
install-package Microsoft.Aspnet.WebApi.Owin
添加一個測試控制器
下面這個控制器會返回所有的聲明信息給調(diào)用者--我們可以通過這個方法得到token所包含的信息椎镣。
[Route("identity")]
[Authorize]
public class IdentityController : ApiController
{
public IHttpActionResult Get()
{
var user = User as ClaimsPrincipal;
var claims = from c in user.Claims
select new
{
type = c.Type,
value = c.Value
};
return Json(claims);
}
}
在Startup中連接Web API 和 Security
在所有基于katana的應(yīng)用,配置都發(fā)生在Startup
中兽赁。
public class Startup
{
public void Configuration(IAppBuilder app)
{
// web api configuration
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
我們希望用IdentityServer來保護(hù)我們的API---需要實(shí)現(xiàn)兩件事:
- 只接受來自IdentityServer的令牌
- 只接受給API的令牌 - 為了實(shí)現(xiàn)這一點(diǎn)状答,我們給API接口一個名字sampleApi(也叫
作用域
)
To accomplish that, we add a Nuget packages:
為了達(dá)到這個目標(biāo),我們需要安裝一個Nuget包:
install-package IdentityServer3.AccessTokenValidation
..并在Startup
中使用他們:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44319/identity",
RequiredScopes = new[] { "sampleApi" }
});
// web api configuration
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
注意
IdentityServer發(fā)送標(biāo)準(zhǔn)的JWT(JSON Web Tokens),你也可以用無格式的katana JWT中間件來驗(yàn)證他們刀崖。上面安裝的中間件自動用IdentityServer的自動發(fā)現(xiàn)文檔(metadata)來配置自己惊科,用起來比較方便。
在IdentityServer中注冊API
接下來亮钦,我們需要注冊這個API--通過擴(kuò)展作用域來實(shí)現(xiàn)馆截,這次我們增加一個資源作用域:
public static class Scopes
{
public static IEnumerable<Scope> Get()
{
var scopes = new List<Scope>
{
new Scope
{
Enabled = true,
Name = "roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
},
new Scope
{
Enabled = true,
DisplayName = "Sample API",
Name = "sampleApi",
Description = "Access to a sample API",
Type = ScopeType.Resource
}
};
scopes.AddRange(StandardScopes.All);
return scopes;
}
}
注冊web api客戶端
下面我們要調(diào)用這個API,你可以使用客戶端證書(作為一個服務(wù)賬號)蜂莉,或者使用用戶身份蜡娶。
我們首先使用客戶端證書
第一步,我們注冊為MVC 應(yīng)用一個新的客戶映穗,因?yàn)榘踩矫娴脑蚪颜牛琁dentityServer 只允許每個客戶一個flow。
而我們當(dāng)前的MVC客戶端已經(jīng)使用隱式flow蚁滋,所以我們需要為服務(wù)到服務(wù)的通信創(chuàng)建一個新的客戶宿接。
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
ClientName = "MVC Client",
ClientId = "mvc",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"https://localhost:44319/"
},
PostLogoutRedirectUris = new List<string>
{
"https://localhost:44319/"
},
AllowedScopes = new List<string>
{
"openid",
"profile",
"roles",
"sampleApi"
}
},
new Client
{
ClientName = "MVC Client (service communication)",
ClientId = "mvc_service",
Flow = Flows.ClientCredentials,
ClientSecrets = new List<Secret>
{
new Secret("secret".Sha256())
},
AllowedScopes = new List<string>
{
"sampleApi"
}
}
};
}
}
備注 上面的代碼片段通過AllowdScopes
設(shè)置,限制了不同的客戶端可以訪問的作用域辕录。
調(diào)用API
調(diào)用這個API由兩部分組成:
- 使用客戶證書從IdentityServer獲得訪問令牌澄阳。
- 使用訪問令牌調(diào)用API
下面的nuget包可以簡化OAuth2的交互,把它安裝到MVC項(xiàng)目下:(注意不是webapi項(xiàng)目)
install-package IdentityModel
在MVC的Controllers目錄下增加一個新的類 CallApiController. 下面的代碼片段使用服務(wù)端客戶憑據(jù)獲得sampleApi的訪問令牌踏拜。
private async Task<TokenResponse> GetTokenAsync()
{
var client = new TokenClient(
"https://localhost:44319/identity/connect/token",
"mvc_service",
"secret");
return await client.RequestClientCredentialsAsync("sampleApi");
}
下面的代碼片段使用訪問令牌調(diào)用web Api獲得identity信息:
private async Task<string> CallApi(string token)
{
var client = new HttpClient();
client.SetBearerToken(token);
var json = await client.GetStringAsync("https://localhost:44321/identity");
return JArray.Parse(json).ToString();
}
加上一個視圖和對應(yīng)的控制方法碎赢,調(diào)用上述輔助方法,一個新的顯示聲明
的操作就okay啦速梗。代碼如下:
public class CallApiController : Controller
{
// GET: CallApi/ClientCredentials
public async Task<ActionResult> ClientCredentials()
{
var response = await GetTokenAsync();
var result = await CallApi(response.AccessToken);
ViewBag.Json = result;
return View("ShowApiResult");
}
// helpers omitted
}
創(chuàng)建一個ShowApiResult.cshtml
文件, 簡單的顯示結(jié)果的視圖:
<h2>Result</h2>
<pre>@ViewBag.Json</pre>
訪問這個URL肮塞,結(jié)果如下:
換句話說襟齿,API知道調(diào)用者的信息:
- 發(fā)布者信息,聽眾和過期時間(通過令牌驗(yàn)證中間件)
- 令牌在那個作用域里面有效(通過作用域驗(yàn)證中間件)
- 客戶端ID
令牌包含的所有聲明信息會保存到ClaimsPrincipal枕赵,可以通過.User屬性查看猜欺,使用。
使用登錄用戶的權(quán)限信息
現(xiàn)在我們使用登錄者的權(quán)限信息調(diào)用WebAPI拷窜。在OpenID 連接中間件作用域上面配置上sampleAPI
, 同時在期望響應(yīng)類型上加上 token
开皿,要求認(rèn)證服務(wù)器返回訪問令牌。
Scope = "openid profile roles sampleApi",
ResponseType = "id_token token"
為了優(yōu)化效率篮昧,IdentityServer
發(fā)現(xiàn)請求包括訪問token
后赋荆,會把聲明從標(biāo)識令牌中移除,這樣可以減小標(biāo)識令牌的大小懊昨。有了訪問令牌后窄潭,聲明信息可以從用戶信息接口獲取。
從用戶信息結(jié)構(gòu)獲取聲明很簡單酵颁,UserInfoClient
類簡化了操作嫉你。另外,我們把訪問令牌放到cookie里面躏惋,要訪問API的時候幽污,我們從cookie中獲取,而不用每次都去認(rèn)證服務(wù)器認(rèn)證簿姨。
譯者注 :標(biāo)識令牌在每次調(diào)用webapi或者請求頁面時都要從客戶端發(fā)到服務(wù)器端距误,太大會影響通訊效率。
SecurityTokenValidated = async n =>
{
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role);
// get userinfo data
var userInfoClient = new UserInfoClient(
new Uri(n.Options.Authority + "/connect/userinfo"),
n.ProtocolMessage.AccessToken);
var userInfo = await userInfoClient.GetAsync();
userInfo.Claims.ToList().ForEach(ui => nid.AddClaim(new Claim(ui.Item1, ui.Item2)));
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
// add access token for sample API
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
// keep track of access token expiration
nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));
// add some other app specific claim
nid.AddClaim(new Claim("app_specific", "some data"));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
}
練習(xí):請重新配置IdentityServer
款熬,設(shè)置作用域聲明的AlwaysIncludeInIdToken
強(qiáng)制包括一些聲明在標(biāo)識令牌中,無論IdentityServer
是否優(yōu)化令牌訪問攘乒。
調(diào)用API
我們現(xiàn)在把訪問令牌保存到了cookie中贤牛,我們可以從聲明對象(claims principal)中取出令牌,并用這個令牌調(diào)用服務(wù)则酝。
// GET: CallApi/UserCredentials
public async Task<ActionResult> UserCredentials()
{
var user = User as ClaimsPrincipal;
var token = user.FindFirst("access_token").Value;
var result = await CallApi(token);
ViewBag.Json = result;
return View("ShowApiResult");
}
登陸后殉簸,轉(zhuǎn)到UserCredentials頁面,你會看到sub
信息,說明你現(xiàn)在是使用用戶的權(quán)限在訪問API沽讹。
譯者注:sub
是用戶的唯一標(biāo)識般卑, 之前使用特定客戶端的權(quán)限的時候,是沒有這個標(biāo)識的爽雄。
現(xiàn)在可以增加一個role
的作用域聲明到sampleApi
作用域中蝠检。--用戶角色將會包括在訪問令牌中。
new Scope
{
Enabled = true,
DisplayName = "Sample API",
Name = "sampleApi",
Description = "Access to a sample API",
Type = ScopeType.Resource,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
}