从零搭建一个IdentityServer——会话管理与登出

  在上一篇文章中我们介绍了单页应用是如何使用IdentityServer完成身份验证的,并且在讲到静默登录以及会话监听的时候都提到会话(Session)这一概念,会话指的是用户与系统之间交互过程,反过来说就是用户与系统之间交互的状态就保存在会话(Session)中,对于HTTP协议来说,由于它本身是无状态的,所以为了能够记录用户访问系统的状态,一般使用Cookie来存放会话信息。但是现在我们需要保存的是与IdentityServer之间的会话,对于单页应用来说它一般会存在跨域问题,那IdentityServer是如何处理跨域来完成会话管理的呢?同时IdentityServer4又提供了哪些与登录登出相关的特性?本文就从会话管理开始来一一介绍。
  本文内容有:

会话管理

  首先会话本身有两个主体,即服务器和客户端,服务端就是identityServer本身,它是一个asp.net core应用程序,那么实际上它的会话机制就和普通的asp.net core应用程序是一致的,通过cookie来保存相应会话的id或信息。
  下图为登录IdentityServer后浏览器端存储的会话信息和身份信息:
  而对于客户端来说,我们知道IdentityServer4实际上是OpenIDConnect(OIDC)协议的一个实现,而OIDC协议本身是没有会话管理这一特性的,它的出现实际上是在一个补充协议中:https://openid.net/specs/openid-connect-session-1_0.html,该协议约定了客户端如何对服务端的会话信息进行管理,而协议的主要内容是以下几个点:
  • 协议定义:如何持续监控终端用户在OpenID Provider(OP,Identity Server)上提供的会话信息,以便于终端用户登出OpenID Provider(OP,IdentityServer)时能够同时登出客户端(Relying Party)。
  关于OP(IdentityServer)和RP(client)见下图:
  
  简单来说就是上一篇文章演示的“会话监控”内容,当用户直接从IdentityServer直接登出时,客户端本身能够感知到并作出相应动作(客户端登出)。
  • iframe:一个HTML的标签,它代表一个内嵌的HTML文档,如果在HTML使用iframe那就是文档中包含另一个文档,iframe可以通过src属性来设置包含文档的url地址。当iframe设置的url与主文档的url不同域时,可以使用iframe的postmessage方法实现跨域通信。
  关于iframe及postmessage可参考:https://blog.csdn.net/tang_yi_/article/details/79401280
  • RP iframe:位于客户端(Relying Party, RP)中的一个iframe,这个iframe的作用是用于向OP iframe发送及接收信息,发送的信息是用于告知OP iframe进行会话检查,接收的信息是OP iframe完成会话检查后的结果。
  下图是oidc-client.js中用于创建RP iframe的代码:
  
  下图为使用RP iframe向OP iframe发送信息的代码:
  
  下图为接收到OP iframe会话验证结果消息后的处理代码:
  
  • OP iframe:一个由OpenID Provider(OP,IdentityServer)提供的,位于客户端(Relying Party, RP)中的一个iframe,它的作用是与IdentityServer同域,保存于IdentityServer的会话信息,并提供检查接口(基于postmessage)的iframe。
  当用户身份验证成功后,oidc-client会根据配置信息来访问获取OP iframe:
  
  OP iframe请求:
  
  下图为OP iframe中监听RP iframe会话检查消息,完成检查并返回消息结果的代码:
  
  会话检查是对用户数据中包含的会话状态(session_state)信息进行核对,会话状态(session_state)信息分为两个部分,它们用“.”分隔,前部分是客户端id、客户端域名、会话id加盐计算出来的哈希值,后部分是哈希计算使用的盐(salt)。
  
  下图为会话检查的具体逻辑,获取当前的会话id并进行哈希计算后与用户信息中的哈希值进行核对,如果不一致那么认为会话发生变化。
  
  发生变化后oidc-client会自动发起授权请求来确认新会话的信息,这个也就是上一篇文章登出后发起的请求返回需要登录的原因:
  从以上内容看来oidc协议的会话管理主要是通过iframe完成的。
  下图为单页应用完成登录后发起静默登录时候的页面信息:
  
  图中存在两个iframe,第一个是OP iframe包含了会话检查相关内容,第二个是发起静默登录时,创建的一个指向授权终结点的iframe,通过跨域完成登录,需要注意的是由于RP iframe是通过js代码创建的,所以无法在页面代码中找到。
  到此为止我们了解到的仅仅是会话管理在单页应用中实现的登录与登出功能,通过会话管理它可以将浏览器与客户端(RP)及授权服务器(OP)之间的关系联系起来,简单来说就是当浏览器与授权服务器(OP)会话中断时客户端(RP)程序能够知道(会话信息改变),同时如果浏览器与客户端(RP)会话中断时授权服务器(OP)也能知道(先清除客户端身份信息,然后跳转到授权服务器登出界面)。
