认证授权:IdentityServer4 - 单点登录

前言

 上一篇文章介绍了IdentityServer4的各种授权模式,本篇继续介绍使用IdentityServer4实现单点登录效果。

单点登录(SSO)

 SSO( Single Sign-On ),中文意即单点登录,单点登录是一种控制多个相关但彼此独立的系统的访问权限,拥有这一权限的用户可以使用单一的ID和密码访问某个或多个系统从而避免使用不同的用户名或密码,或者通过某种配置无缝地登录每个系统。

 概括就是:一次登录,多处访问

案例场景:

 1、提供资源服务(WebApi):订单:Order(cz.Api.Order)、商品:Goods(cz.Api.Goods)……

 2、业务中存在多个系统:门户系统、订单系统、商品系统……

 3、实现用户登录门户后,跳转订单系统、商品系统时,不需要登录认证(单点登录效果)

一、环境准备:

 调整项目如下图结构:

 

 

 

  在身份认证项目(cz.IdentityServer)中InMemoryConfig中客户端列表中添加以下客户端内容:(其他内容同上一篇设置相同)

new Client
{
    ClientId = "main_client",
    ClientName = "Implicit Client",
    ClientSecrets = new [] { new Secret("secret".Sha256()) },
    AllowedGrantTypes = GrantTypes.Implicit,
    RedirectUris = { "http://localhost:5020/signin-oidc" },
    PostLogoutRedirectUris = { "http://localhost:5020/signout-callback-oidc" },
    //是否显示授权提示界面
    RequireConsent = true,
    AllowedScopes = {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile
    }
},
new Client
{
    ClientId = "order_client",
    ClientName = "Order Client",
    ClientSecrets = new [] { new Secret("secret".Sha256()) },
    AllowedGrantTypes = GrantTypes.Code,
    AllowedScopes = {
        "order","goods",
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile
    },
    RedirectUris = { "http://localhost:5021/signin-oidc" },
    PostLogoutRedirectUris = { "http://localhost:5021/signout-callback-oidc" },
    //是否显示授权提示界面
    RequireConsent = true,
},
new Client
{
    ClientId = "goods_client",
    ClientName = "Goods Client",
    ClientSecrets = new [] { new Secret("secret".Sha256()) },
    AllowedGrantTypes = GrantTypes.Code,
    RedirectUris = { "http://localhost:5022/signin-oidc" },
    PostLogoutRedirectUris = { "http://localhost:5022/signout-callback-oidc" },
    //是否显示授权提示界面
    RequireConsent = true,
    AllowedScopes = {
        "goods",
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile
    }
}

 

 

 

二、程序实现:

 1、订单、商品Api项目:

  a)订单API项目调整:添加Nuget包引用:

Install-Package IdentityServer4.AccessTokenValidation

 

  b)调整Statup文件:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        //IdentityServer
        services.AddMvcCore()
                .AddAuthorization();

        //配置IdentityServer
        services.AddAuthentication("Bearer")
                .AddIdentityServerAuthentication(options =>
                {
                    options.RequireHttpsMetadata = false; //是否需要https
                    options.Authority = $"http://localhost:5600";  //IdentityServer授权路径
                    options.ApiName = "order";  //需要授权的服务名称
                });
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseAuthentication();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

 

  c)添加控制器:OrderController 

namespace cz.Api.Order.Controllers
{
   [ApiController]
   [Route("[controller]")]
   [Authorize]    
  public class OrderController : ControllerBase { private static readonly string[] Summaries = new[] { "Order1", "Order2", "Order3", "Order4", "Order5", "Order6", "Order7", "Order8", "Order9", "Order10" }; private readonly ILogger<OrderController> _logger; public OrderController(ILogger<OrderController> logger) { _logger = logger; }      //模拟返回数据 [HttpGet] public IEnumerable<WeatherForecast> Get() { var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); } } }

 

  d)商品项目同步调整,调整Api和方法

 2、门户项目:

  添加Nuget引用:

Install-Package IdentityServer4.AccessTokenValidation
Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect

  a)调整HomeController如下内容:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }
    [Authorize]
    public IActionResult Index()
    {
        //模拟返回应用列表
        List<AppModel> apps = new List<AppModel>();
        apps.Add(new AppModel() { AppName = "Order Client", Url = "http://localhost:5021" });
        apps.Add(new AppModel() { AppName = "Goods Client", Url = "http://localhost:5022" });
        return View(apps);
    }

    [Authorize]
    public IActionResult Privacy()
    {
        return View();
    }
public IActionResult Logout()
    {
        return SignOut("oidc", "Cookies");
    }

}

 

  b)调整主页视图: 

@model List<AppModel>
@{
    ViewData["Title"] = "Home Page";
}
<style>
    .box-wrap {
        text-align: center;
        /*        background-color: #d4d4f5;*/
        overflow: hidden;
    }

        .box-wrap > div {
            width: 31%;
            padding-bottom: 31%;
            margin: 1%;
            border-radius: 10%;
            float: left;
            background-color: #36A1DB;
        }
