自定义门户¶
最后更新:2021-12-02
场景介绍¶
术语定义¶
参考:IDP中产品术语介绍
什么是自定义门户¶
门户一般指企业员工统一进入业务系统的入口系统网站,简称portal。门户一般带有企业自主风格的统一登录界面,相对于使用IDaaS提供的统一登录页,一般都有改造登录页的需求。
注意:这并不意味着自定义门户登录界面就一定都是自己做,也有少数需要使用IDaaS统一登录页的情况。
自定义门户和业务系统接入IDaaS区别¶
业务系统,即SP, 一般仅会接入IDaaS的单点登录和退出功能。
自定义门户则不同,普遍会获取用户在IDaaS平台上拥有访问权限的应用列表,再将列表放在自己的门户网站上展示,通过访问列表中的应用触发单点登录到对应系统。当用户从门户退出时,会触发清理IDaaS相关的用户会话(或其他已经登录的业务系统),又重新回到门户的登录页。
接入流程¶
接入顺序主要为,创建OAuth2应用 -> 对接IDaaS登录 -> 调用IDaaS获取用户授权的应用接口 > 对接IDaaS退出 > 测试
对于门户来说,对接IDaaS登录可分为:直接调用IDaaS登录接口和使用IDaaS统一登录页两种情况,详细参考后续内容
以下为门户接入IDaaS时使用IDaaS的登录接口的完整时序图
1.创建OAuth2应用¶
使用管理员登录IDaaS平台,在添加应用,标准协议中选择OAuth2模板,点击添加,如下图
Redirect URI:授权码模式下,门户需要进行接收IDaaS产生的临时code地址,凭借临时code,门户可再次通过授权码得到用户的访问令牌access_token GrantType: 此处选择 authorization_code
2.对接IDaaS登录¶
门户接入IDaaS登录时,可分为两种情况:
第一种:门户使用自己的登录页,用户登录时调用IDaaS的接口进行登录,IDaaS返回用户的访问令牌access_token
第二种:门户使用IDaaS的统一登录页。用户登录时直接引导到IDaaS的统一登录页,后续门户获取IDaaS用户访问令牌access_token
注意:无论选择哪种,其最终目的都是为了获取到用户的访问令牌access_token
¶
- 调用IDaaS的接口进行登录¶
注意调用此接口,使用的client_id和client_secret为步骤1创建OAuth2应用的API Key和API Secret,在详情** ‘API’ **处复制
请求地址: /oauth/token
请求方法: POST
**接口描述:**第三方/门户系统调用IDAAS登录接口,进行认证用户,IDAAS返回认证结果,若成功,返回用户的access_token,(access_token为调用IDAAS用户相关API的接口凭证)失败时将返回对应错误。
请求参数
Headers
参数名称 |
参数值 |
是否必须 |
示例 |
备注 |
---|---|---|---|---|
Content-Type |
application/x-www-form-urlencoded |
是 |
Body
参数名称 |
参数类型 |
是否必须 |
示例 |
备注 |
---|---|---|---|---|
client_id |
text |
是 |
0d855656533d0e6988c8ea35c0a52phKHIFq55p |
应用APIKey |
client_secret |
text |
是 |
KWsRAi675ZSYbycbMg7ofmo61ov8hQst6kOi8F |
应用APISecret |
grant_type |
text |
是 |
password |
授权类型,固定password |
scope |
text |
是 |
read |
授权范围,固定read |
username |
text |
是 |
zhangsan |
用户名 |
password |
text |
是 |
123456 |
密码(明文) |
返回数据
名称 |
类型 |
是否必须 |
默认值 |
备注 |
其他信息 |
---|---|---|---|---|---|
access_token |
string |
必须 |
用户令牌 |
||
token_type |
string |
必须 |
令牌类型 |
||
expires_in |
number |
必须 |
有效期,单位s |
||
scope |
string |
必须 |
授权范围 |
||
jti |
string |
非必须 |
若令牌格式为jwt,代表令牌id |
补充说明
curl请求示例:
curl -X POST 'http://127.0.0.1/oauth/token?client_id=0d8e9ae61c23533d0e6988c8ea35c0a52phKHIFq55p&client_secret=KWsRAiZhEajM5ZSYbycbMg7ofmo61ov8hQst6kOi8F&grant_type=password&scope=read&username=zhangsan&password=123456' -H 'content-type: application/x-www-form-urlencoded'
认证成功时返回格式如下:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ....",
"token_type": "bearer",
"expires_in": 43199,
"scope": "read",
"jti": "b9c7214b-d900-41c6-9fa0-e76293a1e70d"
}
认证失败时返回格式如下:
client_id或client_secret错误
{
"error": "invalid_client",
"error_description": "Bad client credentials"
}
用户名或密码错误
{
"error": "invalid_grant",
"error_description": "Bad credentials"
}
- 使用IDaaS统一登录页登录¶
使用IDaaS统一登录页登录时,其流程为标准的Oauth2授权码模式获取用户访问令牌access_token。
注意调用此接口,使用的client_id和client_secret为步骤1创建OAuth2应用的client_id和client_secret,在查看详情中展示的client_id,client_secret
详细获取流程如下
第二步: code换取access_token¶
门户接收到code后,可凭此code换取用户的访问令牌access_token
详细地址如下:
http(s)://{IDaaS_server}/oauth/token?grant_type=authorization_code&code={code}&client_id={client_id}&client_secret={client_secret}&redirect_uri={redirect_uri}
请求方法为POST
参数名称 |
参数类型 |
是否必须 |
示例 |
备注 |
---|---|---|---|---|
grant_type |
text |
是 |
authorization_code |
响应类型,固定 |
code |
text |
是 |
WgWQe6 |
为第一步用户登录成功后IDaaS回调至Redirect URI上请求参数code值 |
client_id |
text |
是 |
1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U |
OAuth2应用详情 |
client_secret |
text |
是 |
vdDtBPNiHRCz8S267fURHYnr2bMlgL7ahqrpgrDscG |
OAuth2应用详情 |
redirect_uri |
text |
是 |
http%3A%2F%2Foa.com%2Fcallback |
OAuth2应用详情 |
补充说明
curl请求示例:
curl -X POST 'http://127.0.0.1:81/oauth/token?grant_type=authorization_code&code=dIKvfA&client_id=1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U&client_secret=vdDtBPNiHRCz8S267fURHYnr2bMlgL7ahqrpgrDscG&redirect_uri=http%3A%2F%2Foa.com%2Fcallback'
成功时响应
{
"access_token":"eyJhbGciO...",
"token_type":"bearer",
"refresh_token":"eyJhbGciOiJIUzI1...",
"expires_in":7199,
"scope":"read",
"jti":"17147278-7f3e-45f2-be6f-8105c4334a30"
}
失败时响应:
client_id或client_secret错误 ```json { "error":"invalid_client", "error_description":"Bad client credentials" } ```
code错误
{
"error":"invalid_grant",
"error_description":"Invalid authorization code: dIKvfA"
}
code失效
{
"error":"invalid_grant",
"error_description":"authorization code expired: WgWQe6"
}
3.调用IDaaS获取用户授权的应用接口¶
请求地址:/api/bff/v1.2/enduser/portal/sso/app_list
**请求方法:**GET
**接口描述:**通过用户的访问令牌access_token获取用户所有授权可单点登录的应用。
响应主要字段格式示例如下
{ “success”:true,
“code”:“200”,
“message”:null,
“requestId”:“1620373926480$c1112d2b-114d-237b-59ad-fe2f5d18eee8”,
“data”:{
“authorizationApplications”:[
{
“name”:“OAuth2”,
“applicationId”:“goverplugin_oauth2”,
“applicationUuid”:“2761c1936dbddbef56f3cec7bc885da6grO48gmRExY”,
“idpApplicationId”:“plugin_oauth2”,
“logoUuid”:“a4d0e4f81f4ad3f882a0128fc0d62a19mQzh887ke2p”,
“startUrl”:“http://127.0.0.1:81/api/bff/v1.2/enduser/portal/sso/go_2761c1936dbddbef56f3cec7bc885da6grO48gmRExY”,
“createTime”:“2021-04-20 14:09”,
“description”:“OAuth 是一个开放的资源授权协议,应用可以通过 OAuth 获取到令牌 access_token,并携带令牌来服务端请求用户资源。应用可以使用 OAuth 应用模板来实现统一身份管理。”,
“enabled”:true,
“supportDeviceTypes”:[“WEB”],
“existAccountLinking”:false,
“enableTwoFactor”:false,
“display”:true,
“defaultLinking”:true,
“autoLogin”:false,
“classifyUuid”:null,
“orderId”:0
}
]
}
}
如果想要实现单点登录,请将用户令牌access_token拼接到应用的startUrl上,如
http://127.0.0.1:81/api/bff/v1.2/enduser/portal/sso/go_2761c1936dbddbef56f3cec7bc885da6grO48gmRExY?access_token={access_token}
请求参数
Headers
参数名称 |
参数值 |
是否必须 |
示例 |
备注 |
---|---|---|---|---|
Authorization |
Bearer {access_token} |
是 |
eyJhbGciOiJI… |
用户令牌access_token,请求头中格式为 |
返回数据
名称 |
类型 |
是否必须 |
默认值 |
备注 |
其他信息 |
---|---|---|---|---|---|
success |
boolean |
必须 |
是否成功 |
||
code |
string |
必须 |
错误码,200则视为成功 |
||
message |
null |
必须 |
错误信息,code为非200时,可参考此值看错误信息 |
||
data |
object |
必须 |
|||
├─ |
object [] |
必须 |
授权可单点登录的应用列表 |
item 类型:object |
|
├─ |
string |
必须 |
应用名 |
||
├─ |
string |
必须 |
应用ID |
||
├─ |
string |
必须 |
应用uuid |
||
├─ |
string |
必须 |
应用模板ID |
||
├─ |
string |
必须 |
应用图标uuid |
||
├─ |
string |
必须 |
应用单点登录地址 |
||
├─ |
string |
必须 |
创建时间 |
||
├─ |
string |
必须 |
应用模板描述 |
||
├─ |
boolean |
必须 |
是否启用 |
||
├─ |
string [] |
必须 |
支持的设备类型 |
item 类型:string |
|
├─ |
非必须 |
||||
├─ |
boolean |
必须 |
是否存在子账户 |
||
├─ |
boolean |
必须 |
应用是否开启二次认证 |
||
├─ |
boolean |
必须 |
是否显示 |
||
├─ |
number |
必须 |
应用排序号 |
4.对接IDaaS退出¶
当门户退出时,可调用IDaaS提供的全局统一退出地址,并传递一个门户portal的登录地址redirect_url,待IDaaS注销会话和注销用户的访问令牌后,则会重定向至门户的登录地址,接口地址信息如下
请求地址:/public/sp/slo/{appId}
请求方式:GET,POST
请求参数:{appId} - 应用ID,此处为门户应用,如:idaasoauth2
redirect_url - 退出成功后跳转的URL地址,可选(若不传或为空则跳转回IDP登录页),此处传递门户登录地址,注意使用urlencode
access_token - IDP签发的用户令牌access_token,可选,若有值则将access_token置为无效状态
请求示例:http://127.0.0.1:81/public/sp/slo/idaasoauth2?access_token={access_token}&redirect_url={portal_login_url}
注意事项:推荐使用POST请求方式,将参数redirect_url与access_token放在form表单中提交。
更多信息参考文档全局统一退出场景
Q&A¶
1.如何通过用户access_token获取用户更多信息¶
在获取到AT(Access Token)后,门户可以接着向IDaaS发送进一步的请求, 以获取到用户信息
①发送GET请求到https://{IDaaS_server}/api/bff/v1.2/oauth2/userinfo?access_token={access_token}
{access_token}替换为获取到的用户访问令牌
②从返回参数即可获取userinfo信息,详细API信息
Request URI: https://{IDaaS_server}/api/bff/v1.2/oauth2/userinfo
接口说明: 获取用户详细信息
请求方式: GET
请求参数
参数 |
类型 |
是否必选 |
示例值 |
描述 |
---|---|---|---|---|
access_token |
String |
是 |
eyJhbGc1NiIs… |
Access Token |
返回参数响应示例
{
"success": true,
"code": "200",
"message": null,
"requestId": "149DA248-8F49-4820-B87A-5EA36D932354",
"data": {
"sub": "823071756087671783",
"ou_id": "2079225187122667069",
"nickname": "test",
"phone_number": 11136618971,
"ou_name": "阿里云IDAAS",
"email": "test@test.com",
"username": "test"
}
}
参数说明
参数 |
类型 |
示例值 |
描述 |
---|---|---|---|
success |
boolean |
true |
是否成功 |
code |
String |
200 |
状态码 |
message |
String |
null |
返回消息 |
requestId |
String |
B3776BB1-930F-4581-B4C3-18F2D7D136CA |
请求ID |
data |
Object |
响应数据 |
|
└sub |
String |
823071756087671783 |
子编号 |
└ouid |
String |
2079225187122667069 |
父组织ID |
└nickname |
String |
test |
昵称 |
└phone_number |
String |
11136618971 |
手机号 |
└ou_name |
String |
阿里云IDAAS |
父组织名称 |
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. |
发生未知错误 |
2.门户没有用户的密码怎么登录¶
适用于无密码登录场景,使用秘钥加密唯一标识,到IDaaS换取用户access_token
支持的算法:SM2、RSA、AES
支持的唯一标识:用户名、手机号、邮箱、外部ID
1).上传插件
2).创建应用
3).获取秘钥
无需授权、启用应用、查看应用详细。
创建应用时,会自动生成 SM2密钥对、RSA密钥对、AES秘钥,使用对应秘钥调用认证接口
SM2:使用 idsmanager-sm2 包生成
RSA:使用 idsmanager-oidc 包生成(长度2048)
AES:随机生成(长度32)
4).调用接口
接口说明
通过解析加密值得到用户唯一标识,找到用户生成access_token返回。
请求URL
{Host}/api/public/bff/v1.2/application/plugin_mutualtrust/login
提交方式
POST
Content-Type: application/json
请求参数
参数名 |
参数值 |
必填 |
备注 |
---|---|---|---|
algorithmType |
加密算法 |
是 |
encryptedIdentity值的加密算法 |
encryptedIdentity |
加密值 |
是 |
用户唯一标识加密后的值。 |
identityType |
唯一标识类型 |
是 |
用户的身份类型: |
purchaseId |
应用ID |
是 |
步骤3应用详细中获取 |
请求示例
POST /api/public/bff/v1.2/application/plugin_mutualtrust/login
Host: {Host}
Content-Type: application/json
{
"algorithmType":"SM2",
"encryptedIdentity":"BLTCpPB5wsl/ws2QlJrkijihIqQxg0CQzM2qVdetncw/sM0Edd2kLKi84QM/dqMyKISmTBEP53hvvjbHtR7tLzWvkSrMv2OkCYCs50LkjY4JiVVT9M+/PjvVpBcNij7g714+imp8Pdg=",
"identityType":"USERNAME",
"purchaseId":"zhangdhplugin_mutualtrust2"
}
调用成功响应
{
"success": true,
"code": "200",
"message": null,
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZW50ZXJwcmlzZV9tb2JpbGVfcmVzb3VyY2UiLCJiZmZfYXBpX3Jlc291cmNlIl0sImV4cCI6MTYzNzg2ODI1MCwidXNlcl9uYW1lIjoiemhhbmdkaCIsImp0aSI6IjY4YjBiZmFkLWEzYTItNGRlYi1hZTg4LWYxNGY0YjRjMzM0MSIsImNsaWVudF9pZCI6Ijk2NzRhYjczYjZjOTc4YzE1ZTBiNThjNTY2NmJhNTVlWlJ4SVpqcUNCMzciLCJzY29wZSI6WyJyZWFkIl19.BUznu8_oNGcVcQpMmVNO6bEn7sO11RdzocSS3HrfbYg"
}
}
成功响应参数
参数名 |
参数名称 |
备注 |
---|---|---|
success |
响应结果 |
true 成功 false 失败 |
code |
响应码 |
详细见响应码列表 |
message |
描述 |
调用失败时返回错误信息 |
data |
Object |
|
access_token |
用户的token |
可以用此获取用户详细、拥有有权限的应用列表等信息 |
调用失败响应
{
"success": true,
"code": "400100",
"message": "无效的algorithmType"
}
失败响应参数
响应码 |
响应信息 |
备注 |
---|---|---|
200 |
成功 |
|
500 |
系统繁忙 |
请联系管理员 |
400100 |
无效的algorithmType |
根据接口规范修改 |
400101 |
无效的encryptedIdentity |
加密算法或加密内部不正确 |
400102 |
无效的encryptedIdentity,已过期 |
timestamp已超过10分钟或加密服务器与验证服务器时间差大于10分钟 |
400103 |
无效的用户唯一标识 |
用户在系统中未找到 |
400104 |
无效的identityType |
根据接口规范修改 |
400105 |
无效的purchaseId |
需要管理员在应用详细中查看 |
400106 |
无效的purchaseId,应用未启用 |
应用未启用需要管理员在应用管理中启用 |
400107 |
无效的用户唯一标识,用户已禁用 |
账户禁用需要管理员在管理页面启用 |
400108 |
无效的用户唯一标识,用户已锁定 |
账户锁定需要管理员在管理页面解锁 |
5).加密示例
SM2:
Github demo工程:https://github.com/aliyun-idaas/idp4-developer-sm2-utils
public static void main(String[] args) throws Exception {
//SM2的加密与解密
String publicKey = "BMzrnltiDD/CCfr6r90Rf/qHn8Wc0LqPGm4rkSezjgEOUaxD5eNoBQw4xbdKLIwUj7UpfAtjw6ooH3Duu2qY+Cw=";
String encodeParam = SM2Encrypt.encryptUseBase64(publicKey, System.currentTimeMillis() + "_zhangsan", true);
System.out.println("加密后: " + encodeParam);
}
RSA:
package com.idaas.controller;
import org.jose4j.json.JsonUtil;
import org.jose4j.jwk.RsaJsonWebKey;
import sun.misc.BASE64Encoder;
import javax.crypto.Cipher;
import java.security.PublicKey;
public class RSADemo {
public static void main(String[] args) throws Exception {
//RSA公钥字符串 注册应用时提供
String publicKeyString = "{\"kty\":\"RSA\",\"kid\":\"2929530392857633439\",\"alg\":\"RS256\",\"n\":\"zhJ0pd63SMsgnoCnk_smt3-ePdjiEjJtveqH2UFLgGomaweA54qpTquYe_XyemoCeegRpwOJFd44NtdJgCMkoIXqVTkrLnEvaK-rSqAkPfsWvwv2QUiicnsb1hpXQv8rOIhQ9Txuae92vp4ZV9XZmf3phTD-hg8YZw1bjkUbyua_veh6oGphAvZoHntFJIrKIyf_oftwGtz9xpiLzbX3saOMq2NoDhUslVT4p2wNN3hVFKRwrCqxOcwTW0sARRAWm-jPJwhQuVa4i01QnHhN4KalHRPX4YVaLOPp57_ajIFOQCd6MFvTGW3i05GdAEbajNQEuYlrugUk-YXOnzDcqQ\",\"e\":\"AQAB\"}";
PublicKey publicKey = new RsaJsonWebKey(JsonUtil.parseJson(publicKeyString)).getPublicKey();
//身份证号
String usernameBefore = System.currentTimeMillis() + "_zhangsan";
//加密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] bytes = cipher.doFinal(usernameBefore.getBytes("utf-8"));
//转为base64(jdk默认)
BASE64Encoder encoder = new BASE64Encoder();
//最终传递的参数
String username = encoder.encode(bytes);
System.out.println(username);
}
}
POM引用
<!-- https://mvnrepository.com/artifact/org.bitbucket.b_c/jose4j -->
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.7.6</version>
</dependency>
AES:
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class xxxx {
public static void main(String[] args) throws Exception {
String username = System.currentTimeMillis() + "_zhangsan";
String key = "XrYOSEYikzf1aww9Szp3QnZ4dB3LkkGq";
byte[] raw = key.getBytes("UTF-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(username.getBytes("UTF-8"));
//转为base64(jdk默认)
BASE64Encoder encoder = new BASE64Encoder();
System.out.println(encoder.encode(encrypted));
}
}
附录¶
IDaaS Portal接入Demo¶
链接地址:https://github.com/aliyun-idaas/idp4-developer-portal-demo , 克隆后请先查看README.md文档参考步骤修改对应参数进行测试。