其次还有一个特点就是由于OIDC的会话管理协议是使用iframe来完成跨域会话检查,虽然默认检查频率是2秒一次,但是它不需要向授权服务器发送任何请求即可完成检查,所以可以节省大量的网络资源和服务器资源。
  但最后看来这个会话管理协议只适用于单页应用来完成相关功能,但是对于web应用来说,使用单页方式实现的仅仅是一部分,其它方式是如何处理客户端(RP)与授权服务器(OP)之间的登录联系的呢?

前端登出

  OIDC前端登出协议(OpenID Connect Front-Channel Logout),这个协议提供了一种登出的机制,该机制是通过浏览器的前端技术来与被登出的客户端(RP)/服务器(OP)建立通信,不再需要iframe就可以实现相关登出功能,具体协议内容参见:https://openid.net/specs/openid-connect-frontchannel-1_0.html
  接下来我们就通过asp.net core应用程序来演示一下这个协议是如何完成前端登出的。

授权服务器(OP)登出联动客户端(RP)

  1. API项目中添加一个登出页面
  API项目实际上就是我们的客户端(RP),当前的例子就是通过在该应用上添加一个登出页面来完成授权服务器登出后通知客户端登出的功能。
  注:asp.net core api项目实际上是不包含页面的,此处仅为了方便通过api项目中添加Razor页面来完成演示。
  首先添加一个Razor页面的布局:
  完成后获得相关的目录结构和必要文件:
  
  添加一个登出页面:
  
  后端代码,代码非常简单,就是通过get方法访问该页面时就直接进行登出操作:
  
  最后在Startup文件中添加Razor Page的服务和路由:
  

   

  然后运行程序即可访问到代码了:
  
  2. 授权服务器中创建一个前端登出页面,同时对Identity登出页面改造:
  在本系列文章前面我们通过IdentityServer4集成asp.net core identity实现了用户的登录登出功能,并且在使用中也暂时没发现任何问题,可以满足基础的授权服务器的登录和登出,但是如果要实现登出联动,那么就需要进行一些改造。
  主要改造有下面几个步骤:
  1)添加一个前端登出页面:
  
  2)对前端登出的Razor Page的后端Model中添加三个字段,并且用特性标明它们从Query中获取:
  
  3)在前端登出的Razor Page的前端代码中添加以下代码:
  
  4)修改Identity登出页面的后端Post请求处理方法:
  
  3. 修改客户端数据,添加uri(客户端新增的登出地址):
  
  4. 验证登出联动:
  首先通过IdentityServer完成身份验证,并可访问受保护资源:
  
  然后开启新的选项卡访问IdentityServer的登出页面,此时因为客户端程序是通过客户端完成了授权服务器的身份验证,在浏览器会话信息保存期间,它默认是登录状态:
  最后我们点击登出链接,程序将携带相关参数跳转到我们添加的前端登出页面:
  现在我们再去刷新受保护资源时得到以下结果,它跳转到授权服务器的登录页面了,这意味着我们在授权服务器(OP)登出的时候,客户端(RP)同时也完成了登出:

原理简析

  它们是如何完成联动登出的呢?我们首先来分析一下相关主体有哪些:
  • 客户端(RP)登出页面:访问该页面即可完成