入門教程: JS認證和WebAPI

本教程會介紹如何在前端JS程序中集成IdentityServer。因為所有的處理都在前端拨拓,我們會使用一個JS庫oidc-client-js, 來處理諸如獲取瘩燥,驗證tokens的工作轧粟。

本教程的代碼在這里.

本教程分為三大塊:

  • 在前端JS程序中使用IdentityServer進行認證
  • 在前端JS中調(diào)用API
  • 僚機如何在前端更新令牌,登出和檢查會話

第一部分 - 在前端JS程序中使用IdentityServer進行認證

第一部分首装,我們專注在如何前端認證创夜。我們準備了兩個項目,一個是JS前端程序仙逻,一個是IdentityServer.

創(chuàng)建JS前端程序

在Visual Studio中創(chuàng)建一個空Web應用驰吓。


create js app
create js app

注意項目的URL,后面需要在瀏覽器中使用:

js app url
js app url

創(chuàng)建IdentityServer 項目

在Visual Studio中創(chuàng)建另外一個空Web應用程序來托管IdentityServer.

create web app
create web app

切換到項目屬性系奉,啟用SSL:

set ssl
set ssl

提醒
不要忘了把Web程序的啟動URL改成https的鏈接(具體鏈接參看你項目的SSL URL).

譯者注: identityserver3不支持http的網(wǎng)站,必須有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分發(fā)。運行下面的命令安裝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"  //請檢查端口號,確保和你剛才創(chuàng)建的JS項目一樣
                },

                AllowedCorsOrigins = new List<string>
                {
                    "http://localhost:56668"
                },

                AllowAccessToAllScopes = true
            }
        };
    }
}

特別注意AllowedCorsOrigins屬性萌踱,上面代碼的設置葵礼,讓IdentityServer接受這個指定網(wǎng)站的認證請求。
譯者注: 考慮到安全性并鸵, 網(wǎng)站一般不接受不同域的請求鸳粉,這里是設置可以接受指定的跨域請求

popup.html會在后面詳細講解,這里你照樣填就好了.

備注 現(xiàn)在這個客戶端可以接受任何作用域(AllowAccessToAllScopes設置為true).在生產(chǎn)環(huán)境园担,必須通過AllowScopes來限制作用域范圍届谈。

配置IdentityServer - 用戶

接下來,我們在IdentityServer里硬編碼一些用戶--同樣的弯汰,這個可以通過一個簡單的C#類來實現(xiàn)艰山。生產(chǎn)環(huán)境中,我們應該從數(shù)據(jù)庫里獲取用戶信息蝙泼。 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調(diào)用舔腾,我們會創(chuàng)建我們自己的作用域溪胶。

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

添加Startup

IdentityServer是一個OWIN中間件,需要在Startup類中配置稳诚。這個教程中哗脖,我們會配置客戶端,用戶,作用域才避,認證證書和一些配置選項橱夭。
在生產(chǎn)環(huán)境需要從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支持,否則有一些內(nèi)嵌的資源無法被IIS裝載:

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

JS 客戶端- 設置

我們使用下面的第三方庫來簡化我們的JS客戶端開發(fā):

我們通過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 客戶端 - 認證

好了粉臊,現(xiàn)在零件已經(jīng)組裝好了,我們需要加一點邏輯代碼讓它動起來. 感謝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可以查詢?nèi)绾闻c這個IdentityServer通信屠凶, 并驗證token的有效性。
  • client_id 這是客戶端標識肆资,認證服務器用這個標識來區(qū)別不同的客戶端矗愧。
  • popup_redirect_uri 是使用signinPopup方法是的重定向URL。如果你不想用彈出框來登陸郑原,希望用戶能到主登錄界面登陸唉韭,那么你需要使用redirect_uri屬性和signinRedirect 方法。
  • response_type 定義響應類型犯犁,在我們的例子中属愤,我們只需要服務器返回身份令牌
  • scope 定義了我們要求的作用域
  • filterProtocolClaims 告訴oidc-client過濾掉OIDC協(xié)議內(nèi)部用的聲明信息,如: nonce, at_hash, iat, nbf, exp, aud, issidp

我們監(jiān)聽處理Login按鈕的單擊事件酸役,當用戶單擊登陸的時候住诸,打開登陸彈出框(signinPopup). signinPopup返回一個Promise驾胆。只有收到用戶信息并驗證通過后才會標記成功。

有兩種方式得到identityServer返回的數(shù)據(jù):

  • 從Promise 的成功(done)處理函數(shù)得到
  • userLoaded 事件的參數(shù)得到

