• 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="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的各个方法)
  • 相关阅读:
    入门级: WinForm 下的 ComboBox,ListBox 的使用 (一)
    C#:谨慎 DateTime.Now 带来的危险
    HTML5 小游戏审核通过,请各位有兴趣的朋友帮忙投票!谢谢。
    基于fpga的单线激光雷达数据处理
    左右法则-复杂指针解析
    智能指针(auto_ptr和shared_ptr) 转
    iphone游戏引擎
    C++对象和内存
    让你的代码变的更加强大
    Class Loader
  • 原文地址:https://www.cnblogs.com/liumengchen-boke/p/8397883.html
Copyright © 2020-2023  润新知