Core篇——初探IdentityServer4(OpenID Connect客户端验证)
目录
1、Oauth2协议授权码模式介绍
2、IdentityServer4的OpenID Connect客户端验证简单实现
Oauth2协议授权码模式介绍
- 授权码模式是Oauth2协议中最严格的认证模式,它的组成以及运行流程是这样
1、用户访问客户端,客户端将用户导向认证服务器
2、用户在认证服务器输入用户名密码选择授权,认证服务器认证成功后,跳转至一个指定好的"跳转Url",同时携带一个认证码。
3、用户携带认证码请求指定好的"跳转Url"再次请求认证服务器(这一步后台完成,对用户不可见),此时,由认证服务器返回一个Token
4、客户端携带token请求用户资源
- OpenId Connect运行流程为
1、用户访问客户端,客户端将用户导向认证服务器
2、用户在认证服务器输入用户名密码认证授权
3、认证服务器返回token和资源信息
IdentityServer4的OpenID Connect客户端验证简单实现
Server部分
- 添加一个Mvc项目,配置Config.cs文件
public class Config
{
//定义要保护的资源(webapi)
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
};
}
//定义可以访问该API的客户端
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "mvc",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.Implicit, //简化模式
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
RequireConsent =true, //用户选择同意认证授权
RedirectUris={ "http://localhost:5001/signin-oidc" }, //指定允许的URI返回令牌或授权码(我们的客户端地址)
PostLogoutRedirectUris={ "http://localhost:5001/signout-callback-oidc" },//注销后重定向地址 参考https://identityserver4.readthedocs.io/en/release/reference/client.html
LogoUri="https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3298365745,618961144&fm=27&gp=0.jpg",
// scopes that client has access to
AllowedScopes = { //客户端允许访问个人信息资源的范围
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.Address,
IdentityServerConstants.StandardScopes.Phone
}
}
};
}
public static List<TestUser> GeTestUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password"
},
new TestUser
{
SubjectId = "2",
Username = "bob",
Password = "password"
}
};
}
//openid connect
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email()
};
}
}
- 添加几个ViewModel 用来接收解析跳转URL后的参数
public class InputConsentViewModel
{
public string Button { get; set; }
public IEnumerable<string> ScopesConsented { get; set; }
public bool RemeberConsent { get; set; }
public string ReturnUrl { get; set; }
}
//解析跳转url后得到的应用权限等信息
public class ConsentViewModel:InputConsentViewModel
{
public string ClientId { get; set; }
public string ClientName { get; set; }
public string ClientUrl { get; set; }
public string ClientLogoUrl { get; set; }
public IEnumerable<ScopeViewModel> IdentityScopes { get; set; }
public IEnumerable<ScopeViewModel> ResourceScopes { get; set; }
}
//接收Scope
public class ScopeViewModel
{
public string Name { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
public bool Emphasize { get; set; }
public bool Required { get; set; }
public bool Checked { get; set; }
}
public class ProcessConsentResult
{
public string RedirectUrl { get; set; }
public bool IsRedirectUrl => RedirectUrl != null;
public string ValidationError { get; set; }
public ConsentViewModel ViewModel { get; set; }
}
配置StartUp,将IdentityServer加入到DI容器,这里有个ConsentService,用来处理解析跳转URL的数据,这个Service在下面实现。
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddDeveloperSigningCredential() //添加登录证书
.AddInMemoryIdentityResources(Config.GetIdentityResources()) //添加IdentityResources
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GeTestUsers());
services.AddScoped<ConsentService>();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseIdentityServer();//引用IdentityServer中间件
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
- 添加一个ConsentService,用来根据Store拿到Resource
public class ConsentService
{
private readonly IClientStore _clientStore;
private readonly IResourceStore _resourceStore;
private readonly IIdentityServerInteractionService _identityServerInteractionService;
public ConsentService(IClientStore clientStore,
IResourceStore resourceStore,
IIdentityServerInteractionService identityServerInteractionService)
{
_clientStore = clientStore;
_resourceStore = resourceStore;
_identityServerInteractionService = identityServerInteractionService;
}
private ConsentViewModel CreateConsentViewModel(AuthorizationRequest request, Client client, Resources resources,InputConsentViewModel model)
{
//用户选中的Scopes
var selectedScopes = model?.ScopesConsented ?? Enumerable.Empty<string>();
//客户端传入信息填充consentViewModel
var vm = new ConsentViewModel();
vm.ClientName = client.ClientName;
vm.ClientLogoUrl = client.LogoUri;
vm.ClientUrl = client.ClientUri;
vm.RemeberConsent = model?.RemeberConsent??true;
vm.IdentityScopes = resources.IdentityResources.Select(t => CreateScopeViewModel(t,selectedScopes.Contains(t.Name) || model==null)); //resources的IdentityResources需要转换成我们自己的ViewModel ; 假如用户存在用户选中的Scope的话check 就传递一个true
vm.ResourceScopes = resources.ApiResources.SelectMany(t => t.Scopes).Select(e => CreateScopeViewModel(scope: e, check: selectedScopes.Contains(e.Name) || model == null));
return vm;
}
private ScopeViewModel CreateScopeViewModel(IdentityResource identityResource,bool check)
{
return new ScopeViewModel
{
Name = identityResource.Name,
Checked = check||identityResource.Required,
DisplayName = identityResource.DisplayName,
Description = identityResource.Description,
Required = identityResource.Required,
Emphasize = identityResource.Emphasize
};
}
private ScopeViewModel CreateScopeViewModel(Scope scope, bool check)
{
return new ScopeViewModel
{
Name = scope.Name,
Checked = check||scope.Required,
DisplayName = scope.DisplayName,
Description = scope.Description,
Required = scope.Required,
Emphasize = scope.Emphasize
};
}
public async Task<ConsentViewModel> BuildConsentViewModelAsync(string returnUrl, InputConsentViewModel viewModel=null)
{
var request = await _identityServerInteractionService.GetAuthorizationContextAsync(returnUrl); //解析returnUrl 拿到AuthorizationRequest对象,这个对象中包含ClientId等信息
if (request == null)
return null;
var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId); //根据request的ClientId拿到client
var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested);//拿到api的resource
var vm = CreateConsentViewModel(request, client, resources, viewModel);
vm.ReturnUrl = returnUrl;
return vm;
}
public async Task<ProcessConsentResult> PorcessConsent(InputConsentViewModel viewModel)
{
var result = new ProcessConsentResult();
ConsentResponse consentResponse = null;
if (viewModel.Button == "no")
{
consentResponse = ConsentResponse.Denied;
}
else if (viewModel.Button == "yes") //用户选择确认授权,把用户选择的scopes赋值给ConsentResponse的Scopes
{
if (viewModel.ScopesConsented != null && viewModel.ScopesConsented.Any())
{
consentResponse = new ConsentResponse
{
ScopesConsented = viewModel.ScopesConsented,
RememberConsent = viewModel.RemeberConsent //是否记住
};
}
result.ValidationError = "请至少选中一个权限";
}
if (consentResponse != null)
{
var request = await _identityServerInteractionService.GetAuthorizationContextAsync(viewModel.ReturnUrl);
await _identityServerInteractionService.GrantConsentAsync(request, consentResponse);
result.RedirectUrl = viewModel.ReturnUrl;
}
var consentViewModel = await BuildConsentViewModelAsync(viewModel.ReturnUrl,viewModel);
result.ViewModel = consentViewModel;
return result;
}
}
添加一个ConsentController,用来显示授权登录页面,以及相应的跳转登录逻辑。
public class ConsentController : Controller
{
private readonly ConsentService _consentService;
public ConsentController(ConsentService consentService)
{
_consentService = consentService;
}
public async Task<IActionResult> Index(string returnUrl)
{
//调用consentService的BuildConsentViewModelAsync方法,将跳转Url作为参数传入,解析得到一个ConsentViewModel
var model =await _consentService.BuildConsentViewModelAsync(returnUrl);
if (model == null)
return null;
return View(model);
}
[HttpPost]
public async Task<IActionResult> Index(InputConsentViewModel viewModel)
{
//用户选择确认按钮的时候,根据选择按钮确认/取消,以及勾选权限
var result = await _consentService.PorcessConsent(viewModel);
if (result.IsRedirectUrl)
{
return Redirect(result.RedirectUrl);
}
if (!string.IsNullOrEmpty(result.ValidationError))
{
ModelState.AddModelError("", result.ValidationError);
}
return View(result.ViewModel);
}
}
- 接下来给Consent控制器的Index添加视图
@using mvcCookieAuthSample.ViewModels
@model ConsentViewModel
<h2>ConsentPage</h2>
@*consent*@
<div class="row page-header">
<div class="col-sm-10">
@if (!string.IsNullOrWhiteSpace(Model.ClientLogoUrl))
{
<div>
<img src="@Model.ClientLogoUrl" style="width:50px;height:50px" />
</div>
}
<h1>@Model.ClientName</h1>
<p>希望使用你的账户</p>
</div>
</div>
@*客户端*@
<div class="row">
<div class="col-sm-8">
<div asp-validation-summary="All" class="danger"></div>
<form asp-action="Index" method="post">
<input type="hidden" asp-for="ReturnUrl"/>
@if (Model.IdentityScopes.Any())
{
<div class="panel">
<div class="panel-heading">
<span class="glyphicon glyphicon-user"></span>
用户信息
</div>
<ul class="list-group">
@foreach (var scope in Model.IdentityScopes)
{
@Html.Partial("_ScopeListitem.cshtml", scope);
}
</ul>
</div>
}
@if (Model.ResourceScopes.Any())
{
<ul class="list-group">
@foreach (var scope in Model.ResourceScopes)
{
@Html.Partial("_ScopeListitem.cshtml", scope);
}</ul>
}
<div>
<label>
<input type="checkbox" asp-for="RemeberConsent"/>
<strong>记住我的选择</strong>
</label>
</div>
<div>
<button name="button" value="yes" class="btn btn-primary" autofocus>同意</button>
<button name="button" value="no">取消</button>
@if (!string.IsNullOrEmpty(Model.ClientUrl))
{
<a href="@Model.ClientUrl" class="pull-right btn btn-default">
<span class="glyphicon glyphicon-info-sign" ></span>
<strong>@Model.ClientUrl</strong>
</a>
}
</div>
</form>
</div>
</div>
//这里用到了一个分部视图用来显示用户允许授权的身份资源和api资源
@using mvcCookieAuthSample.ViewModels
@model ScopeViewModel;
<li>
<label>
<input type="checkbox"
name="ScopesConsented"
id="scopes_@Model.Name"
value="@Model.Name"
checked=@Model.Checked
disabled=@Model.Required/>
@if (Model.Required)
{
<input type="hidden" name="ScopesConsented" value="@Model.Name" />
}
<strong>@Model.Name</strong>
@if (Model.Emphasize)
{
<span class="glyphicon glyphicon-exclamation-sign"></span>
}
</label>
@if(string.IsNullOrEmpty(Model.Description))
{
<div>
<label for="scopes_@Model.Name">@Model.Description</label>
</div>
}
</li>
- 添加客户端,依旧添加一个mvc项目,配置startup,Home/Index action打上Authorize标签。
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options => {
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";//openidconnectservice
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc",options=> {
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000"; //设置认证服务器
options.RequireHttpsMetadata = false;
options.ClientId = "mvc"; //openidconfig的配置信息
options.ClientSecret = "secret";
options.SaveTokens = true;
});
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
设置服务端端口5000,运行服务器端;设置客户端端口5001,运行客户端。我们可以看到,localhost:5001会跳转至认证服务器
然后看下Url=》
使用config配置的testuser登录系统,选择允许授权的身份权限。登录成功后看到我们的Claims
总结
- 最后来总结一下
用户访问客户端(5001端口程序),客户端将用户导向认证服务器(5000程序),用户选择允许授权的身份资源和api资源后台解析(这两个资源分别由Resources提供,resources 由IResourceStore解析returnurl后的Scopes提供),最后由ProfileService返回数条Claim。(查看ConsentService的各个方法)