認證授權:IdentityServer4 - 單點登入

cwsheng發表於2020-09-20

前言

 上一篇文章介紹了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

 

相關文章