這個例子中贱呐,我們通過events.addUserLoaded·掛載了userLoaded事件處理函數(shù)丧诺,把用戶信息保存到全局的user對象中。這個對象有:id_token,scopeprofile`等屬性奄薇, 這些屬性包含各種用戶具體的數(shù)據(jù)锅必。

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

new Oidc.UserManager().signinPopupCallback();

登陸內(nèi)部過程:在index.html頁面的UserManager實例會打開一個彈出框,然后把它重定向到登陸頁面惕艳。當identityServer認證好用戶搞隐,把用戶信息發(fā)回到彈出框,彈出框發(fā)現(xiàn)登陸已經(jīng)成功后自動關閉远搪。

代碼抄到這里劣纲,登陸可以工作啦:

login-popup
login-popup
login-complete
login-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應用不需要改养泡,因為我們申請了所有的作用域嗜湃。 但是在生產(chǎn)環(huán)境中,我們應該只返回用戶需要的作用域澜掩,這種情況下购披,客戶端代碼也需要修改。

完成上面的改動后肩榕,我們現(xiàn)在可以看到email信息啦:

login-email
login-email

第二部分 - 調(diào)用API

第二部分刚陡,我們演示如何從JS應用中調(diào)用受保護的API。
為了調(diào)用被保護的API株汉,除了身份令牌筐乳,我們還要從IdentityServer得到訪問令牌,并用這個訪問令牌調(diào)用被保護的API郎逃。

創(chuàng)建一個API項目

在Visual Studio中創(chuàng)建一個空應用程序.

create api
create api

API項目的URL需要指定為 http://localhost:60136.

配置API

在本教程哥童,我們將創(chuàng)建一個非常簡單的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項目無法啟動:
譯者注:在我翻譯的時候好像已經(jīng)解決這個問題了

api-update-microsoft-identity-tokens
api-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

現(xiàn)在讓我們創(chuàng)建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朵你。我們允許來自任何網(wǎng)站的跨域請求,在生產(chǎn)中揣非,我們需要限制一下抡医,改成只允許我們希望的網(wǎng)站來跨域請求。
API項目需要驗證令牌的有效性早敬,我們通過IdentityServer3.AccessTokenValidation包來實現(xiàn)忌傻。在指定Authority 屬性后,AccessTokenValidation會自動下載元數(shù)據(jù)并完成令牌驗證的設置。
2.2版本以后搞监,IdentityServer實現(xiàn)了introspection endpoint 來驗證令牌水孩。這個端點會進行作用域認證,比傳統(tǒng)的令牌驗證更安全琐驴。
最后是WebAPI配置俘种。我們使用AuthroizeAttribute來指定所有的API請求都需要認證。

現(xiàn)在我們來加上一個簡單的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
            }
        };
    }
}

新的作用域是資源作用域绝淡,也就是說它會在訪問令牌中體現(xiàn)宙刘。當然例子中的JS應用不需要修改,因為它請求了全部作用域牢酵,但是在生產(chǎn)環(huán)境中悬包,應該限制申請那些作用域。

更新 JS 應用

現(xiàn)在我們更新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-token
access-token

調(diào)用 API

拿到訪問令牌铁追,我們就可以在JS應用里調(diào)用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
        });
    });
});

代碼改好了,我們現(xiàn)在有一個調(diào)用API的按鈕和一個顯示API結果的Panel。
注意,訪問令牌會放到Authroization請求頭里。

登錄前調(diào)用,結果如下:

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

登陸后調(diào)用柒室,結果如下:

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

登陸前訪問API不脯,JS應用沒有得到訪問令牌复局,所以不會添加Authorization請求頭吝沫,那么訪問令牌驗證中間件不會介入辫愉。請求做為未認證的請求發(fā)送到API冀墨,全局特性AuthroizeAttribute會拒絕請求虫腋,返回`401未授權錯誤。

登陸后訪問API, 令牌驗證中間件在請求頭中發(fā)現(xiàn)了Authorization辟癌,把它傳給introspection端點驗證厂置,收到身份信息及包含的聲明。好了系草,請求帶著認證信息流向了Web API,全局特性AuthroizeAttribute約束滿足了能耻,具體的API成功調(diào)用赏枚。

Part 3 - 更新令牌,登出及檢查會話

現(xiàn)在JS應用可以登錄晓猛,可以調(diào)用受保護的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秒床绪。
現(xiàn)在你登陸JS應用后,過10秒鐘在訪問API其弊,你又會得到401未授權錯誤啦癞己。

更新令牌

