zoukankan      html  css  js  c++  java
  • 06 Authorization Code Flow 实例

    原文:https://www.yuque.com/yuejiangliu/dotnet/ac0xok


    06 Authorization Code Flow 实例.mp4 (137.6 MB)

    使用 Authorization Code Flow 保护 ASP.NET Core MVC 客户端(为其做用户的身份认证),并访问被保护资源。

    简单说就是 MVC 做客户端,IdentityServer4 做身份认证和授权。

    一、OAuth 2.0 vs OpenID Connect

    image.png

    1、OAuth 2.0 - Authorization Code Grant

    image.png

    流程按字母先后顺序执行。

    2、OpenlD Connect - Authorization Code Flow

    image.png

    主要差别就是除了 Access Token,客户端还能从授权服务器获得 Id Token,进而通过它获得最终用户的相关信息。

    • D 通过前端浏览器的重定向完成
    • E 通过后端服务器间的通讯完成

    二、Authorization Code

    • 适用于机密客户端(Confidential Client)
    • 服务器端的 Web 应用
    • 对用户和客户端进行身份认证

    客户端类型:

    image.png

    三、Authorization Code 实战

    1、新建 MVC 项目

    修改 launchSettings.json 把端口改为 5002:

    {
      "profiles": {
        "MvcClient": {
          "commandName": "Project",
          "launchBrowser": true,
          "applicationUrl": "http://localhost:5002",
          "environmentVariables": {
            "ASPNETCORE_ENVIRONMENT": "Development"
          }
        }
      }
    }

    安装 IdentityModel NuGet 包。

    在 Startup 里面注册并启用身份认证中间件:

    public void ConfigureServices(IServiceCollection services)
    {
        ...
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    
        // 关闭 JWT Claim 映射
        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
    
        // 注册身份认证中间件
        services.AddAuthentication(options =>
            {
                // CookieAuthenticationDefaults.AuthenticationScheme == "Cookies"
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                // OpenIdConnectDefaults.AuthenticationScheme == "OpenIdConnect"
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
            {
                options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.Authority = "http://localhost:5000";
                options.RequireHttpsMetadata = false;
                options.ClientId = "mvc client";
                options.ClientSecret = "mvc secret";
                options.SaveTokens = true;
                options.ResponseType = "code";
                
                options.Scope.Clear();
                options.Scope.Add("api1");
                options.Scope.Add(OidcConstants.StandardScopes.OpenId);
                options.Scope.Add(OidcConstants.StandardScopes.Profile);
                options.Scope.Add(OidcConstants.StandardScopes.Email);
                options.Scope.Add(OidcConstants.StandardScopes.Phone);
                options.Scope.Add(OidcConstants.StandardScopes.Address);
                // 必须添加 OfflineAccess 才能获取到 Refresh Token
                options.Scope.Add(OidcConstants.StandardScopes.OfflineAccess);
            });
    }
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }
        // 要在添加 MVC 中间件之前添加 Authentication 到管道
        app.UseAuthentication();
    
        app.UseStaticFiles();
        ...
    }

    这些代码的具体作用可以参考官方文档 Adding User Authentication with OpenID Connect 中的解释

    最后在 HomeController 上标注 [Authorize] 以保护 HomeController。

    2、添加 MVC Client

    打开 Idp 项目,添加 MVC Client:

    // MVC client, authorization code
    new Client
    {
        ClientId = "mvc client",
        ClientName = "ASP.NET Core MVC Client",
    
        AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,
        ClientSecrets = { new Secret("mvc secret".Sha256()) },
    
        RedirectUris = { "http://localhost:5002/signin-oidc" },
    
        FrontChannelLogoutUri = "http://localhost:5002/signout-oidc",
        PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
        
        // 总是在 Id Token 里面包含所有 User Claims 信息
        AlwaysIncludeUserClaimsInIdToken = true,
    
        // 设为 True 即支持 Refresh Token
        AllowOfflineAccess = true, // offline_access
        
        AllowedScopes =
        {
            "api1",
            IdentityServerConstants.StandardScopes.OpenId,
            IdentityServerConstants.StandardScopes.Email,
            IdentityServerConstants.StandardScopes.Address,
            IdentityServerConstants.StandardScopes.Phone,
            IdentityServerConstants.StandardScopes.Profile
        }
    },

    3、运行项目

    依次启动 Idp、Api1 和 MVC Client 三个项目。

    由于 MVC Client 的 HomeController 被保护了,所以启动项目后会自动跳转到授权服务器进行身份认证。

    image.png

    身份认证完毕后跳转到 grant 界面:

    image.png

    同意后回到 MVC Client 的 Home 界面:

    image.png

    4、在 MVC Client 中获取各个 Token

    修改 HomeController 的 Privacy 方法,获取各个 Token:

    public async Task<IActionResult> Privacy()
    {
        var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
        var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
    
        var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
      
        return View();
    }

    注:修改 MVC Client 后每次重新调试记得手动清除浏览器 Cookie。

    通过 Preserve log 查看 Authorization Token:

    image.png

    5、通过 Fiddler 监视到两个往返

    image.png

    点下 Yes, Allow 的瞬间,可以通过 Fiddler 监视到两个往返。

    image.png

    1 - 身份认证请求(浏览器与授权服务器间通讯):

    HTTP/1.1 302 Found
    Location: https://server.example.com/authorize?
        response _type=code
        &scope=openid%20profile&20email
        &client_id=s6BhdRkqt3
        &state=af0ifjsldkj
        &redirect_uri=https$3A%2F%2Fclient.example.org%2Fcb

    1 - 身份认证请求的响应:

    HTTP/1.1 302 Found
    Location: https://client.example.org/cb?
        code=Splx10BeZQQYbYS6WxSbIA
        &state=af0ifjsldkj

    2 - Token 请求(客户端服务器与授权服务器间通讯):

    POST /token HTTP/1.1
    Host: server.example.com
    Content-Iype: application/x-www-form-urlencoded
    Authorization: Basic czzCaGRSa3FOMzpniDFmQmFOM2JW
    
    grant_type=authorization_code
        &code=Splx10BeZQQYbYS6WxSbIA
        &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb

    2 - Token 请求的响应:

    HTTP/1.1 200 OK
    Content-Type: application/json
    Cache-Control: no-store
    Pragma: no-cache
    
    {
      "access_token": "S1AV32hkKG",
      "token_type": "Bearer",
      "refresh_token": "8xLOxBtZp8",
      "expires_in": 3600,
      "id_token": "eyJhbGci0iJSUzI1NiIsImtpZCI6IjF10WdkazcifQ.ewogImlzc
        yI6ICJodHRwOi8vc2VyimVyLmV4YW1wbGUuY29tIiwKICJzdWIi0iAiM)Q4Mjg5
        NzYXMDAXIiwKICJh/Qi0iAiczZCaGRSa3FOMyIsCiAibm9uY2Ui0iAibi0wUz2
        fV3pBMK1qIiwKICJLeHAi01AXMzExM)gxOTcwLAogImlhdCI6IDEzMTEyODA5Nz
        AKEQ.ggwehZ1EuVLuxNuuIJKX V8a OMXzROEHR9R6jgdqrOOF4daGU96Sr P6g
        Jp6IcmD3HP990bi1PR3-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CU
        NgeGpe-gccMg4vfKjkMBFcGvnzZUN4 _KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpG
        QyHE51cMiKPXEEIQILVqOpC E2DzL7emopioaoZTF mO NOYzFC6g6EJbOEoRoS
        K5hoDalrcvRYLSrQAZZKf1yuVCyixEoV9GfNQC3_o3jzM2PAithfubEEBLuVWk4
        XUVrWOLrL10nx7RkKUSNXNHg-rvMzgg"
    }

    6、展示 Token

    修改 HomeController 的 Privacy 方法,展示 Token:

    public async Task<IActionResult> Privacy()
    {
        var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
        var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
    
        var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
    
        ViewData["accessToken"] = accessToken;
        ViewData["idToken"] = idToken;
        ViewData["refreshToken"] = refreshToken;
    
        return View();
    }

    Privacy 的 View:

    @{
        ViewData["Title"] = "Privacy Policy";
    }
    <h1>@ViewData["Title"]</h1>
    
    <h2>Access Token:</h2>
    <p>@ViewData["accessToken"]</p>
    
    <h2>Id Token:</h2>
    <p>@ViewData["idToken"]</p>
    
    <h2>Refresh Token:</h2>
    <p>@ViewData["refreshToken"]</p>
    
    <dl>
        @foreach (var claim in User.Claims)
        {
            <dt>@claim.Type</dt>
            <dd>@claim.Value</dd>
        }
    </dl>

    image.png

    7、使用 Access Token 访问 Api1 的资源

    先修改 MVC Client HomeController 的 Index Action:

    public async Task<IActionResult> Index()
    {
        var client = new HttpClient();
        var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/");
        if (disco.IsError) throw new Exception(disco.Error);
    
        var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
    
        client.SetBearerToken(accessToken);
    
        var response = await client.GetAsync("http://localhost:5001/identity");
        if (!response.IsSuccessStatusCode) throw new Exception(response.ReasonPhrase);
    
        var content = await response.Content.ReadAsStringAsync();
        return View("Index", content);
    }

    再修改 Index View:

    @model string
    
    @{
        ViewData["Title"] = "Home Page";
    }
    
    <div class="text-center">
        <h1 class="display-4">Api1 Resource Respose:</h1>
        <p>@Model</p>
    </div>

    效果:

    image.png

    8、单点登录

    用户登录后,再打开授权服务器可以看到 alice 处于登录状态(即会话被 Idp 项目保持了)。

    此时如果有其他 Web 应用请求授权,跳转到授权服务器后会发现 alice 已经登录了,它就无需登录直接跳转回去。整个过程就相当于是一个单点登录。

    image.png

    9、登出

    登出时既要登出 MVC 客户端,又要登出 IdentityServer4 用户会话。

    MVC Client _Layout 中添加 Logout 链接:

    <ul class="navbar-nav flex-grow-1">
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
        </li>
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </li>
        @if (User.Identity.IsAuthenticated)
        {
            <li class="nav-item">
                <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Logout">Logout</a>
            </li>
        }
    </ul>

    Logout Action:

    public async Task Logout()
    {
        // 登出当前网站的 Cookie
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        // 登出 IdentityServer4 的 Cookie
        await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
    }

    通过设置 Idp 项目 QuickstartAccountAccountOptions.cs 的 AutomaticRedirectAfterSignOut = true; 可以在登出后自动跳转回客户端。

  • 相关阅读:
    Wonderful hyperlinks of MVVM(ModelViewViewModel)Design Pattern Virus
    [Translation]Silverlight 4MVVM with Commanding and WCF RIA Services Virus
    微软企业库4.1学习笔记(一)开篇乱弹 Virus
    根据总用量计算每种包装规格的购买量和总价 Virus
    微软企业库4.1学习笔记(二)各功能之间的依赖关系以及对象创建 Virus
    silverlight+wcf:relation entity VS. relation entity's code Virus
    根据总用量计算每种包装规格的购买量和总价 后续篇(一)并且使得用户花费最少 Virus
    Team Build Workflow 资源汇总
    VSTF Build Workflow(3)Hello World!
    初探798
  • 原文地址:https://www.cnblogs.com/springsnow/p/13879469.html
Copyright © 2011-2022 走看看