从零搭建一个IdentityServer——单页应用身份验证

  • A+
所属分类:.NET技术
摘要

     

  上一篇文章我们介绍了Asp.net core中身份验证的相关内容,并通过下图描述了身份验证及授权的流程:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  注:改流程图进行过修改,第三方用户名密码登陆后并不是直接获得code/id_token/access_token,而是登录后可以访问identityServer中受保护的资源(Authorize Endpoint),通过发起身份验证请求来实现授权码流程、隐式流程及混合流程来完成token的获取,它与直接通过用户名密码来获取token的Oauth2.0 Password GrantType方式是不一样的。
  在asp.net core应用程序中,通过授权码流程可以使用第三方(IdentityServer)的用户名密码,经过一系列的token、userinfo获取,最后生成身份信息载体(Cookie),asp.net core应用程序使用cookie就能完成身份验证工作。这个过程对于用户来说,它与一般的asp.net core应用程序(特指基于asp.net core identity的应用程序)是没有任何区别的,都是通过用户名密码登录,然后就可以进入系统。对于应用程序来说它仍然是基于cookie来完成身份验证,只不过生成cookie所需的数据是第三方提供的而已。
  但是单页应用由于其特殊性,其UI渲染工作及业务逻辑的处理都是由浏览器完成,服务器不具备相关功能(仅需静态文件传输即可),其次单页应用会存在跨域问题,所以cookie就不适合作为单页应用的身份信息载体,本文就介绍如何使用jwt来完成单页应用的身份验证。
  主要内容有:
  • 创建一个简单的单页应用项目
  • 使用单页应用完成受保护资源访问
  • oidc Client简介
  • oidc-client.js的各种用法
    • 弹出式登录/登出
    • 静默登录与静默刷新
    • 会话监听
  • 小结

创建一个简单的单页应用项目

  注:该单页应用完全参考官方文档,本小节仅对要点进行介绍,详情参见文档:https://identityserver4.readthedocs.io/en/latest/quickstarts/4_javascript_client.html
  1、新建一个asp.net core的web应用程序:
从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  2、添加oidc的js组件:
  可以通过npm进行安装或者在Github上直接下载,以下为npm安装方法:
  npm i oidc-client
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  完成之后在相关目录下可找到oidc-client相关文件:
  从零搭建一个IdentityServer——单页应用身份验证 
