OAuth2¶
最后更新:2021-12-01
1. 应用介绍¶
1.1. OAuth2.0是什么?¶
OAuth(Open Authorization,开放授权)是一个开放标准的授权协议,允许用户授权第三方应用访问他们存储在资源服务上受保护的信息,而不需要将用户名和密码提供给第三方应用,解耦了认证和授权。OAuth2.0是OAuth协议的延续版本,更加安全,更易于实现,但不向后兼容OAuth1.0,即完全废止了OAuth1.0,由于OAuth1.0协议复杂,安全性不高,已被OAuth2.0取代。它主要是一个授权协议,允许软件应用代表(而不是充当)资源拥有者去访问资源拥有者的资源。应用向资源拥有者请求授权,然后得到令牌(Token),并用它来访问资源。
OAuth作为一种国际标准,目前传播广泛并被持续采用,并在协议规范【RFC 6749】详细定义OAuth2.0的实现细节。
OAuth 2.0 规定了四种获得令牌的流程,分别是授权码模式(authorization_code)、简化模式(implicit)、密码模式(password)、客户端模式(client_credentials), 可以选择实际情况选择最适合的一种,向第三方应用颁发令牌。比如授权码模式, 客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。
1.2. IDaaS平台OAuth2概述¶
IDaaS平台上的OAuth2应用基于标准OAuth2协议
支持简化模式(implicit)和 授权码模式(authorization-code)以及 PKCE (属于授权码模式的一个扩展)
其中,使用授权码模式能实现从IDaaS到您业务应用单点登录功能。
以下主要包含以下内容:
时序说明 - 以OAuth2协议授权码模式接入IDaaS的简单时序图说明,以及交互参数
主要流程 - OAuth2.0模板使用主要流程
操作步骤 - 从新建开始配置一个OAuth2应用,以及如何在客户端中开发,包含具体API请求、响应和错误提示,详细如下
如何获取用户令牌
如何获取用户信息userinfo
如何刷新用户令牌
常见QA - 常见问题以及其对策
2. 如何配置¶
2.1. 时序说明¶
2.1.1. 场景:SP发起单点登录时序¶
OAuth 2.0的草案是在2010年5月初在IETF发布的。OAuth 2.0是OAuth协议的下一版本,但不向后兼容OAuth 1.0。 OAuth 2.0关注客户端开发者的简易性,同时为Web应用,PC应用和手机,和IOT设备提供专门的认证流程。规范在IETF OAuth工作组的主导下,OAuth标准于2010年末完成。
OAuth2是一个授权协议, 主要用来作为API的保护, 我们称之为STS(安全令牌服务, Security Token Service)。 但是在某些情况下, 也可以被用来实现WEB SSO单点登录。一般的流程是用户把发起页面的URL和state参数关联上, 并保存在SP本地, 用户登录后, 可以获取一个Code, 利用Code拿到AT(Access Token) 后, 可以利用这个AT获取用户信息userinfo, 进而从state 中, 获取到对应的原始URL, 并跳转到这个URL, 从而实现登录到一个业务应用SP的效果。 下图详细描述了这个SSO过程。
详细时序图(以授权码模式为例):
说明:
第[5]步参数要求
response_type:必选、值固定为”code”
client_id:必选、第三方应用的标识ID
state:推荐、Client提供的一个字符串,服务器会原样返回给Client,它既能防止CSRF、XSRF, 同时也可以用来对应SP初始发起的状态。
redirect_uri:必选、授权成功后的重定向地址
scope:可选、表示授权范围
prompt:可选
第[6]步校验内容
a.client_id是否合法
b.prompt:
若应用请求IDP时不带prompt参数,则逻辑为用户没登录就跳转到登录页
若应用请求IDP时带参数prompt=none,则默认用户已经登录验证,如果IDP发现用户未登录验证,则直接报interaction_required错误
若应用请求IDP时带参数prompt=login,则不论用户是否已经登录认证,都重新走一次认证流程
第[11]步返回参数
跳转到[5]中指定redirect_uri,并返回:code:授权码 state:步骤[5]中客户端提供的state参数原样返回
第[13]步校验参数
state是否和自己发送出的一致
第[14]步请求参数
grant_type:必选、固定值”authorization_code”
code:必选、Authorization Response中响应的code
redirect_uri:必选、必须和Authorization Request中提供的redirect_uri相同
client_id:必选、必须和AuthorizationRequest中提供的client_id相同
client secret: client的secret,用于授权服务器校验client的合法身份
第[15]步校验参数
a.client_id、client_secret(若有)是否合法
b.redirect_uri是否和步骤[A]中的redirect_uri一致
c.code是否合法:
是否过期
是否被重复使用,若是就视为一次攻击,加入日志审计,并将之前为code生成的access token撤销
比较code和应用的client id是否匹配
d.server必须在http server头部返回:Cache-Control:no-store and Pragma:no-cache,确保client不会被缓存
第[16]步返回参数
access_token:访问令牌
refresh_token:刷新令牌
expires in:过期时间
第[19]步完成单点登录
完成了这一步,就获取到access_token和用户信息,可以展示当前登录用户信息,基于此保存的session会话,用户可以不用再频繁登录,实现点击图标即可跳转应用的过程。
2.2. 主要流程¶
Step1 创建OAuth2应用,基于OAuth2模板快速创建应用
Step2 授权OAuth2应用,对OAuth2应用授予访问权限
Step3 获取应用信息,基于配置应用信息主要为获取授权码Code
Step4 访问授权URL获取Code,通过相关应用配置,跳转应用地址
Step5 完成应用侧的开发/配置,就可以实现业务应用单点登录功能
2.3. 操作步骤¶
2.3.1. Step1 创建OAuth2应用:¶
1、首先以IT管理员账号登录云盾IDaaS管理平台。具体操作请参考 IT管理员指南-登录 。
2、点击左侧导航栏应用 > 添加应用 选择右侧OAuth
3、选择OAuth2应用模板点击添加应用。
4、Redirect URI : 填写需要使用OAuth2单点登录应用的URL
GrantType : 选择authorization_code
其他参数默认即可,有需要也可按照实际需要修改
2.3.2. Step2 OAuth2应用授权¶
应用授权:选择应用(搜索应用)、选择组织机构(搜索组织机构)、勾选授权即可
2.3.3. Step3 获取应用信息¶
点击左侧导航栏应用 > 应用列表 查看 OAuth2 应用详情, 获得 Client Id、Client Secret、Authorize URL.
2.3.4. Step4 访问授权URL获取Code¶
参考SP发起单点登录时序:认证成功生成code
应用通过一个IDP登录按钮等或其它方式, 触发浏览器打开 AuthorizeURL,使用授权的账户进行登录,登录成功后跳转到回调地址redirect_uri,并把Code参数一同转发过去。
2.3.5. Step5 完成应用侧的开发/配置¶
2.3.5.1. 、利用Code从服务器获取AT(Access Token)¶
参考SP发起单点登录时序:请求access_token
无论是JAVA, PHP, 还是.NET应用, 接下来要做的是,应用通过URL参数拿到这个Code 后, 紧接着构建一个应用Token 换AT(Access Token)的过程。
Request URI:
https://{IDaaS_server}/oauth/token?grant_type=authorization_code&code={code}&client_id={client_id}&client_secret={client_secret}&redirect_uri={redirect_uri}
IDaaS_server:为IDaaS服务部署访问的host地址
注: OAuth支持多种grant_type 这里使用的是authorization_code模式。
接口说明:获得 access_token
请求方式:POST
请求参数
参数 |
类型 |
是否必选 |
示例值 |
描述 |
---|---|---|---|---|
code |
String |
是 |
vuQ3n6 |
用户登录成功后回调传递的code值 |
client_id |
String |
是 |
oauth2 client_id |
OAuth2 client_id |
client_secret |
String |
是 |
oauth2 client_secret |
OAuth2 client_secret |
redirect_uri |
String |
是 |
重定向 url |
返回参数示例:
{
"access_token": "eyJhbGc1NiIs...",
"token_type": "bearer",
"refresh_token": "eyJhbGciOi...",
"expires_in": 7199,
"scope": "read",
"jti": "956901cd-5669-411f"
}
参数 |
类型 |
示例值 |
描述 |
---|---|---|---|
access_token |
String |
eyJhbGc1NiIs… |
Access Token |
token_type |
String |
bearer |
Token 类型 |
refresh_token |
String |
eyJhbGciOi… |
刷新token |
expires_in |
String |
7199 |
Access Token 过期时间 |
scope |
String |
read |
申请的权限范围 |
jti |
String |
956901cd-5669-411f |
当令牌是jwt格式时,该值表示令牌的id |
错误码说明
HttpCode |
错误码 |
错误信息 |
描述 |
---|---|---|---|
400 |
invalid_grant |
Invalid authorization code: “code”. |
无效的授权码 |
400 |
invalid_grant |
Redirect URI mismatch. |
重定向 URI 不匹配 |
401 |
Unauthorized |
Unauthorized |
未授权的访问 |
403 |
Forbidden |
Forbidden |
无权限访问 |
404 |
ResourceNotFound |
ResourceNotFound |
访问的资源不存在 |
415 |
UnsupportedMediaType |
UnsupportedMediaType |
不支持的媒体类型 |
500 |
InternalError |
The request processing has failed due to some unknown error, exception or failure. |
发生未知错误 |
①{code}需要替换为授权应答Authorization Response中提取到的 code 参数的值。
注意 Code 的值只能用一次
②{client_id}、{client_secret}需要替换为认证成功生成code中获得的值
③{redirect_uri} 需要替换为302重定向到IDP进行认证授权添加 OAuth2 应用时输入的跳转值
以上完成后你将获得AT(Access Token),此AT将作为你访问的凭证。
2.3.5.2. 、获取用户信息userinfo¶
参考SP发起单点登录时序:** 应用请求userinfo(携带access_token)**
在获取到AT(Access Token)后,应用可以接着向IDaaS平台发送进一步的请求, 以获取到用户信息,实现登录到一个业务应用SP的效果。
①发送GET请求到https://{IDaaS_server}/api/bff/v1.2/oauth/userinfo?access_token={access_token}
{access_token}替换为前一步获取到的AT(Access Token)
②从返回参数即可获取userinfo信息
Request URI: https://{IDaaS_server}/api/bff/v1.2/oauth/userinfo
接口说明:获取用户详细信息
请求方式:GET
请求参数
参数 |
类型 |
是否必选 |
示例值 |
描述 |
---|---|---|---|---|
access_token |
String |
是 |
eyJhbGc1NiIs… |
Access Token |
返回参数响应示例
{
"sub": "823071756087671783",
"ou_id": "2079225187122667069",
"nickname": "test",
"phone_number": 11136618971,
"ou_name": "研发部",
"email": "test@test.com",
"username": "test"
}
参数说明
参数 |
类型 |
示例值 |
描述 |
---|---|---|---|
sub |
String |
823071756087671783 |
子编号 |
ouid |
String |
2079225187122667069 |
父组织ID |
nickname |
String |
test |
昵称 |
phone_number |
String |
11136618971 |
手机号 |
ou_name |
String |
研发部 |
父组织名称 |
String |
test@test.com |
邮箱 |
|
username |
String |
test |
用户名 |
错误码说明
HttpCode |
错误码 |
错误信息 |
描述 |
---|---|---|---|
401 |
Unauthorized |
Unauthorized |
未授权的访问 |
403 |
Forbidden |
Forbidden |
无权限访问 |
404 |
ResourceNotFound |
ResourceNotFound |
访问的资源不存在 |
415 |
UnsupportedMediaType |
UnsupportedMediaType |
不支持的媒体类型 |
500 |
InternalError |
The request processing has failed due to some unknown error, exception or failure. |
发生未知错误 |
这样, 用户登录成功后, 浏览器有了主会话, 一个SP应用利用它获取一个令牌AT(AccessToken),应用拿到AT令牌后去IDaaS认证中心校验令牌是否有效,同时到/userinfo接口去拉取更多的用户信息, 获取到具体的子账户UserId, 有了UserId 就可以创建SP的子会话。 从而在子会话有效期都不用再登录,实现从IDaaS单点登录到应用的全过程。
2.3.5.3. 获取应用子账户信息¶
Request URI: https://{IDaaS_server}/api/bff/v1.2/oauth2/userinfo/sub
接口说明:获取应用子账户信息
请求方式:GET
请求参数
参数 |
类型 |
是否必选 |
示例值 |
描述 |
---|---|---|---|---|
access_token |
String |
是 |
eyJhbGc1NiIs… |
Access Token |
client_id |
String |
是 |
xxxxxx |
应用client_id |
返回参数响应示例
{
"success": true,
"code": "200",
"message": null,
"requestId": "1658299120963$c975d578-e80c-1f99-8a38-42e08576df05",
"data": {
"sub_accounts": [
"fff"
]
}
}
参数说明
参数 |
类型 |
示例值 |
描述 |
---|---|---|---|
success |
boolean |
true |
是否成功 |
code |
String |
200 |
状态码 |
message |
String |
null |
返回消息 |
requestId |
String |
B3776BB1-930F-4581-B4C3-18F2D7D136CA |
请求ID |
data |
Object |
响应数据 |
|
└sub_accounts |
Arrays |
子账户列表 |
错误码说明
HttpCode |
错误码 |
错误信息 |
描述 |
---|---|---|---|
401 |
Unauthorized |
Unauthorized |
未授权的访问 |
403 |
Forbidden |
Forbidden |
无权限访问 |
404 |
ResourceNotFound |
ResourceNotFound |
访问的资源不存在 |
415 |
UnsupportedMediaType |
UnsupportedMediaType |
不支持的媒体类型 |
500 |
InternalError |
The request processing has failed due to some unknown error, exception or failure. |
发生未知错误 |
2.3.5.4. 获取应用账户扩展信息¶
Request URI: https://{IDaaS_server}/api/bff/v1.2/oauth2/userinfo/extra
接口说明:获取应用账户扩展信息
请求方式:GET
请求参数
参数 |
类型 |
是否必选 |
示例值 |
描述 |
---|---|---|---|---|
access_token |
String |
是 |
eyJhbGc1NiIs… |
Access Token |
返回参数响应示例
{
"success": true,
"code": "200",
"message": null,
"requestId": "1667302908374$df5dc0f0-b358-cf8d-877d-20381d841934",
"data": {
"extendFields": {
"major": "计算机",
"age": 30
}
}
}
参数说明
参数 |
类型 |
示例值 |
描述 |
---|---|---|---|
success |
boolean |
true |
是否成功 |
code |
String |
200 |
状态码 |
message |
String |
null |
返回消息 |
requestId |
String |
B3776BB1-930F-4581-B4C3-18F2D7D136CA |
请求ID |
data |
Object |
响应数据 |
|
└extendFields |
Object |
扩展信息(扩展字典) |
错误码说明
HttpCode |
错误码 |
错误信息 |
描述 |
---|---|---|---|
401 |
Unauthorized |
Unauthorized |
未授权的访问 |
403 |
Forbidden |
Forbidden |
无权限访问 |
404 |
ResourceNotFound |
ResourceNotFound |
访问的资源不存在 |
415 |
UnsupportedMediaType |
UnsupportedMediaType |
不支持的媒体类型 |
500 |
InternalError |
The request processing has failed due to some unknown error, exception or failure. |
发生未知错误 |
2.3.5.5. 、刷新用户令牌¶
当用户的令牌(access_token)快过期,或过期后,如不想再次让用户重新登录,可使用5.1步骤返回的刷新令牌(refresh_token)再次刷新用户的令牌,成功刷新后,将会返回该用户一个新的令牌和刷新令牌。注意在调用时,每次返回的刷新令牌也会有一个过期时间,其值请在OAuth2应用中设置和查看,过期后不能调用成功。
使用refresh_token换取用户令牌地址格式为:
https://{IDaaS_server}/oauth/token?grant_type=refresh_token&refresh_token={refresh_token}&client_id={client_id}
注: OAuth支持多种grant_type 这里使用的是refresh_token模式。
接口说明:刷新用户令牌
请求方式:POST
请求参数
参数 |
类型 |
是否必选 |
示例值 |
描述 |
---|---|---|---|---|
grant_type |
String |
是 |
refresh_token |
固定值 |
refresh_token |
String |
是 |
eyJhbGciOi… |
刷新token |
client_id |
String |
是 |
oauth2 client_id |
OAuth2 client_id |
返回参数示例:
{
"access_token": "eyJhbGc1NiIs...",
"token_type": "bearer",
"refresh_token": "eyJhbGciOi...",
"expires_in": 7199,
"scope": "read",
"jti": "956901cd-5669-411f"
}
参数 |
类型 |
示例值 |
描述 |
---|---|---|---|
access_token |
String |
eyJhbGc1NiIs… |
Access Token |
token_type |
String |
bearer |
Token 类型 |
refresh_token |
String |
eyJhbGciOi… |
刷新token |
expires_in |
String |
7199 |
Access Token 过期时间 |
scope |
String |
read |
申请的权限范围 |
jti |
String |
956901cd-5669-411f |
当令牌是jwt格式时,该值表示令牌的id |
返回异常示例
client_id 错误:
{
"error": "invalid_client",
"error_description": "Bad client credentials"
}
refresh_token 错误:
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
grant_type 错误:
{
"error": "unsupported_grant_type",
"error_description": "Unsupported grant type: refresh_token1"
}
3. 常见QA¶
3.1. 如何强制用户登录认证?¶
3.1.1. 方法一【注销session】¶
在登录接口增加prompt参数, 当prompt=login则强制跳转登录页 , 也就是在下图 Authorize URL后面增加”&prompt=login”则不论用户是否已经登录认证,都会展示登录页,用户必须进行一次认证,才可继续单点登录流程,地址格式如下:
https://{IDaaS_server}/oauth/authorize?response_type=code&scope=read&client_id={client_id}&redirect_uri={your_register_callback_uri}&state={state}&prompt=login
3.1.2. 方法二【注销session或token】¶
IDP 4.11 及以上的版本才支持。
在应用详情中找到 SLO 地址,可以通过该地址清除 IDaaS session、cookie、token,达到登出 IDaaS 的目的。
该接口支持GET、POST访问,POST 时 redirect_url 和 access_token 可使用表单提交,如果不借助浏览器跳转只使用接口调用,则只能清除 access_token。
redirect_url 为登出后跳转地址为可选项,若有值,则登出后重定向到该地址,若没值,则重定向到IDaaS登录页(在该登录页登录后会进入 IDaaS 主页)。
地址格式如下:
https://{IDaaS_server}/public/sp/slo/{应用ID}?redirect_url={redirect_url}&access_token={access_token}
3.2. 如何判断用户是否已经在IDaaS平台中登录?¶
在登录接口增加prompt参数, 当prompt=none则获取是否在IDaaS平台登录 , 也就是在下图 Authorize URL后面增加”&prompt=none”,如果能直接跳转Redirect URI标识已IDaaS登录,未登录则会响应interaction_required错误
地址格式如下:
https://{IDaaS_server}/oauth/authorize?response_type=code&scope=read&client_id={client_id}&redirect_uri={your_register_callback_uri}&state={state}&prompt=none
响应如下则代表未登录
3.3. 如何保存初始发起页面?¶
在SP发起一个SSO请求的时候, SP需要能够把对应的URL, 保存在内存中, 并和OAuth 中的State 参数关联起来。 这样, 在IDaaS返回State 后, 可以找到当初的URL, 并跳转到这个URL, 实现DeepLinking。比如使用了JAVA的Spring框架的话, 可以用SavedRequest来完成。