目录
1. 关于认证
记得上大学时,做的登录认证非常简单,那时还是Servlet/Jsp、Struts、模版引擎
的时代,
Web后端提供登录认证接口,用户在浏览器端界面中输入用户名、密码,web后端验证通过后将认证信息写进session中,
后续的浏览器端认证都是走的session认证
,这个时候前后端都是一个应用中了,统一使用Tomcat部署。
后来压力大了,我们的应用都需要分布式部署(支持多实例分摊压力),这个时候就用出现了分布式的session管理
,
比如借助Redis集群来统一管理session信息,避免请求负载到不同Web后端而导致的Session不存在问题。
后来应用多了,就又现了单点登录、单点登出
的需求,这时就需要统一的认证中心
来维护用户认证信息,
比如可以借助CAS
来实现,在CAS中就已经使用了浏览器端重定向、颁发ticket、验证ticket并返回用户信息等,
CAS Server本身借助session来管理用户登录状态,而其他接入的Cas Client也借助各自的Session来管理各自的登录态。
再到后来,好多应用需要对接支付、第三方登录
,比如对接微信,这个时候就注意到了OAuth协议
,
也是利用浏览器重定向,code换accessToken,accessToken换用户信息的套路,
起初对OAuth的理解就局限于借助第三方登录,并获取第三方用户信息,且转化为自己的系统用户,
当时看到比较多的就是OAuth2的4种授权类型,
但是随着学习的深入,发现OAuth2能做的远不止第三方登录。
之后再从OAuth2到OIDC(支持身份认证、单点登出等)
,貌似一套完整的统一认证模型
就清晰了。
可以对比之前说的对接微信登录,那么:
- 微信 - 是认证中心(提供了OAuth协议)
- 微信 - 是资源(用户身份信息)的提供者
- 我们的接入系统 - 是接入微信登录的第三方应用,即客户端,且是资源(用户身份)的索取者
注:
通过Oauth 用户在微信平台的域名下完成认证,在微信平台的网页上输入用户认证信息,
接入微信登录的系统是无法获取到微信平台的用户账号、密码等信息的。
换个角度,假如把微信也变成我们自己内部的一个系统,即统一认证中心(管理用户及认证,实现Oauth协议),那么:
- 我们的统一认证中心 - 提供了OAuth协议(用户管理、权限管理、认证UI管理等)
- 我们的资源系统 - 是资源(如后端API接口)的提供者,需要对accessToken进行认证和鉴权
- 我们的接入系统 - 是接入统一认证中心的客户端,且是资源(调用后端API)的索取者
从这个角度来看,我们自己也可以实现OAuth/OIDC协议,来作为我们自己的统一认证中心,
我们自己的内部应用都可以通过标准协议OAuth/OIDC进行接入,
将繁琐的用户认证工作都放到统一认证中心,减轻接入应用的负担,
而我们的资源服务仅需要对accessToken进行验证,
通过OAuth/OIDC还可以实现单点登录、单点登出,
而其他第三方应用也可以通过标准的OAuth/OIDC协议接入到我们的统一认证中心,
即通过我们的用户登录他们的系统。
下文对OAuth2、OIDC 1.0的讲解均是站在后一种角度来说的,
即通过OAuth/OIDC协议来实现的统一认证中心。
注:
现如今的接入系统已不仅仅是之前的Web应用(包含后端),
更有可能是纯前端SPA、移动端应用等,后续会讲到借助PKCE + Code Flow的方式。
2. OAuth 2.0
OAuth 2.0(Open Authorization,开发授权) 是一个关于授权(Authorization)的开放行业标准,目前仅支持HTTP协议栈,适用于传统Web应用、SPA、移动端App、桌面应用及其他智能设备。OAuth2.0引入了授权服务器Authorization Server、客户端Client、资源拥有者Resource Owner、资源服务器Resource Server等概念,解耦并明确划分了各角色的职责。相较于传统认证模式,OAuth2.0通过向客户端Client颁发(在用户Resource Owner同意授权给客户端后)临时的访问令牌Access Token(JWT形式,包含访问的范围scope, 有效时长及其他自定义属性) 的方式避免了用户的账号密码直接暴露给客户端,之后客户端Client使用访问令牌Access Token访问资源服务器Resource Server上被保护的资源,由资源服务器对携带访问令牌的请求进行认证与鉴权。借助JWT的优势,OAuth2.0可以更细粒度的控制单个访问令牌Access Token(对应单次授权而非用户全局设置)的访问范围(JWT scope)以及对单个访问令牌的吊销(对应OAuth2.0的Token Revocation Endpoint)。同时亦提供了令牌刷新模式(对应OAuth2.0的Refresh Token Endpoint),给用户带来体验更好的无感登录延续。
Oauth 2.0支持授权模式如下:
授权模式 | 说明 |
---|---|
授权码模式(Authorization Code) 通过code换取oken | 较为常用,安全等级高,适用于存在Web后端的应用。 注: 通过code获取Token需要同时携带ClientId和ClientSecret, 所以该模式仅适用于可以安全存储ClientKey且不会泄露的应用(如后端服务), 而前端服务(SPA)、原生应用(桌面、移动端)都会暴露给具体用户,ClientKey有被用户拦截的风险, 下面提到的PKCE可适用于前端服务(SPA)、原生应用(桌面、移动端)。 |
授权码+PKCE模式(Authorization Code With PKCE) 通过code+code_verifier换取oken | 可适用于前端服务(SPA)、原生应用(桌面、移动端)。 |
客户端凭证模式(Client Credentials) 通过Cient凭证(ClientId和Secret)换取token | 适用于服务器间(M2M)通信。 |
刷新令牌模式(Refresh Token) 通过refresh_token换取新token | 根据之前授权通过时颁发的refresh_token获取新的access_token,避免用户多次重复登录(无感登录),提升用户体验。 |
设备码模式(Device Code) deviceCode+UserCode+VerficationUri 引导用户通过其他设备(如手机)进行认证授权 | 适用于可联网的无浏览器或输入受限的设备授权,例如智能电视、电子相册、打印机等。 |
直接返回token | 不推荐, 作为授权码模式的备用选项。 |
直接通过用户名密码换取token | 不推荐, 最不安全,客户端需要知道用户账号, 且客户端与认证方式强绑定,后续认证方式修改客户端也需要一起修改。 |
注:
关于密码模式,即Client端直接拿到用户的账号、密码,然后请求认证中心进行认证,
用户的账号、密码由第三方认证中心(如微信) 或者 自建认证中心 进行管理,
试想微信的账号密码暴露给接入的Client端(比如新开发的App接入微信登录),那么Client端就可以拿着用户的微信账号为所欲为,那就费菜了,这显然是最不安全的,微信也绝不可能同意,
即便是我们自建的认证中心,接入的Client端也都是我们自己公司的应用,接入的Client都能拿到用户账号密码,如果黑心的开发人员在Client应用中下了黑手留了后门啥的,岂不是可以拿到大量用户账号密码,显然也是不可取的,关于用户的账密码,还是越少人(越少Client端应用)知道越好,
还有一点就是使用密码模式(username、password)方式进行登录,客户端会与认证方式进行绑定,
例如今天通过用户名、密码登录,明天想通过手机号、验证码登录,后天添加验证码…,那么客户端都得跟着修改,仅使用username、password两个参数是远远不够的,所以就进入了用户认证逻辑一修改,所有客户端都得跟着修改的情况,而使用授权码这种模式,客户端和认证中心的协议是固定的,认证逻辑的修改仅限制在认证中心,并不会影响客户端。
如此也就是不推荐密码模式的原因。
3. OIDC 1.0
OIDC 1.0(OpenID Connect) 在OAuth2上构建了一个身份层(Identity, Authentication),是一个基于OAuth2协议的身份认证标准协议(OAuth2的超集)。OAuth2更关注授权,OIDC使用OAuth2的授权服务器来为第三方客户端提供用户的身份认证,并把对应的身份认证信息idtoken传递给客户端,且提供获取用户信息的接口,适用于各种类型的客户端(比如服务端Server应用,移动APP,JS应用(SPA)
),且完全兼容OAuth2
,也就是说你搭建了一个OIDC的服务后,也可以当作一个OAuth2的服务来用。
现阶段实现单点登录SSO及单点登出SLO
或先鉴权用户再返回资源
,优先建议使用 OIDC 协议。
注:
OIDC是OpenID 2.0和Oauth 2.0的组合,
OpenID负责身份认证层协议
Oauth负责授权层协议
OIDC及OAuth核心术语:
OIDC | OAuth | 说明 |
---|---|---|
EU End User | Resource Owner | 拥有资源的用户 |
RP Relying Party | Client Third-party application | 用来代指OAuth2中的受信任的客户端(Web应用、SPA、移动端应用…), 即接入认证中心(OP、Authorization Server)登录跳转且并且获取Token(IdToken、AccessToken), 可通过IdToken获取用户身份信息, 然后携带AccessToken访问资源(Resource Server、Userinfo Endpoint) |
OP OpenID Provider | Auhtorization Server | 为用户(EU、Resource Owner)提供认证的服务(如微信授权平台、Github授权平台、自建用户中心等), 用来为应用客户端(RP、Client)提供用户(EU、Resource Owner)的身份认证信息 |
Resource Server | Resource Server | 资源服务器, 即提供资源API且需要对(受保护的)资源访问进行鉴权的服务, Cilent端携带AccessToken访问ResourceServer, 然后由Resource Server对此AccessToken进行鉴权 |
Endpoint | Endpoint | 端点,即提供的API接口 |
ID Token | 无 | 身份令牌(JWT), 用于认证,标识用户身份已经认证,可用于获取用户身份信息(如用户名、头像等), 同时客户端需对获取到的ID Token进行签名验证、属性验证(aud、azp、nonce等)。 |
Access Token | Access Token | 访问令牌(JWT), 用于鉴权,适用于API的访问鉴权。 |
User Agent | User Agent | 应用执行端, 如浏览器,手机端,即RP的执行端 |
OIDC和OAuth的常用端点(Endpoint,即提供的API接口)
endpoint | 提供方 | 调用方 | 认证 | 格式 | desc |
---|---|---|---|---|---|
authoriztion_endpoint | OP | RP | 无 | Code Flow模式: GET authorization_endpoint ?response_type=code &redirect_uri={redirect_uri} &client_id={client_id} &state={random_str} &nonce={another_random_str} &scope=openid profile email PKCE + Code Flow模式 (额外添加code_challenge参数) GET authorization_endpoint ?response_type=code &redirect_uri={redirect_uri} &client_id={client_id} &state={random_str} &nonce={another_random_str} &scope=openid profile email &code_challenge_method=S256 &code_challenge={code_challenge} | 进入OP登录授权界面 |
redirect_uri | RP | OP | 无 | GET redirect_uri ?code={code} &state={random_str} | 在OP登录成功后,将code回调给RP |
token_endpoint | OP | RP | client_secret_post client_secret_basic | Code Flow模式: POST token_endpoint ?grant_type=authorization_code &code={code} &redirect_uri={redirect_uri} &client_id={client_id} &client_secret={client_secret} PKCE + Code Flow模式 (删除client_secret参数后 额外添加code_verfier参数) POST token_endpoint ?grant_type=authorization_code &code={code} &redirect_uri={redirect_uri} &client_id={client_id} &code_verifier={code_verifier} Refresh Token模式: POST token_endpoint ?grant_type=refresh_token &client_id={client_id} &client_secret={client_secret} &refresh_token={refresh_token} 注:在PKCE模式下支持无client_secret执行刷新token流程 | 通过code获取access_token、id_token, 根据refresh_token获取新的access_token |
userinfo_endpoint | OP | RP | Bearer {access_token} | GET userinfo_endpoint | 根据acces_token获取用户信息 |
introspection_endpoint | OP | Resource Server | client_secret_post client_secret_basic | POST introspection_endpoint ?token={your_token} | 用于验证access_token是否有效, 通过返回结果中的active进行标识 |
revocation_endpoint | OP | RP | client_secret_post client_secret_basic | POST revocation_endpoint ?token={your_token} &token_type_hint={} &token_type_hint=access_token | 撤销token、access_token, 若撤销refresh_token,则此refresh_token关联的access_token也会被撤销 |
end_session_endpoint | OP | RP | 无 | GET end_session_point ?id_token_hint={id_token_issued_to_client} &post_logout_redirect_uri={post_logout_redirect_uri} &state={random_str} | 结束浏览器中OP的session会话, 并触发后续的多RP的Front-Channel或Back-Channel登出, 最后可配合post_logout_redirect_uri 重定向回RP页面 |
post_logout_redirect_uri | RP | OP | 无 | GET post_logout_redirect_uri | OP登出成功后会重定向回RP页面, 或对应于RP登录界面、登出状态展示页面等 |
check_session_iframe | OP | RP | 无 | RP iframe.src=check_session_iframe | Session Management,检查当前RP的登录状态 |
frontchannel_logout_uri | RP | OP iframe | 无 | GET fontchannel_logout_uri ?iss={issureId} &sid={idToken.sid} | RP前端实现的登出处理页面, 由OP Front-Channel模式下返回的Html iframe调用 |
backchannel_logout_uri | RP | OP | 无 | POST backchannel_logout_uri ?logout_token={logout_token like idToken} | RP后端实现的后端登出接口, RP后端需验证logout_token并根据logout_token找出并清除后端的对应的session信息 |
4. OIDC选型建议
关于OIDC授权模式的选型可参见下图:
注:
关于OIDC授权模式的选型皆是建立在:
已拥有(第三方身份云 或者 自建)独立的认证中心(OP、Authorization Server) 的角度。
推荐企业自建用户中心(用户中台),将用户认证剥离出来,减少各层服务集成认证的侵入依赖,
终端(RP、Client)接入认证及Token(使用认证中心的Token,无需每个Client单独维护Session或者Token),
服务端(Resource Server)接入资源鉴权。
OAuth2中定义了2种Client类型:
Confidential机密型:能确保安全存储凭证(Client Secrets),例如后端服务(部署在安全的服务器上且有访问控制) 或者 能通过其他方式保证认证过程安全的Client
Public公共型:无法安全存储凭证(Client Secrets),例如原生Native应用(安装在移动设备、电脑上,直接暴露给用户)、浏览器应用,又或者不能保证认证过程安全的Client
对应Public型的客户端,推荐使用PKCE模式
总结上图,关于OIDC的最优建议如下:
应用类型(即Client) | 选型建议 |
---|---|
SPA 如Vue、React等单页应用(前后端分离), 有专门的后端资源服务层(Resource Server)提供API服务 | PKCE + 授权码模式 即由前端接入认证,而后端仅接入鉴权 |
原生应用Native(移动端应用、桌面应用) 如Android、Ios(前后端分离), 有专门的后端资源服务层(Resource Server)提供API服务 | PKCE + 授权码模式 即由前端接入认证,而后端仅接入鉴权 |
WEB应用(即存在Web后端服务) 适用于获取第三方资源、集成第三方登录(如微信登录)、集成SSO | 授权码模式 接入Oauth后携带code回调到Web后端, 由后端获取AccessToken, 在后端携带AccessToken请求资源, 可使用session存储Token和User信息, 即使用session作为当前Web应用的身份验证机制 |
服务器间通信 无终端用户 | Client Credentials 模式 |
4.1 PKCE
PKCE(Proof Key for Code Exchange)
通过一种密码学手段确保恶意第三方即使截获授权码Authorization Code,也无法向认证服务器交换Access Token,
同时不需要在客户端存储Client Secret,避免Client Secret被泄露。
code_verifier 验证码,随机字符串
code_challenge 挑战码,code_challenge = Base64(Sha256(code_verifier))
PKCE的流程大概如下:
- 随机生成一串字符并作URL-Safe的Base64编码处理,结果用作 code_verifier(这个值记录下来后续会用到)
- 将这串字符通过SHA256哈希,并用URL-Safe的Base64编码处理,结果用作 code_challenge
- 把 code_challenge 带上,跳转认证服务器,获取 Authorization Code
- 把 code_verifier 带上,换取Access Token
如上图所示,即使黑客同时拦截了上图中的第1步中的挑战码code_challenge和第4步中返回的授权码code,但是黑客无法根据挑战码code_challenge(通过Sha256单向哈希过)反向推出原始的验证码code_verifier,所以在后续第5步中也就无法提供正确的验证码code_verifier(授权服务端需要比对挑战码和验证码是否匹配)来换取访问令牌Access Token,如此即通过PKCE有效地防止了恶意第三方截获授权码code来换取访问令牌Access Token的可能。
5. SSO方案
SSO(Single Sign-On) 单点登录,即同时访问多个应用仅需要登录一次
以下流程中提到的RP,即需要我们开发集成SSO的应用。
SSO总体方案
1)统一重定向到OP进行登录认证(若OP中已经登录过,则直接返回code重定向回RP)
2)OP显示认证界面,待用户输入账户信息完成认证后,会返回code再重定向回RP
3)RP通过code及secret等换取IdToken、AccessToken和用户信息
4)RP登录成功
5.1 SPA)RP本地存储Token及用户信息,然后携带AccessToken访问Resource Server,Resource Server通过Jwks或者introspection_endpoint 验证access_token
5.2 WEB)RP转换OP EU信息为RP自己的session用户信息,并设置RP session为登录态
5.1 SSO SPA
- 1)RP(SPA、Native)向OP发送登录请求(
PKCE(code_challenge) + 授权码模式
)到authorization_endpoint
- 2)OP先检查当前请求是否对应EU已登录的session信息(是否已在同一个浏览器中登录过OP)
- 2.1) 若存在EU的已登录的session信息,则直接跳到第6步
- 2.2) 若不存在EU的已登录的session信息,则继续下一步
- 3)OP后端重定向到OP的认证登录页面
- 4)EU在OP提供的登录界面进行登录并提交认证到OP
- 5)OP在对EU认证通过后,触发后续的OAuth2 Code Flow(授权码流程)
- 5.1) 此处OP端存储当前浏览器端对应的EU已登录的session信息(当前浏览器端的所有RP均共享此session)
- 6)OP重定向到RP的
redirect_uri
(重定向SPA的一个空白页面),并返回code
参数 - 7)RP通过
PKCE(code_verifier)
模式调用token_endpoint
接口(根据code换取IdToken、AccessToken),然后在本地(User Agent端)存储Token并标记为登录态 - 8)RP携带Authorization: Bearer AccessToken向OP发送
userinfo_endpoint
请求(查询用户信息),OP对RP的携带的AccessToken进行鉴权(此步骤可选) - 9)RP携带Authorization: Bearer AccessToken向Resource Server发送请求,由Resource Server端对RP的携带的AccessToken进行鉴权
- 10)后续在有新的RP登录,则会依次触发1、2、2.1后直接到过OP端的登录认证(EU无需再次输入用户名密码等),而直接到第6步(重定向会新的RP后,新RP通过code换取属于自己的Token)
5.2 SSO WEB
- 1)RP后端(WEB应用,存在后端)发送重定向响应到OP端
authorization_endpoint(授权码模式)
- 2)OP先检查当前请求是否对应EU已登录的session信息(是否已在同一个浏览器中登录过OP)
- 2.1)若存在EU的已登录的session信息,则直接跳到第6步
- 2.2)若不存在EU的已登录的session信息,则继续下一步
- 3)OP后端重定向到OP的认证登录页面
- 4)EU在OP提供的登录界面进行登录并提交认证到OP
- 5)OP在对EU认证通过后,触发后续的OAuth2 Code Flow(授权码流程)
- 5.1)此处OP端存储当前浏览器端对应的EU已登录的session信息(当前浏览器端的所有RP均共享此session)
- 6)OP重定向到RP的
redirect_uri
(重定向到RP后端),并返回code参数 - 7)RP后端调用
token_endpoint
接口(根据code换取IdToken、AccessToken),然后在当前session中存储Token并标记session为登录态 - 8 )RP后端携带Authorization: Bearer AccessToken向OP发送
userinfo_endpoint
请求(查询用户信息),OP对RP的携带的AccessToken进行鉴权(此步骤可选),RP后端存储用户信息到session中 - 9)根据WEB端接入OAuth用途不同,可分为Login和Client场景。
- 9.1)Login(接入SSO 或者 三方登录如微信登录):将OP
userinfo_endpoint
提供的用户信息,转换为RP应用的自身的用户信息及session,后续走RP原有的session鉴权流程注:
此种场景每个RP都会单独维护session,区别于OP的全局session,
此种场景的流程类似CAS SSO,关于CAS SSO可参见我之前的博客:CAS入门 - CAS协议 - 9.2)Client(作为Client端去向Resource Server发送请求):如由浏览器请求RP页面,RP后端携带Authorization: Bearer AccessToken向其他Resource Server发送请求,由Resource Server端对RP的携带的AccessToken进行鉴权,然后将返回结果注入到RP前端页面
- 9.1)Login(接入SSO 或者 三方登录如微信登录):将OP
- 10)后续在有新的RP登录,则会依次触发1、2、2.1后直接到过OP端的登录认证(EU无需再次输入用户名密码等),而直接到第6步(重定向回新的RP后端,新RP后端通过code换取属于自己的Token)
6. SLO方案
SLO(Single Logout) 单点登出,即同时访问多个应用仅需要登出一次,其他应用也自动登出
OIDC的关于登出的相关协议:
协议 | 功能 |
---|---|
OpenID Connect RP-Initiated Logout 1.0 | RP端向OP发出的初始登出请求(适用于单点登出SLO,end_session_endpoint ),OP首先清除OP端的session, 然后向当前session下的其他已登录的RP触发Front-Channel或者Back-Channel登出请求, 最终可通过 post_logout_redirect_uri 重定向会当前RP |
OpenID Connect Front-Channel Logout 1.0 | 返回OP前端登出页面, 页面通过嵌入iframe(src= frontchannel_logout_uri ),同时触发当前OP Session对应的多个Client的登出接口 |
OpenID Connect Back-Channel Logout 1.0 | OP直接向当前OP Session对应的多个Client后端服务(不依赖User Agent,如不依赖浏览器)发送登出请求backchannel_logout_uri ,且适用于User Agent被关闭的时候也可以退出登录。 |
OpenID Connect Session Management 1.0 | RP端通过check_session_iframe 监听当前UserAgent中用户的登录状态,收到登录状态改变通知后RP应该清除自己的Session信息、页面跳转等。 |
SLO总体思路:
1)RP触发自身的登出清理操作(RP清理自身session)
2)重定向到OP的end_session_point(OP清理自身session)
3)OP向其他RP发送登出请求Front-Channel、Back-Channel(OP触发其他R
P清理自身session)
4)OP处理完SLO请求end_session_point后重定向回RP页面
6.1 SLO SPA
- RP(SPA、Native)触发自身的登出请求,即清空本地存储的用户登录态及Token信息
- RP重定向到OP的
end_session_endpoint
- OP收到
RP-initiated-Logout(end_session_endpoint)
请求,清除OP的session信息 - OIDC Front-Channel Logout:
- OP返回前端页面,前端页面(通过iframe 或 jsonp)同时向当前OP session对应的多个已登录的RP(携带RP已登录的用户信息)发送登出请求
frontchannel_logout_uri
- OP返回前端页面向其他已登录RP发送完登出请求后,可重定向回当前RP的
post_logout_redirect_uri
- OP返回前端页面,前端页面(通过iframe 或 jsonp)同时向当前OP session对应的多个已登录的RP(携带RP已登录的用户信息)发送登出请求
- 其他RP收到登出请求后(在OP返回页面的iframe中,对应一个需单独实现的RP
frontchannel_logout_uri
登出处理页面),清空RP本地的用户登录态及Token信息 - RP亦可借助OpenID Connect Session Management 1.0
check_session_iframe
来实时监听登录态,当察觉到登出状态时,除了清空RP本地的用户登录态及Token信息,亦可跳转到登出后对应的页面
6.2 SLO WEB
- RP前端向Web后端发送登出请求,RP的Web后端清除RP端session及token信息,
- RP Web后端重定向到OP的
end_session_endpoint
- OP收到
RP-initiated-Logout(end_session_endpoint)
请求,清除OP的session信息 - Back-Channel:
- OP后端向当前OP session对应的已登录的RP(携带
logout_token
,类似idToken)发送后端登出请求backchannel_logout_uri
- OP后端向其他已登录RP发送完登出请求后,可重定向回当前RP前端的
post_logout_redirect_uri
- OP后端向当前OP session对应的已登录的RP(携带
- 其他RP后端收到登出请求后(由OP后端调用,对应一个需RP后端单独实现的RP
backchannel_logout_uri
登出API接口),- RP后端需验证logout_token
- 并根据logout_token找出并清除后端对应的EU的session信息
- 之后RP再次向后端发送请求后,即session已失效,完成统一登出
参考:
authing文档 - 概念
authing文档 - 联邦认证
Authing 成为 OIDC 身份源(授权码、pkce)
Authing文档 - 选择OIDC授权模式
https://lequ7.com/guan-yu-javajava-kai-fa-zhong-jwtjwejwsjwk-dou-shi-gan-sha-de.html
OIDC(OpenId Connect)身份认证
https://docs.authing.cn/v2/concepts/oidc/oidc-overview.html
https://docs.authing.cn/v2/guides/federation/oidc.html#授权码-pkce-模式
https://docs.authing.cn/v2/apn/more-oidc-tests/type2.html - Type 2 授权码 + PKCE 模式测试
https://curity.io/resources/learn/openid-connect-logout/
https://stackoverflow.com/questions/26892177/openid-connect-how-to-handle-single-logout