目录
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协议(用户管理、权限管理、认证UI管理等)
- 我们的资源系统 – 是资源(如后端API接口)的提供者,需要对accessToken进行认证和鉴权
- 我们的接入系统 – 是接入统一认证中心的客户端,且是资源(调用后端API)的索取者
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模式 | 进入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模式 Refresh 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)接入资源鉴权。

总结上图,关于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的应用。

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鉴权流程
- 9.2)Client(作为Client端去向Resource Server发送请求):如由浏览器请求RP页面,RP后端携带Authorization: Bearer AccessToken向其他Resource Server发送请求,由Resource Server端对RP的携带的AccessToken进行鉴权,然后将返回结果注入到RP前端页面
注:
此种场景每个RP都会单独维护session,区别于OP的全局session,
此种场景的流程类似CAS SSO,关于CAS SSO可参见我之前的博客:CAS入门 – CAS协议 - 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信息、页面跳转等。 |

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已失效,完成统一登出
参考:
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/177469.html原文链接:https://javaforall.net