我們將依賴oidc-client-js幫我們自動更新令牌
oidc-client-js內(nèi)部會記錄訪問令牌的過期時間,并在過期前向IdentityServer發(fā)送授權請求來獲取新的訪問令牌梭伐。按照prompt 設置 --默認設置為none, 在會話有效期內(nèi)痹雅,用戶不需要重新授權來得到訪問令牌--,這些動作是用戶不可見的糊识。IdentityServer會返回一個新的訪問令牌替代即將過期的舊令牌绩社。
下面是訪問令牌過期和更新的設置說明:

  • accessTokenExpiring 事件在過期前會激發(fā)
  • accessTokenExpiringNotificationTime 用來調(diào)整accessTokenExpiring激發(fā)時間.默認是過期前60 秒。
  • 另外一個是automaticSilentRenew赂苗,用來在令牌過期前自動更新令牌愉耙。
  • 最后 silent_redirect_uri 是指得到新令牌后需要重定向到的URL。

oidc-client-js更新令牌的大致步驟如下:
當令牌快過期的時候拌滋,oidc-client-js會創(chuàng)建一個不可見的iframe朴沿,并在其中啟動要給新的授權請求,如果請求成功,identityServer會讓iframe重定向到silent_redirect_uri指定的URL赌渣,這部分的的JS代碼會自動更新全局用戶信息魏铅,這樣主窗口就可以得到更新后的令牌。
理論講完了坚芜,我們現(xiàn)在來按照上述內(nèi)容改代碼:

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>

現(xiàn)在需要告訴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會觸發(fā)一個userLoaded事件沧竟,因為我們在前面已經(jīng)寫好了事件處理器,更新的數(shù)據(jù)會自動顯示在UI上千贯。
當失敗的時候屯仗,silentRenewError事件會觸發(fā)搞坝,我們可以訂閱這個事件來了解具體什么錯了搔谴。

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

我們把訪問令牌生存期設置為10秒,并告訴oidc-client-js過期前4秒更新令牌桩撮。
現(xiàn)在登陸以后敦第,每6秒會向identityserver請求更新訪問令牌一次。

登出

前端程序的登出和服務端程序的登出不一樣店量,比如芜果,你在瀏覽器里刷新頁面,訪問令牌就丟失了融师,你需要重新登陸右钾。但是當?shù)顷憦棾隹虼蜷_時,它發(fā)現(xiàn)你還有一個IdentityServer的有效會話Cookie旱爆,所以它不會問你要用戶名密碼舀射,反而立刻關閉自己。整個過程和自動后臺更新令牌差不多怀伦。
真正的登出意味著從IdentityServer登出脆烟,下次進入由IdentityServer保護的程序時,必須重新輸入用戶名密碼房待。
過程不復雜邢羔,我們只需要在登出按鈕事件里面調(diào)用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會被清除流椒。

logout
logout

注意敏簿,上面圖片顯示的是IdentityServer的頁面,不是JS應用的界面
上面的例子是通過主頁面登出镣隶,oidc-client-js提供了一種在彈出框中登出的方式极谊,和登錄差不多诡右,具體的信息可以參考 oidc-client-js的文檔.

檢查會話

JS應用的會話開始于我們從IdentityServer得到有效的身份令牌。IdentityServer自身也要維護一個會話管理轻猖,在響應授權請求的時候會返回一個session_state帆吻。關于OpenID Connect詳細規(guī)格說明,請參看這里.

有些情況下咙边,我們想知道用戶是否結束了IdentityServer上的回話猜煮,比如說,在另外一個應用程序中登出引起在IdentityServer上登出败许。檢查的方式是計算 session_state 的值. 如果它和IdentityServer發(fā)出來的一樣王带,那么說明用戶還處于登陸狀態(tài)。如果變化了市殷,用戶就有可能已經(jīng)登出了愕撰,這時候建議啟動一次后臺登陸請求(帶上prompt=none).如果成功,我們會得到一個新的身份令牌醋寝,也說明在IdentityServer上搞挣,用戶還是處于登陸狀態(tài)。失敗了音羞,則說明用戶已經(jīng)登出了囱桨,我們需要讓用戶重新登陸。

不幸的是嗅绰,JS應用自己沒辦法計算session_state的值舍肠,因為session_state是IdentityServer的cookie,我們的JS應用無法訪問窘面。OpenID的規(guī)格 要求裝載一個不可見的iframe調(diào)用IdentityServer的checksession端點翠语。JS應用和iframe可以通過postMessage API通信.

checksession 端點

這個端點監(jiān)聽來自postMessage的消息,按要求提供一個簡單的頁面民镜。傳送到端點的數(shù)據(jù)用來計算會話的哈希值啡专。如果和IdentityServer上的一樣,這個頁面返回unchanged值制圈,否則返回changed值们童。如果出現(xiàn)錯誤,則返回error.

實現(xiàn)會話檢查功能

好消息是oidc-client-js啥都會 O(∩_∩)O.
事實上鲸鹦,默認設置就會監(jiān)視會話狀態(tài)慧库。
相關的屬性是 monitorSession.

