入门教程: MVC 认证和WebAPI
术语http://www.tuicool.com/articles/2mQjIr
本文翻译自IdentityServer教程,如感觉有不好理解的地方,请参考原文。
</br>
本教程将引导你建立一个基础版的IdentityServer
。从简单化角度,本教程将合并IdentityServer
和Client
到同一个Web程序--这不是真实使用场景,但是可以让你快速了解IdentityServer
的核心概念。
请从此处here获取完整代码.
第一节 - MVC 认证和授权
第一节我们将创建一个简单的MVC程序,并通过IdentityServer
添加认证。然后仔细了解声明(claims)
,声明转换
和授权
。
创建Web应用程序
在Visual Studio 2015中创建一个标准的MVC应用并且设置认证方式为“无认证"。
创建MVC应用
通过属性框把项目切换到SSL:
设置 ssl
重要
不要忘记在项目属性框中更新启动URL(https://localhost:44387/)。
添加 IdentityServer
IdentityServer
基于 OWIN/Katana 并且通过 Nuget 分发. 用下面的命令在程序包管理器控制台
添加对应的包到刚刚创建的的WEB应用中:
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
}
};
}
}
注意 当前客户可以访问所有的范围(Scope) (通过AllowAccessToAllScopes
设置).在生产环境需要对它限制.后面有更详细解释。
配置 IdentityServer - Users
下面我们会加一些用户到IdentityServer
--这里我们直接硬编码一些用户,生产环境应该从其他数据源中获取。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类中,我们提供了客户,用户,范围,签名证书和其它配置信息。生产环境应该从Windows certificates store或者类似的源加载证书。简单起见我们直接把证书加到项目中,你可以从这里直接下载.添加到工程中,并且设置为始终复制
.
关于如何从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");
}
}
我们已经添加好全功能的IdentityServer
, 可以通过发现端点(discovery endpoint)了解配置情况
https://localhost:44387/identity/.well-known/openid-configuration
RAMMFAR
最后,不要忘记在web.config中添加RAMMFAR支持,否则有些内嵌的资源无法在IIS中正常加载:
<system.webServer>
<modules runAllManagedModulesForAllRequests="true" />
</system.webServer>
添加和配置OpenID Connect 认证中间件
支持OIDC认证需要另外两个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中间件,指向我们内嵌的IdentityServer
并使用我们前面配置的客户信息:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44387/identity",
ClientId = "mvc",
RedirectUri = "https://localhost:44387/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies"
});
添加被保护的资源和现实声明
使用IdentityServer
是为了保护一些资源(页面,API)的访问, 本教程中,我们通过全局授权过滤器,简单保护Home
控制器的About
页面,并且显示哪一个声明(用户)在访问。
[Authorize]
public ActionResult About()
{
return View((User as ClaimsPrincipal).Claims);
}
对应的View(About.cshtml)修改如下:
@model IEnumerable<System.Security.Claims.Claim>
<dl>
@foreach (var claim in Model)
{
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>
}
</dl>
认证和声明
经过上面的设置后,在例子程序的主页上单击关于
链接将激活认证机制,例子程序将显示一个登陆界面--用前面硬编码的用户(bob)和密码(secret)登陆后-- 会发回一个token到主程序,OpenID connect中间件验证token,提取声明信息,然后把声明信息传给cookie中间件设置认证cookie。如下图用户现在登陆啦。
译者注此处需要使用https://localhost:44837/来访问,不能使用http,否则会反复认证。
角色声明Claims transformation
仔细检查关于
页面上的声明信息,有两点引起我们的注意:
- 有些声明带有很长的类型名
- 很多的声明信息我们并不需要.
长类型名是由微软的JWT handler试图把声明类型映射到.Net的ClaimTypes
类型上。我们可以通过下面的代码关闭这个功能(在Startup
类里面)。关闭这个功能后,对于跨域访问会有些问题,--例子中不会有问题,但是大部分oauth2服务会跨域的--,所以我们要调整反跨站点请求伪造
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
修改后,声明看起来简洁多了:
简洁的声明长长的声明名称没了,但是还有很多底层的协议用的声明,我们并不需要。把原始的声明转换成程序需要的声明叫做声明转换
。在这个过程中,我们拿到传入的全部声明,选择那些声明需要以及从数据源中获取更多的声明信息以便程序使用。
OIDC中间件有一个通知机制让我们做声明转换
,转换后的声明会保存到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);
}
}
});
最终的声明信息看起来简单多了:
增加一个空Web项目通过Nuget增加WebAPI和Katana的支持:
install-package Microsoft.Owin.Host.SystemWeb
install-package Microsoft.Aspnet.WebApi.Owin
添加一个测试控制器
下面这个控制器会返回所有的声明信息给调用者--我们可以通过这个方法得到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的应用,配置都发生在Startup
中。
public class Startup
{
public void Configuration(IAppBuilder app)
{
// web api configuration
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
我们希望用IdentityServer来保护我们的API---需要实现两件事:
- 只接受来自IdentityServer的令牌
- 只接受给API的令牌 - 为了实现这一点,我们给API接口一个名字sampleApi(也叫
作用域
)
To accomplish that, we add a Nuget packages:
为了达到这个目标,我们需要安装一个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发送标准的JWT(JSON Web Tokens),你也可以用无格式的katana JWT中间件来验证他们。上面安装的中间件自动用IdentityServer的自动发现文档(metadata)来配置自己,用起来比较方便。
在IdentityServer中注册API
接下来,我们需要注册这个API--通过扩展作用域来实现,这次我们增加一个资源作用域:
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客户端
下面我们要调用这个API,你可以使用客户端证书(作为一个服务账号),或者使用用户身份。
我们首先使用客户端证书
第一步,我们注册为MVC 应用一个新的客户,因为安全方面的原因,IdentityServer 只允许每个客户一个flow。
而我们当前的MVC客户端已经使用隐式flow,所以我们需要为服务到服务的通信创建一个新的客户。
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
设置,限制了不同的客户端可以访问的作用域。
调用API
调用这个API由两部分组成:
- 使用客户证书从IdentityServer获得访问令牌。
- 使用访问令牌调用API
下面的nuget包可以简化OAuth2的交互,把它安装到MVC项目下:(注意不是webapi项目)
install-package IdentityModel
在MVC的Controllers目录下增加一个新的类 CallApiController. 下面的代码片段使用服务端客户凭据获得sampleApi的访问令牌。
private async Task<TokenResponse> GetTokenAsync()
{
var client = new TokenClient(
"https://localhost:44319/identity/connect/token",
"mvc_service",
"secret");
return await client.RequestClientCredentialsAsync("sampleApi");
}
下面的代码片段使用访问令牌调用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();
}
加上一个视图和对应的控制方法,调用上述辅助方法,一个新的显示声明
的操作就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
}
创建一个ShowApiResult.cshtml
文件, 简单的显示结果的视图:
<h2>Result</h2>
<pre>@ViewBag.Json</pre>
访问这个URL,结果如下:
callapiclientcreds
换句话说,API知道调用者的信息:
- 发布者信息,听众和过期时间(通过令牌验证中间件)
- 令牌在那个作用域里面有效(通过作用域验证中间件)
- 客户端ID
令牌包含的所有声明信息会保存到ClaimsPrincipal,可以通过.User属性查看,使用。
使用登录用户的权限信息
现在我们使用登录者的权限信息调用WebAPI。在OpenID 连接中间件作用域上面配置上sampleAPI
, 同时在期望响应类型上加上 token
,要求认证服务器返回访问令牌。
Scope = "openid profile roles sampleApi",
ResponseType = "id_token token"
为了优化效率,IdentityServer
发现请求包括访问token
后,会把声明从标识令牌中移除,这样可以减小标识令牌的大小。有了访问令牌后,声明信息可以从用户信息接口获取。
从用户信息结构获取声明很简单,UserInfoClient
类简化了操作。另外,我们把访问令牌放到cookie里面,要访问API的时候,我们从cookie中获取,而不用每次都去认证服务器认证。
译者注 :标识令牌在每次调用webapi或者请求页面时都要从客户端发到服务器端,太大会影响通讯效率。
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);
}
练习:请重新配置IdentityServer
,设置作用域声明的AlwaysIncludeInIdToken
强制包括一些声明在标识令牌中,无论IdentityServer
是否优化令牌访问。
调用API
我们现在把访问令牌保存到了cookie中,我们可以从声明对象(claims principal)中取出令牌,并用这个令牌调用服务。
// 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");
}
登陆后,转到UserCredentials页面,你会看到sub
信息,说明你现在是使用用户的权限在访问API。
译者注:sub
是用户的唯一标识, 之前使用特定客户端的权限的时候,是没有这个标识的。
现在可以增加一个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")
}
}
delegationroles