美文网首页程序员我爱编程小知识
入门教程: JS认证和WebAPI

入门教程: JS认证和WebAPI

作者: 灭蒙鸟 | 来源:发表于2016-10-07 12:26 被阅读4005次

    本教程会介绍如何在前端JS程序中集成IdentityServer。因为所有的处理都在前端,我们会使用一个JS库oidc-client-js, 来处理诸如获取,验证tokens的工作。

    本教程的代码在这里.

    本教程分为三大块:

    • 在前端JS程序中使用IdentityServer进行认证
    • 在前端JS中调用API
    • 僚机如何在前端更新令牌,登出和检查会话

    第一部分 - 在前端JS程序中使用IdentityServer进行认证

    第一部分,我们专注在如何前端认证。我们准备了两个项目,一个是JS前端程序,一个是IdentityServer.

    创建JS前端程序

    在Visual Studio中创建一个空Web应用。


    create js appcreate js app

    注意项目的URL,后面需要在浏览器中使用:

    js app urljs app url

    创建IdentityServer 项目

    在Visual Studio中创建另外一个空Web应用程序来托管IdentityServer.

    create web appcreate web app

    切换到项目属性,启用SSL:

    set sslset ssl

    提醒
    不要忘了把Web程序的启动URL改成https的链接(具体链接参看你项目的SSL URL).

    译者注: identityserver3不支持http的网站,必须有SSL保护

    增加IdentityServer

    IdentityServer is based on OWIN/Katana and distributed as a NuGet package. To add it to the newly created web host, install the following two packages:
    IdentityServer是一个OWIN/Katana的中间件,通过Nuget分发。运行下面的命令安装nuget包到IdentityServer托管程序。

    Install-Package Microsoft.Owin.Host.SystemWeb
    Install-Package IdentityServer3 
    

    配置IdentityServer的客户端

    IdentityServer需要知道客户端的一些信息,可以通过返回Client对象集合告诉IdentityServer.

    public static class Clients
    {
        public static IEnumerable<Client> Get()
        {
            return new[]
            {
                new Client
                {
                    Enabled = true,
                    ClientName = "JS Client",
                    ClientId = "js",
                    Flow = Flows.Implicit,
    
                    RedirectUris = new List<string>
                    {
                        "http://localhost:56668/popup.html"  //请检查端口号,确保和你刚才创建的JS项目一样
                    },
    
                    AllowedCorsOrigins = new List<string>
                    {
                        "http://localhost:56668"
                    },
    
                    AllowAccessToAllScopes = true
                }
            };
        }
    }
    

    特别注意AllowedCorsOrigins属性,上面代码的设置,让IdentityServer接受这个指定网站的认证请求。
    译者注: 考虑到安全性, 网站一般不接受不同域的请求,这里是设置可以接受指定的跨域请求

    popup.html会在后面详细讲解,这里你照样填就好了.

    备注 现在这个客户端可以接受任何作用域(AllowAccessToAllScopes设置为true).在生产环境,必须通过AllowScopes来限制作用域范围。

    配置IdentityServer - 用户

    接下来,我们在IdentityServer里硬编码一些用户--同样的,这个可以通过一个简单的C#类来实现。生产环境中,我们应该从数据库里获取用户信息。 IdentityServer也直接支持ASP.NET 的Identity和MembershipReboot.

    public static class Users
    {
        public static List<InMemoryUser> Get()
        {
            return new List<InMemoryUser>
            {
                new InMemoryUser
                {
                    Username = "bob",
                    Password = "secret",
                    Subject = "1",
    
                    Claims = new[]
                    {
                        new Claim(Constants.ClaimTypes.GivenName, "Bob"),
                        new Claim(Constants.ClaimTypes.FamilyName, "Smith"),
                        new Claim(Constants.ClaimTypes.Email, "bob.smith@email.com")
                    }
                }
            };
        }
    }
    

    配置IdentityServer - 作用域

    最后,我们加上作用域。 纯粹认证功能,我们只需要支持标准的OIDC作用域。将来我们授权API调用,我们会创建我们自己的作用域。

    public static class Scopes
    {
        public static List<Scope> Get()
        {
            return new List<Scope>
            {
                StandardScopes.OpenId,
                StandardScopes.Profile
            };
        }
    }
    

    添加Startup

    IdentityServer是一个OWIN中间件,需要在Startup类中配置。这个教程中,我们会配置客户端,用户,作用域,认证证书和一些配置选项。
    在生产环境需要从windows证书仓库或者其它安全的地方装载证书。简化起见,这个教程我们把证书文件直接保存在项目中。(演示用的证书可以从 这里下载.下载后直接添加到项目中,并把文件的Copy to Output Directory property 改为 Copy always).

    关于如何从Azure中装载证书,请看 这里.

    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseIdentityServer(new IdentityServerOptions
            {
                SiteName = "Embedded IdentityServer",
                SigningCertificate = LoadCertificate(),
    
                Factory = new IdentityServerServiceFactory()
                    .UseInMemoryUsers(Users.Get())
                    .UseInMemoryClients(Clients.Get())
                    .UseInMemoryScopes(Scopes.Get())
            });
        }
    
        private static X509Certificate2 LoadCertificate()
        {
            return new X509Certificate2(
                Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"bin\Config\idsrv3test.pfx"), "idsrv3test");
        }
    }
    

    完成上面的步骤后,一个全功能的IdentityServer就好了,你可以浏览探索端点来了解相信配置信息。

    探索探索

    RAMMFAR(Run All Managed Modules For All Requests )

    最后不要忘了在Web.config中添加RAMMFAR支持,否则有一些内嵌的资源无法被IIS装载:

    <system.webServer>
      <modules runAllManagedModulesForAllRequests="true" />
    </system.webServer>
    

    JS 客户端- 设置

    我们使用下面的第三方库来简化我们的JS客户端开发:

    我们通过npm-- the Node.js 前段包管理器--来安装这些前端库. 如果你还没有安装npm, 你可以按照 npm安装说明来安装npm.
    npm安装好了后,打开命令行(CMD),转到JSApplication目录下,运行:

    $ npm install jquery
    $ npm install bootstrap
    $ npm install oidc-client
    

    npm会把上述包按照到默认目录node_modules.

    重要npm包一般不会提交到源码仓库,如果你是从github仓库中克隆代码, 你需要在命令行(cmd)下,转到JSApplication目录,然后运行npm install来恢复这几个前端包。

    JSApplication项目,加入一个基础的Index.html文件:

    <!DOCTYPE html>
    <html>
    <head>
        <title>JS Application</title>
        <meta charset="utf-8" />
        <link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.css" />
        <style>
            .main-container {
                padding-top: 70px;
            }
    
            pre:empty {
                display: none;
            }
        </style>
    </head>
    <body>
        <nav class="navbar navbar-inverse navbar-fixed-top">
            <div class="container">
                <div class="navbar-header">
                    <a class="navbar-brand" href="#">JS Application</a>
                </div>
            </div>
        </nav>
    
        <div class="container main-container">
            <div class="row">
                <div class="col-xs-12">
                    <ul class="list-inline list-unstyled requests">
                        <li><a href="index.html" class="btn btn-primary">Home</a></li>
                        <li><button type="button" class="btn btn-default js-login">Login</button></li>
                    </ul>
                </div>
            </div>
    
            <div class="row">
                <div class="col-xs-12">
                    <div class="panel panel-default">
                        <div class="panel-heading">ID Token Contents</div>
                        <div class="panel-body">
                            <pre class="js-id-token"></pre>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    
        <script src="node_modules/jquery/dist/jquery.js"></script>
        <script src="node_modules/bootstrap/dist/js/bootstrap.js"></script>
        <script src="node_modules/oidc-client/dist/oidc-client.js"></script>
    </body>
    </html>
    

    popup.html 文件:

    <!DOCTYPE html>
    <html>
    <head>
        <title></title>
        <meta charset="utf-8" />
    </head>
    <body>
        <script src="node_modules/oidc-client/dist/oidc-client.js"></script>
    </body>
    </html>
    

    因为oidc-client可以打开一个弹出窗口让用户登录,所以我们做了一个popup页面

    JS 客户端 - 认证

    好了,现在零件已经组装好了,我们需要加一点逻辑代码让它动起来. 感谢UserManager JS类,它做了大部分肮脏的工作,我们只要一点简单代码就好。

    // helper function to show data to the user
    function display(selector, data) {
        if (data && typeof data === 'string') {
            data = JSON.parse(data);
        }
        if (data) {
            data = JSON.stringify(data, null, 2);
        }
    
        $(selector).text(data);
    }
    
    var settings = {
        authority: 'https://localhost:44300',
        client_id: 'js',
        popup_redirect_uri: 'http://localhost:56668/popup.html',
    
        response_type: 'id_token',
        scope: 'openid profile',
    
        filterProtocolClaims: true
    };
    
    var manager = new Oidc.UserManager(settings);
    var user;
    
    manager.events.addUserLoaded(function (loadedUser) {
        user = loadedUser;
        display('.js-user', user);
    });
    
    $('.js-login').on('click', function () {
        manager
            .signinPopup()
            .catch(function (error) {
                console.error('error while logging in through the popup', error);
            });
    });
    

    简单了解一下这些配置项:

    • authorityIdentityServer的入口URL. 通过这个URL,oidc-client可以查询如何与这个IdentityServer通信, 并验证token的有效性。
    • client_id 这是客户端标识,认证服务器用这个标识来区别不同的客户端。
    • popup_redirect_uri 是使用signinPopup方法是的重定向URL。如果你不想用弹出框来登陆,希望用户能到主登录界面登陆,那么你需要使用redirect_uri属性和signinRedirect 方法。
    • response_type 定义响应类型,在我们的例子中,我们只需要服务器返回身份令牌
    • scope 定义了我们要求的作用域
    • filterProtocolClaims 告诉oidc-client过滤掉OIDC协议内部用的声明信息,如: nonce, at_hash, iat, nbf, exp, aud, issidp

    我们监听处理Login按钮的单击事件,当用户单击登陆的时候,打开登陆弹出框(signinPopup). signinPopup返回一个Promise。只有收到用户信息并验证通过后才会标记成功。

    有两种方式得到identityServer返回的数据:

    • 从Promise 的成功(done)处理函数得到
    • userLoaded 事件的参数得到

    这个例子中,我们通过events.addUserLoaded·挂载了userLoaded事件处理函数,把用户信息保存到全局的user对象中。这个对象有:id_token,scopeprofile`等属性, 这些属性包含各种用户具体的数据。

    popup.html页面也需要配置下:

    new Oidc.UserManager().signinPopupCallback();
    

    登陆内部过程:在index.html页面的UserManager实例会打开一个弹出框,然后把它重定向到登陆页面。当identityServer认证好用户,把用户信息发回到弹出框,弹出框发现登陆已经成功后自动关闭。

    代码抄到这里,登陆可以工作啦:

    login-popuplogin-popup login-completelogin-complete

    你可以把filterProtocolClaims 属性设置为false,看看profile下面会多出那些声明?

    JS 应用 - 作用域

    我们定义了一个email声明,但是它好像没有在我们的身份令牌里面?这是因为我们的JS应用只要了openidprofile作用域,没有包括email声明。
    如果JS应用想拿到邮件地址,JS应用必须在UserManagerscopes属性中申请获取email作用域.

    在我们的例子中,我们首先需要修改IdentityServer包含Email作用域,代码如下:

    public static class Scopes
    {
        public static List<Scope> Get()
        {
            return new List<Scope>
            {
                StandardScopes.OpenId,
                StandardScopes.Profile,
                // New scope
                StandardScopes.Email
            };
        }
    }
    

    在这个教程中,JS应用不需要改,因为我们申请了所有的作用域。 但是在生产环境中,我们应该只返回用户需要的作用域,这种情况下,客户端代码也需要修改。

    完成上面的改动后,我们现在可以看到email信息啦:

    login-emaillogin-email

    第二部分 - 调用API

    第二部分,我们演示如何从JS应用中调用受保护的API。
    为了调用被保护的API,除了身份令牌,我们还要从IdentityServer得到访问令牌,并用这个访问令牌调用被保护的API。

    创建一个API项目

    在Visual Studio中创建一个空应用程序.

    create apicreate api

    API项目的URL需要指定为 http://localhost:60136.

    配置API

    在本教程,我们将创建一个非常简单的API
    首先安装下面的nuget包:

    Install-Package Microsoft.Owin.Host.SystemWeb -ProjectName Api
    Install-Package Microsoft.Owin.Cors -ProjectName Api
    Install-Package Microsoft.AspNet.WebApi.Owin -ProjectName Api
    Install-Package IdentityServer3.AccessTokenValidation -ProjectName Api
    

    注意 IdentityServer3.AccessTokenValidation 包间接依赖于System.IdentityModel.Tokens.Jwt.在编写本教程时,如果更新System.IdentityModel.Tokens.Jwt 到5.0.0会导致API项目无法启动:
    译者注:在我翻译的时候好像已经解决这个问题了

    api-update-microsoft-identity-tokensapi-update-microsoft-identity-tokens

    解决办法是把System.IdentityModel.Tokens.Jwt降级到4.0.2.xxx版本:

    Install-Package System.IdentityModel.Tokens.Jwt -ProjectName Api -Version 4.0.2.206221351
    

    现在让我们创建Startup类,并构建OWIN/Katana管道。

    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // Allow all origins
            app.UseCors(CorsOptions.AllowAll);
    
            // Wire token validation
            app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
            {
                Authority = "https://localhost:44300",
    
                // For access to the introspection endpoint
                ClientId = "api",
                ClientSecret = "api-secret",
    
                RequiredScopes = new[] { "api" }
            });
    
            // Wire Web API
            var httpConfiguration = new HttpConfiguration();
            httpConfiguration.MapHttpAttributeRoutes();
            httpConfiguration.Filters.Add(new AuthorizeAttribute());
    
            app.UseWebApi(httpConfiguration);
        }
    }
    

    代码很直观,但是我们还是仔细看看我们在管道中用了些什么:
    因为JS应用一般都要跨域,所以我们启用了CORS。我们允许来自任何网站的跨域请求,在生产中,我们需要限制一下,改成只允许我们希望的网站来跨域请求。
    API项目需要验证令牌的有效性,我们通过IdentityServer3.AccessTokenValidation包来实现。在指定Authority 属性后,AccessTokenValidation会自动下载元数据并完成令牌验证的设置。
    2.2版本以后,IdentityServer实现了introspection endpoint 来验证令牌。这个端点会进行作用域认证,比传统的令牌验证更安全。
    最后是WebAPI配置。我们使用AuthroizeAttribute来指定所有的API请求都需要认证。

    现在我们来加上一个简单的API方法:

    [Route("values")]
    public class ValuesController : ApiController
    {
        private static readonly Random _random = new Random();
    
        public IEnumerable<string> Get()
        {
            var random = new Random();
    
            return new[]
            {
                _random.Next(0, 10).ToString(),
                _random.Next(0, 10).ToString()
            };
        }
    }
    

    更新identityServer 配置

    我们在IdentityServer项目中的Scopes增加一个api作用域:

    public static class Scopes
    {
        public static List<Scope> Get()
        {
            return new List<Scope>
            {
                StandardScopes.OpenId,
                StandardScopes.Profile,
                StandardScopes.Email,
    
                // New scope registration
                new Scope
                {
                    Name = "api",
    
                    DisplayName = "Access to API",
                    Description = "This will grant you access to the API",
    
                    ScopeSecrets = new List<Secret>
                    {
                        new Secret("api-secret".Sha256())
                    },
    
                    Type = ScopeType.Resource
                }
            };
        }
    }
    

    新的作用域是资源作用域,也就是说它会在访问令牌中体现。当然例子中的JS应用不需要修改,因为它请求了全部作用域,但是在生产环境中,应该限制申请那些作用域。

    更新 JS 应用

    现在我们更新JS应用,申请新的api作用域

    var settings = {
        authority: 'https://localhost:44300',
        client_id: 'js',
        popup_redirect_uri: 'http://localhost:56668/popup.html',
    
        // We add `token` to specify we expect an access token too
        response_type: 'id_token token',
        // We add the new `api` scope to the list of requested scopes
        scope: 'openid profile email api',
    
        filterProtocolClaims: true
    };
    

    修改包括:

    • 一个新的用于显示访问令牌的Panel
    • 更新response_type来同时请求身份令牌和访问令牌
    • 请求api作用域
      访问令牌通过access_token属性获取,过期时间放在expires_at属性上。oidc-client会处理签名证书,令牌验证等麻烦的部分,我们不需要编写任何代码。

    登陆以后我们会得到下面的信息:

    access-tokenaccess-token

    调用 API

    拿到访问令牌,我们就可以在JS应用里调用API了。

    [...]
    <div class="container main-container">
        <div class="row">
            <div class="col-xs-12">
                <ul class="list-inline list-unstyled requests">
                    <li><a href="index.html" class="btn btn-primary">Home</a></li>
                    <li><button type="button" class="btn btn-default js-login">Login</button></li>
                    <!-- New button to trigger an API call -->
                    <li><button type="button" class="btn btn-default js-call-api">Call API</button></li>
                </ul>
            </div>
        </div>
    
        <div class="row">
            <!-- Make the existing sections 6-column wide -->
            <div class="col-xs-6">
                <div class="panel panel-default">
                    <div class="panel-heading">User data</div>
                    <div class="panel-body">
                        <pre class="js-user"></pre>
                    </div>
                </div>
            </div>
    
            <!-- And add a new one for the result of the API call -->
            <div class="col-xs-6">
                <div class="panel panel-default">
                    <div class="panel-heading">API call result</div>
                    <div class="panel-body">
                        <pre class="js-api-result"></pre>
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    [...]
    $('.js-call-api').on('click', function () {
        var headers = {};
        if (user && user.access_token) {
            headers['Authorization'] = 'Bearer ' + user.access_token;
        }
    
        $.ajax({
            url: 'http://localhost:60136/values',
            method: 'GET',
            dataType: 'json',
            headers: headers
        }).then(function (data) {
            display('.js-api-result', data);
        }).catch(function (error) {
            display('.js-api-result', {
                status: error.status,
                statusText: error.statusText,
                response: error.responseJSON
            });
        });
    });
    

    代码改好了,我们现在有一个调用API的按钮和一个显示API结果的Panel。
    注意,访问令牌会放到Authroization请求头里。

    登录前调用,结果如下:

    api-without-access-tokenapi-without-access-token

    登陆后调用,结果如下:

    api-with-access-tokenapi-with-access-token

    登陆前访问API,JS应用没有得到访问令牌,所以不会添加Authorization请求头,那么访问令牌验证中间件不会介入。请求做为未认证的请求发送到API,全局特性AuthroizeAttribute会拒绝请求,返回`401未授权错误。

    登陆后访问API, 令牌验证中间件在请求头中发现了Authorization,把它传给introspection端点验证,收到身份信息及包含的声明。好了,请求带着认证信息流向了Web API,全局特性AuthroizeAttribute约束满足了,具体的API成功调用。

    Part 3 - 更新令牌,登出及检查会话

    现在JS应用可以登录,可以调用受保护的API了。但是,令牌一旦过期,受保护的API又用不了啦。
    好消息是,oidc-token-manager可以配置成在令牌过期前来自动更新访问令牌,无需用户介入。

    过期的令牌

    首先我们来看看如何让令牌过期,我们必须缩短过期时间,过期时间是基于客户端的一个设置项,我们编辑IdentityServer中的Clients类。

    public static class Clients
    {
        public static IEnumerable<Client> Get()
        {
            return new[]
            {
                new Client
                {
                    Enabled = true,
                    ClientName = "JS Client",
                    ClientId = "js",
                    Flow = Flows.Implicit,
    
                    RedirectUris = new List<string>
                    {
                        "http://localhost:56668/popup.html"
                    },
    
                    AllowedCorsOrigins = new List<string>
                    {
                        "http://localhost:56668"
                    },
    
                    AllowAccessToAllScopes = true,
                    AccessTokenLifetime = 10
                }
            };
        }
    }
    

    访问令牌过期时间默认是1小时,我们把它改成10秒。
    现在你登陆JS应用后,过10秒钟在访问API,你又会得到401未授权错误啦。

    更新令牌

    我们将依赖oidc-client-js帮我们自动更新令牌
    oidc-client-js内部会记录访问令牌的过期时间,并在过期前向IdentityServer发送授权请求来获取新的访问令牌。按照prompt 设置 --默认设置为none, 在会话有效期内,用户不需要重新授权来得到访问令牌--,这些动作是用户不可见的。IdentityServer会返回一个新的访问令牌替代即将过期的旧令牌。
    下面是访问令牌过期和更新的设置说明:

    • accessTokenExpiring 事件在过期前会激发
    • accessTokenExpiringNotificationTime 用来调整accessTokenExpiring激发时间.默认是过期前60 秒。
    • 另外一个是automaticSilentRenew,用来在令牌过期前自动更新令牌。
    • 最后 silent_redirect_uri 是指得到新令牌后需要重定向到的URL。

    oidc-client-js更新令牌的大致步骤如下:
    当令牌快过期的时候,oidc-client-js会创建一个不可见的iframe,并在其中启动要给新的授权请求,如果请求成功,identityServer会让iframe重定向到silent_redirect_uri指定的URL,这部分的的JS代码会自动更新全局用户信息,这样主窗口就可以得到更新后的令牌。
    理论讲完了,我们现在来按照上述内容改代码:

    var settings = {
        authority: 'https://localhost:44300',
        client_id: 'js',
        popup_redirect_uri: 'http://localhost:56668/popup.html',
        // Add the slient renew redirect URL
        silent_redirect_uri: 'http://localhost:56668/silent-renew.html'
    
        response_type: 'id_token token',
        scope: 'openid profile email api',
    
        // Add expiration nofitication time
        accessTokenExpiringNotificationTime: 4,
        // Setup to renew token access automatically
        automaticSilentRenew: true,
    
        filterProtocolClaims: true
    };
    

    silent_redirect_uri需要一个页面来处理更新用户信息,代码如下:

    <!DOCTYPE html>
    <html>
    <head>
        <title></title>
        <meta charset="utf-8" />
    </head>
    <body>
        <script src="node_modules/oidc-client/dist/oidc-client.js"></script>
        <script>
            new Oidc.UserManager().signinSilentCallback();
        </script>
    </body>
    </html>
    

    现在需要告诉IdentityServer,新的重定向地址也是合法的。

    public static class Clients
    {
        public static IEnumerable<Client> Get()
        {
            return new[]
            {
                new Client
                {
                    Enabled = true,
                    ClientName = "JS Client",
                    ClientId = "js",
                    Flow = Flows.Implicit,
    
                    RedirectUris = new List<string>
                    {
                        "http://localhost:56668/popup.html",
                        // The new page is a valid redirect page after login
                        "http://localhost:56668/silent-renew.html"
                    },
    
                    AllowedCorsOrigins = new List<string>
                    {
                        "http://localhost:56668"
                    },
    
                    AllowAccessToAllScopes = true,
                    AccessTokenLifetime = 70
                }
            };
        }
    }
    

    当更新成功,UserManager会触发一个userLoaded事件,因为我们在前面已经写好了事件处理器,更新的数据会自动显示在UI上。
    当失败的时候,silentRenewError事件会触发,我们可以订阅这个事件来了解具体什么错了。

    manager.events.addSilentRenewError(function (error) {
        console.error('error while renewing the access token', error);
    });
    

    我们把访问令牌生存期设置为10秒,并告诉oidc-client-js过期前4秒更新令牌。
    现在登陆以后,每6秒会向identityserver请求更新访问令牌一次。

    登出

    前端程序的登出和服务端程序的登出不一样,比如,你在浏览器里刷新页面,访问令牌就丢失了,你需要重新登陆。但是当登陆弹出框打开时,它发现你还有一个IdentityServer的有效会话Cookie,所以它不会问你要用户名密码,反而立刻关闭自己。整个过程和自动后台更新令牌差不多。
    真正的登出意味着从IdentityServer登出,下次进入由IdentityServer保护的程序时,必须重新输入用户名密码。
    过程不复杂,我们只需要在登出按钮事件里面调用UserManagersignoutRedirect方法,当然,我们也需要在IdentityServer注册登出重定向url:

    public static class Clients
    {
        public static IEnumerable<Client> Get()
        {
            return new[]
            {
                new Client
                {
                    Enabled = true,
                    ClientName = "JS Client",
                    ClientId = "js",
                    Flow = Flows.Implicit,
    
                    RedirectUris = new List<string>
                    {
                        "http://localhost:56668/popup.html",
                        "http://localhost:56668/silent-renew.html"
                    },
    
                    // Valid URLs after logging out
                    PostLogoutRedirectUris = new List<string>
                    {
                        "http://localhost:56668/index.html"
                    },
    
                    AllowedCorsOrigins = new List<string>
                    {
                        "http://localhost:56668"
                    },
    
                    AllowAccessToAllScopes = true,
                    AccessTokenLifetime = 70
                }
            };
        }
    
    [...]
    <div class="row">
        <div class="col-xs-12">
            <ul class="list-inline list-unstyled requests">
                <li><a href="index.html" class="btn btn-primary">Home</a></li>
                <li><button type="button" class="btn btn-default js-login">Login</button></li>
                <li><button type="button" class="btn btn-default js-call-api">Call API</button></li>
                <!-- New logout button -->
                <li><button type="button" class="btn btn-danger js-logout">Logout</button></li>
            </ul>
        </div>
    </div>
    
    var settings = {
        authority: 'https://localhost:44300',
        client_id: 'js',
        popup_redirect_uri: 'http://localhost:56668/popup.html',
        silent_redirect_uri: 'http://localhost:56668/silent-renew.html',
        // Add the post logout redirect URL
        post_logout_redirect_uri: 'http://localhost:56668/index.html',
    
        response_type: 'id_token token',
        scope: 'openid profile email api',
    
        accessTokenExpiringNotificationTime: 4,
        automaticSilentRenew: true,
    
        filterProtocolClaims: true
    };
    [...]
    $('.js-logout').on('click', function () {
        manager
            .signoutRedirect()
            .catch(function (error) {
                console.error('error while signing out user', error);
            });
    });
    

    当点击logout按钮时,用户会重定向到IdentityServer,所以回话cookie会被清除。

    logoutlogout

    注意,上面图片显示的是IdentityServer的页面,不是JS应用的界面
    上面的例子是通过主页面登出,oidc-client-js提供了一种在弹出框中登出的方式,和登录差不多,具体的信息可以参考 oidc-client-js的文档.

    检查会话

    JS应用的会话开始于我们从IdentityServer得到有效的身份令牌。IdentityServer自身也要维护一个会话管理,在响应授权请求的时候会返回一个session_state。关于OpenID Connect详细规格说明,请参看这里.

    有些情况下,我们想知道用户是否结束了IdentityServer上的回话,比如说,在另外一个应用程序中登出引起在IdentityServer上登出。检查的方式是计算 session_state 的值. 如果它和IdentityServer发出来的一样,那么说明用户还处于登陆状态。如果变化了,用户就有可能已经登出了,这时候建议启动一次后台登陆请求(带上prompt=none).如果成功,我们会得到一个新的身份令牌,也说明在IdentityServer上,用户还是处于登陆状态。失败了,则说明用户已经登出了,我们需要让用户重新登陆。

    不幸的是,JS应用自己没办法计算session_state的值,因为session_state是IdentityServer的cookie,我们的JS应用无法访问。OpenID的规格 要求装载一个不可见的iframe调用IdentityServer的checksession端点。JS应用和iframe可以通过postMessage API通信.

    checksession 端点

    这个端点监听来自postMessage的消息,按要求提供一个简单的页面。传送到端点的数据用来计算会话的哈希值。如果和IdentityServer上的一样,这个页面返回unchanged值,否则返回changed值。如果出现错误,则返回error.

    实现会话检查功能

    好消息是oidc-client-js啥都会 O(∩_∩)O.
    事实上,默认设置就会监视会话状态。
    相关的属性是 monitorSession.

    当用户一登陆进来,oidc-clieng-js就会创建一个不可见的iframe,这个iframe会装载identityserver的会话检查端点。
    每隔一段时间,这个iframe都会发送client id 和会话状态给IdentityServer,并检查收到的结果来判定会话是否已经改变。

    我们可以利用oidc-client-js的日志系统来认识整个过程是如何进行的。默认情况下oidc-client-js配置的是无操作(no-op)日志记录器,我们可以简单的让它输出到浏览器控制台。

    Oidc.Log.logger = console;
    

    为了减少日志量,我们增加访问令牌的生存期。
    更新令牌会产生大量日志,现在的设置没6秒要来一次,我们都没有时间来详细检查日志。所以我们把它改成1分钟。

    public static class Clients
    {
    public static IEnumerable<Client> Get()
    {
        return new[]
        {
            new Client
            {
                Enabled = true,
                ClientName = "JS Client",
                ClientId = "js",
                Flow = Flows.Implicit,
    
                RedirectUris = new List<string>
                {
                    "http://localhost:56668/popup.html",
                    "http://localhost:56668/silent-renew.html"
                },
    
                PostLogoutRedirectUris = new List<string>
                {
                    "http://localhost:56668/index.html"
                },
    
                AllowedCorsOrigins = new List<string>
                {
                    "http://localhost:56668"
                },
    
                AllowAccessToAllScopes = true,
                // Access token lifetime increased to 1 minute
                AccessTokenLifetime = 60
            }
        };
    }
    

    最后,当用户会话已经改变,自动登录也没成功。 UserManager会触发一个userSinedOut事件,现在让我们来处理这个事件。

    manager.events.addUserSignedOut(function () {
        alert('The user has signed out');
    });
    

    现在重新回到JS应用,登出,打开浏览器控制台,重新登陆; 你会发现每隔2秒钟(默认设置)--oidc-client-js会检查会话是否还是有效。

    session-checksession-check
    现在我们来证明它按照我们设想的那样工作,我们打开一个新的浏览器tab,转到JS应用并登陆。现在这两个tab都在检查会话状态。从其中要给tab登出,你会看到另外一个tab会显示如下窗口:
    logout-eventlogout-event

    相关文章

      网友评论

        本文标题:入门教程: JS认证和WebAPI

        本文链接:https://www.haomeiwen.com/subject/iocsyttx.html