當用戶一登陸進來,oidc-clieng-js就會創(chuàng)建一個不可見的iframe馋嗜,這個iframe會裝載identityserver的會話檢查端點齐板。
每隔一段時間,這個iframe都會發(fā)送client id 和會話狀態(tài)給IdentityServer,并檢查收到的結果來判定會話是否已經(jīng)改變甘磨。

我們可以利用oidc-client-js的日志系統(tǒng)來認識整個過程是如何進行的橡羞。默認情況下oidc-client-js配置的是無操作(no-op)日志記錄器,我們可以簡單的讓它輸出到瀏覽器控制臺济舆。

Oidc.Log.logger = console;

為了減少日志量卿泽,我們增加訪問令牌的生存期。
更新令牌會產(chǎn)生大量日志滋觉,現(xiàn)在的設置沒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
        }
    };
}

最后第租,當用戶會話已經(jīng)改變,自動登錄也沒成功我纪。 UserManager會觸發(fā)一個userSinedOut事件慎宾,現(xiàn)在讓我們來處理這個事件。

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

現(xiàn)在重新回到JS應用宣羊,登出璧诵,打開瀏覽器控制臺,重新登陸仇冯; 你會發(fā)現(xiàn)每隔2秒鐘(默認設置)--oidc-client-js會檢查會話是否還是有效。

session-check
session-check

現(xiàn)在我們來證明它按照我們設想的那樣工作族操,我們打開一個新的瀏覽器tab苛坚,轉到JS應用并登陸。現(xiàn)在這兩個tab都在檢查會話狀態(tài)色难。從其中要給tab登出泼舱,你會看到另外一個tab會顯示如下窗口:
logout-event
logout-event

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市枷莉,隨后出現(xiàn)的幾起案子娇昙,更是在濱河造成了極大的恐慌,老刑警劉巖笤妙,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冒掌,死亡現(xiàn)場離奇詭異,居然都是意外死亡蹲盘,警方通過查閱死者的電腦和手機股毫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來召衔,“玉大人铃诬,你說我怎么就攤上這事。” “怎么了趣席?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵兵志,是天一觀的道長。 經(jīng)常有香客問我宣肚,道長毒姨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任钉寝,我火速辦了婚禮弧呐,結果婚禮上,老公的妹妹穿的比我還像新娘嵌纲。我一直安慰自己俘枫,他們只是感情好,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布逮走。 她就那樣靜靜地躺著鸠蚪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪师溅。 梳的紋絲不亂的頭發(fā)上茅信,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天,我揣著相機與錄音墓臭,去河邊找鬼蘸鲸。 笑死,一個胖子當著我的面吹牛窿锉,可吹牛的內(nèi)容都是我干的酌摇。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼嗡载,長吁一口氣:“原來是場噩夢啊……” “哼窑多!你這毒婦竟也來了?” 一聲冷哼從身側響起洼滚,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤埂息,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后遥巴,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體千康,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年挪哄,在試婚紗的時候發(fā)現(xiàn)自己被綠了吧秕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡迹炼,死狀恐怖砸彬,靈堂內(nèi)的尸體忽然破棺而出颠毙,到底是詐尸還是另有隱情,我是刑警寧澤砂碉,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布蛀蜜,位于F島的核電站,受9級特大地震影響增蹭,放射性物質(zhì)發(fā)生泄漏滴某。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一滋迈、第九天 我趴在偏房一處隱蔽的房頂上張望霎奢。 院中可真熱鬧,春花似錦饼灿、人聲如沸幕侠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晤硕。三九已至,卻和暖如春庇忌,著一層夾襖步出監(jiān)牢的瞬間舞箍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工皆疹, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留疏橄,地道東北人。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓墙基,卻偏偏與公主長得像软族,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子残制,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 術語http://www.tuicool.com/articles/2mQjIr 本文翻譯自IdentitySer...
    滅蒙鳥閱讀 8,344評論 1 13
  • IdentityServer4 本文源碼:https://github.com/forestGzh/Gzh.Ide...
    GongZH丶閱讀 9,043評論 0 1
  • Dribbble API 文檔 概述(Overview)架構(Schema)參數(shù)(Parameters)客戶端錯誤...
    喚靈者閱讀 3,398評論 4 6
  • 一說到REST篓足,我想大家的第一反應就是“啊绘趋,就是那種前后臺通信方式堪滨∽校”但是在要求詳細講述它所提出的各個約束习寸,以及如...
    時待吾閱讀 3,415評論 0 19
  • 1刽沾、今天寶寶值得夸獎的地方或讓你感動的一件事: A七十二般變化的 皮球 玩法 3-4歲的孩子耳贬,創(chuàng)造力一定是很驚人的...
    怪物爸爸閱讀 1,240評論 0 0