</style>
<div class="text-center">
    <div class="box-wrap">
        @foreach (var item in Model)
        {
            <div class="box">
                <a href="@item.Url" target="_blank">@item.AppName</a>
            </div>
        }
    </div>
</div>

 

  c)调整Statup文件中ConfigureServices方法:    

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.Lax;
    });
    JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
    services.AddControllersWithViews();
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = "Cookies";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("Cookies")
    .AddOpenIdConnect("oidc", options =>
    {
        options.RequireHttpsMetadata = false;
        options.Authority = "http://localhost:5600";
        options.ClientId = "main_client";
        options.ClientSecret = "secret";
        options.ResponseType = "id_token";
        options.SaveTokens = true;
        options.GetClaimsFromUserInfoEndpoint = true;
        //事件
        options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents()
        {
            //远程故障
            OnRemoteFailure = context =>
            {
                context.Response.Redirect("/");
                context.HandleResponse();
                return Task.FromResult(0);
            },
            //访问拒绝
            OnAccessDenied = context =>
            {
                //重定向到指定页面
                context.Response.Redirect("/");
                //停止此请求的所有处理并返回给客户端
                context.HandleResponse();
                return Task.FromResult(0);
            },
        };
    });
}

 

 3、订单、商品客户端项目:

  添加Nuget引用:

Install-Package IdentityServer4.AccessTokenValidation
Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect

  a)修改HomeController内容如下:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }

    [Authorize]
    public IActionResult Index()
    {
        return View();
    }

    public async Task<IActionResult> PrivacyAsync()
    {
        var accessToken = await HttpContext.GetTokenAsync("access_token");
        var client = new HttpClient();
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        var content = await client.GetStringAsync("http://localhost:5601/order");

        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        var contentgoods = await client.GetStringAsync("http://localhost:5602/goods");

        ViewData["Json"] = $"Goods:{contentgoods}\r\n " +
            $"Orders:{content}";
        return View();
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }
    public IActionResult Logout()
    {
        return SignOut("oidc", "Cookies");
    }
}

  b)调整对应视图内容:


#####Home.cshtml
@{
    ViewData["Title"] = "Home Page";
}
@using Microsoft.AspNetCore.Authentication

<h2>Claims</h2>
<div class="text-center">
    <dl>
        @foreach (var claim in User.Claims)
        {
            <dt>@claim.Type</dt>
            <dd>@claim.Value</dd>
        }
    </dl>
</div>
<div class="text-center">
    <h2>Properties</h2>
    <dl>
        @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
        {
            <dt>@prop.Key</dt>
            <dd>@prop.Value</dd>
        }
    </dl>
</div>

#####Privacy.cshtml

@{
ViewData["Title"] = "API Result";
}
<h1>@ViewData["Title"]</h1>

<p>@ViewData["Json"]</p>

 

  c)Statup中设置客户端信息  

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.Lax;
    });
    JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
    services.AddControllersWithViews();
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = "Cookies";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("Cookies")
    .AddOpenIdConnect("oidc", options =>
    {
        options.RequireHttpsMetadata = false;
        options.Authority = "http://localhost:5600";
        options.ClientId = "order_client";
        options.ClientSecret = "secret";
        options.ResponseType = "code";
        options.SaveTokens = true;
        options.Scope.Add("order");
        options.Scope.Add("goods");
        options.GetClaimsFromUserInfoEndpoint = true;
        //事件
        options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents()
        {
            //远程故障
            OnRemoteFailure = context =>
            {
                context.Response.Redirect("/");
                context.HandleResponse();
                return Task.FromResult(0);
            },
            //访问拒绝
            OnAccessDenied = context =>
            {
                //重定向到指定页面
                context.Response.Redirect("/");
                //停止此请求的所有处理并返回给客户端
                context.HandleResponse();
                return Task.FromResult(0);
            },
        };
    });
}

 

 d)商品客户端调整按照以上内容调整类似。

三、演示效果:

   1、设置项目启动如下图:

 

    2、示例效果:

   

四、总结:

  通过以上操作,整理单点登录流程如下图:

    

  踩坑:当登录取消、授权提示拒绝时,总是跳转错误界面。

    解决办法:客户端定义时,定义事件:对访问拒绝添加处理逻辑。

//事件
options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents()
{
  //远程故障
  OnRemoteFailure = context =>
  {
    context.Response.Redirect("/");
    context.HandleResponse();
    return Task.FromResult(0);
  },
  //访问拒绝
  OnAccessDenied = context =>
  {
    //重定向到指定页面
    context.Response.Redirect("/");
    //停止此请求的所有处理并返回给客户端
    context.HandleResponse();
    return Task.FromResult(0);
  },
};

 

GitHub地址:https://github.com/cwsheng/IdentityServer.Demo.git

 

posted @ 2020-09-20 18:55  chaney1992  阅读(2681)  评论(4编辑  收藏  举报