从零搭建一个IdentityServer——单页应用身份验证
  将其复制到适合的位置即可。
  3、通过数据库添加一个Client信息(如果是基于内存的,那么需要添加一个client实例,详见文档:https://identityserver4.readthedocs.io/en/latest/quickstarts/4_javascript_client.html#add-a-client-registration-to-identityserver-for-the-javascript-client),用于单页应用的授权配置:
  添加Client时需要注意几个关键信息:
  • 授权类型支持授权码类型;
  • 不需要客户端密码;
  • 允许跨域,允许客户端跨域访问IdentityServer;
  参考如下,内存实例:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  数据库数据:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  4、添加基于oidc-client的登录、API访问以及登出业务逻辑代码App.js:
  UserManager对象初始化:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  使用UserManager实例进行登录跳转:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  使用UserManager实例获取用户信息,然后通过用户信息中的access token访问受保护资源:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  使用UserManager实例进行登出跳转:
从零搭建一个IdentityServer——单页应用身份验证
   从零搭建一个IdentityServer——单页应用身份验证
  5、添加功能页面Index.html,包含登录、API访问及登出功能:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  6、添加用于处理授权码的重定向页面:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  到此单页应用程序已经创建完毕,后面就使用该程序要介绍它是如何完成身份验证,并访问受保护资源的。

使用单页应用完成受保护资源访问

  我们使用一个简单的asp.net core web api项目(本系列文章用过的)来进行演示,它对于普通API项目来说要点在于:
  1、添加基于JwtBearer的身份验证处理器:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  2、添加跨域处理,添加跨域策略配置:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  3、在asp.net core应用请求管道中应用跨域配置:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  4、受保护内容通过authorize特性进行标记:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  一切准备就绪后运行三个应用程序,单页应用运行并打开index.html页面效果如下,一共有三个功能,登录、调用API以及登出:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  登录:它实际上是调用oidc Client的signinRedirect方法,语义上来说它是通过重定向的方式进行登录,而它实际执行的效果如下:
从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  跳转到了IdentityServer的登录页面,然后我们再看看它本质上是做了什么?
从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  它实际上是发起了一个授权码流程的身份验证请求(请求过程可参考:https://www.cnblogs.com/selimsong/p/14355150.html#oidc_code_flow),发起请求后,由于当前用户没有在IdentityServer上登录或者说未通过IdentityServer的身份验证,所以由跳转到登录页面。
  当我们通过用户名密码登录之后,IdentityServer将继续完成授权码流程,后续流程是生成相应的授权码并返回到客户端配置的重定向uri(本例中是https://localhost:5003/html/callback.html),
为了能够看清楚整个请求过程,本例在callback.html页面加入了调试断点:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  断点位于signinRedirectCallback方法之后,也就是完成回调处理之后(这个时候已经完成token等信息的获取),跳转到index.html页面之前。
  以下是输入用户名密码提交后命中断点时的相关请求信息:
从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  由上图可以看到,当输入用户名密码提交后(第一个请求),由于通过了身份验证,那么继续完成授权码流程(第二个请求),授权码流程完成后携带授权码重定向到Client配置的重定向地址(第三个请求).
  第三个请求就到了我们的callback.html页面,页面的加载首先请求了oidc-client.js文件,然后由UserManager的实例化以及signinRedirectCallback方法,来完成了后续请求,后续请求包含openid的配置信息请求、获取Token请求、获取用户信息(userinfo)请求以及检查会话请求。
  以上一系列的请求结果就是在浏览器的会话存储中,我们可以找到相关的数据信息:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  断点通过后就来到了index.html页面,并打印出登录用户信息:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  点击Call API按钮后,程序将从存储信息中获取到access_token,携带access_token完成请求:
从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  点击登出按钮后,程序将删除用户信息并跳转到IdentityServer的登出页面:
从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  注:需要配置identityserver4的登出url:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证

oidc-client.js简介

  前面的内容是基于oidc-client.js,即JavaScript版本的oidc客户端类库来实现的单页应用的,那么oidc-client.js到底为我们提供了什么功能呢?
oidc-client.js是一个支持OIDC和Oauth2.0协议的JavaScript类库,除此之外它还提供用户会话和Token的管理功能。类库中的核心类型是UserManager,它提供了用户登录、登出、用户信息管理等高层次的API,上面的例子中就是使用UserManager来完成的登录、用户信息(Access Token)获取以及登出的。
oidc-client.js或者直接说UserManager使用上需要注意以下几个方面:
  • 配置:配置的目的和asp.net core基于oidc身份验证的配置类似,主要是指明identityServer的地址、用于授权的Client信息、授权所使用的流程(授权码还是隐式流程)、授权完成后的跳转地址以及请求的Scope信息等(更多配置参数可查看文档:https://github.com/IdentityModel/oidc-client-js/wiki),如下图所示:

  从零搭建一个IdentityServer——单页应用身份验证

从零搭建一个IdentityServer——单页应用身份验证
  但这里要注意的是由于以上代码对用户是可见的,所以Client的密码就省略了。
  • 方法:提供了用户管理、登录、登出、以及相关回调方法,除此之外还有会话状态查询和开启/关闭静默刷新(token)的方法。登录/登出分为三种类型:跳转、静默和弹出,具体如何使用后续介绍。

 从零搭建一个IdentityServer——单页应用身份验证

从零搭建一个IdentityServer——单页应用身份验证
  • 属性:可以返回UserManager的配置、事件以及元数据服务。
  • 事件:UserManager包含了8个事件,如用户登录/登出、access token过期等:

  从零搭建一个IdentityServer——单页应用身份验证

从零搭建一个IdentityServer——单页应用身份验证

oidc-client.js的各种用法

弹出式登录/登出

  弹出式登录/登出就是字面的意思,通过弹出窗口打开IdentityServer的登录/登出页面完成相应功能。
  下图为弹出式登录(仅需调用UserManager的signinPopup方法即可):
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  注:回调页面需要使用signinPopupCallback:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  下图为弹出式登出:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证

静默登录与静默刷新

  静默登录和静默刷新指的就是signinSilent和startSilentRenew两个方法,而且需要注意的是startSilentRenew的原理实际上是关注了accessTokenExpiring事件,当token即将过期时调用signinSilent进行静默登录。
  静默登录方式又有两种其一是基于会话的,其二是基于刷新token,其中刷新token的优先级较高,换句话就是刷新token存在的时候,它就默认使用刷新token进行登录,刷新token比较好理解,但是会话是什么呢?它实际上就是通过IdentityServer的登录后所保持的状态,文章最开始的流程图中提到过,我们之所以可以通过授权码流程进行授权是因为登录之后有权访问IdentityServer受保护的授权终结点,从而可以获取授权码及相关Token,那这里的原理就是浏览器保存了登录状态,所以可以再次访问授权终结点来获取并刷新Token信息。
  基于会话的静默登录,下图为点击静默登录按钮后发起的请求信息,也就是正常的请求到授权码之后获取token及用户信息的过程:
从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  需要注意的是回调页面需要使用signinSilentCallback方法,同时不再需要页面跳转:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  基于刷新token的静默登录,在尝试刷新token登录之前首先需要获得刷新token,oidc中刷新token的获取是需要client支持offline_access的scope,同时在发起获取token时携带该scope:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  配置完成后重新登录获取token即可在存储中找到刷新token信息 :
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  然后再次进行静默登录(相应client需要支持refresh_token的授权方式):
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  静默登录发起的请求信息:
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  响应信息中包含了新的token:
从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证

会话监听

  会话监听是默认开启的,在正常登录状态下,通过新的浏览器窗口从identityserver中登出(目的是清除identityserver存储在浏览器的会话信息):
  从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证
  信息清除后它会立即尝试发起新的身份验证请求,但是返回信息中包含“需要登录”错误信息,可以在接收到相关错误信息时清除相关Token及用户信息,已达到单页应用随着IdentityServer会话结束而登出的效果:
从零搭建一个IdentityServer——单页应用身份验证
从零搭建一个IdentityServer——单页应用身份验证

小结

  本篇文章介绍了单页应用使用IdentityServer进行身份验证的过程及oidc-client.js JavaScript类库在应用中的使用,oidc-client.js为我们适配了oidc协议,同时还提供了丰富的功能和机制,使用这个类库可以大大减少实际工作中的开发量。
  说到单页应用的身份验证,它最根本的机制无非就是获得Access Token和Refresh Token,使用Access Token作为身份信息载体来完成身份验证,使用Refresh Token作为更新Access Token的钥匙,通过保证Access Token不过期来保证用户能够正常访问相关资源。
  但如果使用Oauth2.0协议来实现单页应用的登录(Password授权模式)会存在一些问题,首先是单页应用对于授权服务器来说是不可信的,但是Password授权由单页应用发起,属于授权服务器的用户信息及密码都要经过不可信的单页应用,这会造成一些安全问题,其次使用Oauth2.0协议完成授权后,应用与授权服务器之间实际上就没有任何关联了,授权服务器不保留用户会话,也无法实现用户登出联动的功能(即一方登出可以通知另一方),这些也正是IdentityServer4或者说OIDC协议所处理的内容。
  下篇文章,我们将继续以IdentityServer4或者说OIDC的会话管理开始,深入聊一聊它们的登录与登出。
 
参考: