Loading

Core篇——初探IdentityServer4(OpenID Connect模式)

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文件
  •  1   public class Config
     2     {
     3         //定义要保护的资源(webapi)
     4         public static IEnumerable<ApiResource> GetApiResources()
     5         {
     6             return new List<ApiResource>
     7             {
     8                 new ApiResource("api1", "My API")
     9             };
    10         }
    11         //定义可以访问该API的客户端
    12         public static IEnumerable<Client> GetClients()
    13         {
    14             return new List<Client>
    15             {
    16                 new Client
    17                 {
    18                     ClientId = "mvc",
    19                     // no interactive user, use the clientid/secret for authentication
    20                     AllowedGrantTypes = GrantTypes.Implicit,  //简化模式
    21                     // secret for authentication
    22                     ClientSecrets =
    23                     {
    24                         new Secret("secret".Sha256())
    25                     },
    26                     RequireConsent =true,                                  //用户选择同意认证授权
    27                     RedirectUris={ "http://localhost:5001/signin-oidc" },  //指定允许的URI返回令牌或授权码(我们的客户端地址)
    28                     PostLogoutRedirectUris={ "http://localhost:5001/signout-callback-oidc" },//注销后重定向地址 参考https://identityserver4.readthedocs.io/en/release/reference/client.html
    29                     LogoUri="https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3298365745,618961144&fm=27&gp=0.jpg",
    30                     // scopes that client has access to
    31                     AllowedScopes = {                       //客户端允许访问个人信息资源的范围
    32                         IdentityServerConstants.StandardScopes.Profile,
    33                         IdentityServerConstants.StandardScopes.OpenId,
    34                         IdentityServerConstants.StandardScopes.Email,
    35                         IdentityServerConstants.StandardScopes.Address,
    36                         IdentityServerConstants.StandardScopes.Phone
    37                     }
    38                 }
    39             };
    40         }
    41         public static List<TestUser> GeTestUsers()
    42         {
    43             return new List<TestUser>
    44             {
    45                 new TestUser
    46                 {
    47                     SubjectId = "1",
    48                     Username = "alice",
    49                     Password = "password"
    50                 },
    51                 new TestUser
    52                 {
    53                     SubjectId = "2",
    54                     Username = "bob",
    55                     Password = "password"
    56                 }
    57             };
    58         }
    59         //openid  connect
    60         public static IEnumerable<IdentityResource> GetIdentityResources()
    61         {
    62             return new List<IdentityResource>
    63             {
    64                 new IdentityResources.OpenId(),
    65                 new IdentityResources.Profile(),
    66                 new IdentityResources.Email()
    67             };
    68         }
    69     }
    Config
  • 添加几个ViewModel 用来接收解析跳转URL后的参数
  •  1     public class InputConsentViewModel
     2     {
     3         public string Button { get; set; }
     4         public IEnumerable<string> ScopesConsented { get; set; }
     5 
     6         public bool RemeberConsent { get; set; }
     7         public string ReturnUrl { get; set; }
     8     }
     9     //解析跳转url后得到的应用权限等信息
    10     public class ConsentViewModel:InputConsentViewModel
    11     {
    12         public string ClientId { get; set; }
    13         public string ClientName { get; set; }
    14         public string ClientUrl { get; set; }
    15         public string ClientLogoUrl { get; set; }
    16         public IEnumerable<ScopeViewModel> IdentityScopes { get; set; }
    17         public IEnumerable<ScopeViewModel> ResourceScopes { get; set; }
    18     }
    19     //接收Scope
    20     public class ScopeViewModel  
    21     {
    22         public string Name { get; set; }
    23         public string DisplayName { get; set; }
    24         public string Description { get; set; }
    25         public bool Emphasize { get; set; }
    26         public bool Required { get; set; }
    27         public bool Checked { get; set; }
    28     }
    29     public class ProcessConsentResult
    30     {
    31         public string RedirectUrl { get; set; }
    32         public bool IsRedirectUrl => RedirectUrl != null;
    33         public string ValidationError { get; set; }
    34         public ConsentViewModel ViewModel { get; set; }
    35     }
    ViewModel
  • 配置StartUp,将IdentityServer加入到DI容器,这里有个ConsentService,用来处理解析跳转URL的数据,这个Service在下面实现。
 1         public void ConfigureServices(IServiceCollection services)
 2         {
 3             services.AddIdentityServer()
 4                 .AddDeveloperSigningCredential()  //添加登录证书
 5                 .AddInMemoryIdentityResources(Config.GetIdentityResources())  //添加IdentityResources
 6                 .AddInMemoryApiResources(Config.GetApiResources())
 7                 .AddInMemoryClients(Config.GetClients())
 8                 .AddTestUsers(Config.GeTestUsers());
 9             services.AddScoped<ConsentService>();
10             services.AddMvc();
11         }
12         public void Configure(IApplicationBuilder app, IHostingEnvironment env)
13         {
14             if (env.IsDevelopment())
15             {
16                 app.UseDeveloperExceptionPage();
17             }
18             else
19             {
20                 app.UseExceptionHandler("/Home/Error");
21             }
22             app.UseStaticFiles();
23             app.UseIdentityServer();//引用IdentityServer中间件
24             app.UseMvc(routes =>
25             {
26                 routes.MapRoute(
27                     name: "default",
28                     template: "{controller=Home}/{action=Index}/{id?}");
29             });
30         }
Startup配置IdentityServer
  • 添加一个ConsentService,用来根据Store拿到Resource
  •  1     public class ConsentService
     2     {
     3         private readonly IClientStore _clientStore;
     4         private readonly IResourceStore _resourceStore;
     5         private readonly IIdentityServerInteractionService _identityServerInteractionService;
     6 
     7 
     8         public ConsentService(IClientStore clientStore,
     9             IResourceStore resourceStore,
    10             IIdentityServerInteractionService identityServerInteractionService)
    11         {
    12             _clientStore = clientStore;
    13             _resourceStore = resourceStore;
    14             _identityServerInteractionService = identityServerInteractionService;
    15         }
    16 
    17         public async Task<ConsentViewModel> BuildConsentViewModel(string returnUrl)
    18         {
    19             //根据return url 拿到ClientId 等信息
    20             var request = await _identityServerInteractionService.GetAuthorizationContextAsync(returnUrl);
    21             if (returnUrl == null)
    22                 return null;
    23             var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId);
    24             var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested);//根据请求的scope 拿到resources
    25 
    26 
    27             return CreateConsentViewModel(request, client, resources);
    28         }
    29 
    30         private ConsentViewModel CreateConsentViewModel(AuthorizationRequest request, Client client, Resources resources)
    31         {
    32             var vm = new ConsentViewModel();
    33             vm.ClientName = client.ClientName;
    34             vm.ClientLoggoUrl = client.LogoUri;
    35             vm.ClientUrl = client.ClientUri;
    36             vm.RemeberConsent = client.AllowRememberConsent;
    37 
    38             vm.IdentityScopes = resources.IdentityResources.Select(i => CreateScopeViewModel(i));
    39             //api resource
    40             vm.ResourceScopes = resources.ApiResources.SelectMany(i => i.Scopes).Select(x => CreateScopeViewModel(scope: x));
    41             return vm;
    42         }
    43         //identity 1个scopes
    44         private ScopeViewModel CreateScopeViewModel(IdentityResource identityResource)
    45         {
    46             return new ScopeViewModel
    47             {
    48                 Name = identityResource.Name,
    49                 DisplayName = identityResource.DisplayName,
    50                 Description = identityResource.Description,
    51                 Required = identityResource.Required,
    52                 Checked = identityResource.Required,
    53                 Emphasize = identityResource.Emphasize
    54             };
    55         }
    56         //apiresource
    57         private ScopeViewModel CreateScopeViewModel(Scope scope)
    58         {
    59             return new ScopeViewModel
    60             {
    61                 Name = scope.Name,
    62                 DisplayName = scope.DisplayName,
    63                 Description = scope.Description,
    64                 Required = scope.Required,
    65                 Checked = scope.Required,
    66                 Emphasize = scope.Emphasize
    67             };
    68         }
    69     }
    ConsentService
  • 添加一个ConsentController,用来显示授权登录页面,以及相应的跳转登录逻辑。
 1 public class ConsentController : Controller
 2     {
 3         private readonly ConsentService _consentService;
 4         public ConsentController(ConsentService consentService)
 5         {
 6             _consentService = consentService;
 7         }
 8 
 9         public async Task<IActionResult> Index(string returnUrl)
10         {
11             //调用consentService的BuildConsentViewModelAsync方法,将跳转Url作为参数传入,解析得到一个ConsentViewModel
12             var model =await _consentService.BuildConsentViewModelAsync(returnUrl);
13             if (model == null)
14                 return null;
15             return View(model);
16         }
17         [HttpPost]
18         public async Task<IActionResult> Index(InputConsentViewModel viewModel)
19         {
20             //用户选择确认按钮的时候,根据选择按钮确认/取消,以及勾选权限
21             var result = await _consentService.PorcessConsent(viewModel);
22             if (result.IsRedirectUrl)
23             {
24                 return Redirect(result.RedirectUrl);
25             }
26             if (!string.IsNullOrEmpty(result.ValidationError))
27             {
28                 ModelState.AddModelError("", result.ValidationError);
29             }
30             return View(result.ViewModel);
31         }
32     }
ConsentController
  • 配置服务端的登录Controller
  •  1     public class AccountController : Controller
     2     {
     3         private readonly TestUserStore _user;  //放入DI容器中的TestUser(GeTestUsers方法),通过这个对象获取
     4         public AccountController(TestUserStore user)
     5         {
     6             _user = user;
     7         }        public IActionResult Login(string returnUrl = null)
     8         {
     9             ViewData["ReturnUrl"] = returnUrl;
    10             return View();
    11         }
    12            
    13         [HttpPost]
    14         public async Task<IActionResult> Login(LoginViewModel loginViewModel,string returnUrl)
    15         {
    16             //用户登录
    17             if (ModelState.IsValid)
    18             {
    19                 ViewData["ReturnUrl"] = returnUrl;
    20                 var user =  _user.FindByUsername(loginViewModel.Email);  
    21                 if (user == null)
    22                 {
    23                     ModelState.AddModelError(nameof(loginViewModel.Email), "Email not exists");
    24                 }
    25                 else
    26                 {
    27                     var result = _user.ValidateCredentials(loginViewModel.Email, loginViewModel.Password);
    28                     if(result)
    29                     {
    30                         var props = new AuthenticationProperties()
    31                         {
    32                             IsPersistent = true,
    33                             ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30)
    34                         };
    35                         await Microsoft.AspNetCore.Http.AuthenticationManagerExtensions.SignInAsync(   //Id4扩展方法和HttpContext扩展方法重名,这里强制使用命名空间方法
    36                             this.HttpContext,
    37                             user.SubjectId,
    38                             user.Username,
    39                             props);
    40                         return RedirectToLoacl(returnUrl);
    41                     }
    42                     else
    43                     {
    44                         ModelState.AddModelError(nameof(loginViewModel.Email), "Wrong password");
    45                     } 
    46                 }
    47             }
    48 
    49             return View();
    50         }
    AccountController
  • 接下来给Consent控制器的Index添加视图
  •  1 @using mvcCookieAuthSample.ViewModels
     2 @model  ConsentViewModel
     3 <h2>ConsentPage</h2>
     4 @*consent*@
     5 <div class="row page-header">
     6     <div class="col-sm-10">
     7         @if (!string.IsNullOrWhiteSpace(Model.ClientLogoUrl))
     8         {
     9             <div>
    10                 <img src="@Model.ClientLogoUrl" style="width:50px;height:50px" />
    11             </div>
    12         }
    13         <h1>@Model.ClientName</h1>
    14         <p>希望使用你的账户</p>
    15     </div>
    16 </div>
    17 @*客户端*@
    18 <div class="row">
    19     <div class="col-sm-8">
    20         <div asp-validation-summary="All" class="danger"></div>
    21         <form asp-action="Index" method="post">
    22             <input type="hidden" asp-for="ReturnUrl"/>
    23             @if (Model.IdentityScopes.Any())
    24             {
    25                 <div class="panel">
    26                     <div class="panel-heading">
    27                         <span class="glyphicon glyphicon-user"></span>
    28                         用户信息
    29                     </div>
    30                     <ul class="list-group">
    31                         @foreach (var scope in Model.IdentityScopes)
    32                         {
    33                             @Html.Partial("_ScopeListitem.cshtml", scope);
    34                         }
    35                     </ul>
    36                 </div>
    37             }
    38             @if (Model.ResourceScopes.Any())
    39             {
    40                 <ul class="list-group">
    41                     @foreach (var scope in Model.ResourceScopes)
    42                     {
    43                         @Html.Partial("_ScopeListitem.cshtml", scope);
    44                     }</ul>
    45             }
    46             <div>
    47                 <label>
    48                     <input type="checkbox" asp-for="RemeberConsent"/>
    49                     <strong>记住我的选择</strong>
    50                 </label>
    51             </div>
    52             <div>
    53                 <button name="button" value="yes" class="btn btn-primary"  autofocus>同意</button>
    54                 <button name="button" value="no">取消</button>
    55                 @if (!string.IsNullOrEmpty(Model.ClientUrl))
    56                 {
    57                     <a href="@Model.ClientUrl" class="pull-right btn btn-default">
    58                         <span class="glyphicon glyphicon-info-sign" ></span>
    59                         <strong>@Model.ClientUrl</strong>
    60                     </a>
    61                 }
    62             </div>
    63         </form>
    64     </div>
    65 </div>
    66 //这里用到了一个分部视图用来显示用户允许授权的身份资源和api资源
    67 @using mvcCookieAuthSample.ViewModels
    68 @model ScopeViewModel;
    69 <li>
    70     <label>
    71         <input type="checkbox"
    72                name="ScopesConsented"
    73                id="scopes_@Model.Name"
    74                value="@Model.Name"
    75                checked=@Model.Checked
    76                disabled=@Model.Required/>
    77         @if (Model.Required)
    78         {
    79             <input type="hidden" name="ScopesConsented" value="@Model.Name" />
    80         }
    81         <strong>@Model.Name</strong>
    82         @if (Model.Emphasize)
    83         {
    84             <span class="glyphicon glyphicon-exclamation-sign"></span>
    85         }
    86     </label>
    87     @if(string.IsNullOrEmpty(Model.Description))
    88     {
    89         <div>
    90             <label for="scopes_@Model.Name">@Model.Description</label>
    91         </div>
    92     }
    93 </li>
    Index.cshtml
  • 添加客户端,依旧添加一个mvc项目,配置startup,Home/Index action打上Authorize标签。
  •  1 public void ConfigureServices(IServiceCollection services)
     2         {
     3             services.AddAuthentication(options => {
     4                 options.DefaultScheme = "Cookies";
     5                 options.DefaultChallengeScheme = "oidc";//openidconnectservice
     6             })
     7             .AddCookie("Cookies")
     8             .AddOpenIdConnect("oidc",options=> {
     9                 options.SignInScheme = "Cookies";
    10                 options.Authority = "http://localhost:5000";    //设置认证服务器
    11                 options.RequireHttpsMetadata = false;
    12                 options.ClientId = "mvc";                       //openidconfig的配置信息
    13                 options.ClientSecret = "secret";
    14                 options.SaveTokens = true;
    15             });
    16             services.AddMvc();
    17         }
    18         public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    19         {
    20             if (env.IsDevelopment())
    21             {
    22                 app.UseDeveloperExceptionPage();
    23                 app.UseBrowserLink();
    24             }
    25             else
    26             {
    27                 app.UseExceptionHandler("/Home/Error");
    28             }
    29             app.UseStaticFiles();
    30             app.UseAuthentication();
    31             app.UseMvc(routes =>
    32             {
    33                 routes.MapRoute(
    34                     name: "default",
    35                     template: "{controller=Home}/{action=Index}/{id?}");
    36             });
    37         }
    客户端配置Startup

设置服务端端口5000,运行服务器端;设置客户端端口5001,运行客户端。我们可以看到,localhost:5001会跳转至认证服务器

然后看下Url=》

使用config配置的testuser登录系统,选择允许授权的身份权限。登录成功后看到我们的Claims

 

总结

  • 最后来总结一下
    用户访问客户端(5001端口程序),客户端将用户导向认证服务器(5000程序),用户选择允许授权的身份资源和api资源后台解析(这两个资源分别由Resources提供,resources 由IResourceStore解析returnurl后的Scopes提供),最后由ProfileService返回数条Claim。(查看ConsentService的各个方法)
posted @ 2018-02-01 11:24  3WLineCode  阅读(1738)  评论(2编辑  收藏  举报