自定义门户

最后更新: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模板,点击添加,如下图
image.png

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’ **处复制
image.png

请求地址: /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
image.png
详细获取流程如下

第一步: 构造Authorize URL,获取用户授权code

门户可通过一个按钮或其他方式,触发浏览器打开Authorize URL,用户登录成功后,会跳转到回调地址Redirect URI,并把code参数一同转发过去。
Authorize URL 格式为
http(s)://{IDaaS_server}/oauth/authorize?response_type=code&scope=read&client_id={client_id}&redirect_uri={redirect_uri}&state={state}

参数名称

参数类型

是否必须

示例

备注

response_type

text

code

响应类型,固定
code

scope

text

read

授权范围,固定read

client_id

text

1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U

OAuth2应用详情Client Id

redirect_uri

text

http%3A%2F%2Foa.com%2Fcallback

OAuth2应用详情
Redirect URI
注意该地址需要进行url_encode

state

text

10ff0be64971c07f893afc332877f68arS8FH2iyZni_idp

随机值,若为门户发起,可自定义随机一个字符串,若为IDP发起,也是随机一个值,但会带上_idp后缀



**补充说明**

浏览器访问完整地址示例如下 ``` http://127.0.0.1:81/oauth/authorize?response_type=code&scope=read&client_id=1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U&redirect_uri=http%3A%2F%2Foa.com%2Fcallback&state=10ff0be64971c07f893afc332877f68arS8FH2iyZni_idp ``` 访问完成后,会跳转至IDaaS登录页。用户登录成功后,会跳转到回调地址Redirect URI,并把code参数一同转发过去。
![访问Authorize URL获取code.gif](resources/txcu4a_004.png)

第二步: 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

响应类型,固定
authorization_code

code

text

WgWQe6

为第一步用户登录成功后IDaaS回调至Redirect URI上请求参数code值

client_id

text

1d0f8349ad981fe9a306e39d86a3a124uHW6fd0sC7U

OAuth2应用详情
Client Id

client_secret

text

vdDtBPNiHRCz8S267fURHYnr2bMlgL7ahqrpgrDscG

OAuth2应用详情
Client Secret

redirect_uri

text

http%3A%2F%2Foa.com%2Fcallback

OAuth2应用详情
Redirect URI
注意该地址需要进行url_encode


补充说明
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,请求头中格式为
Bearer {access_token}

返回数据

名称

类型

是否必须

默认值

备注

其他信息

success

boolean

必须

是否成功

code

string

必须

错误码,200则视为成功

message

null

必须

错误信息,code为非200时,可参考此值看错误信息

data

object

必须

├─
authorizationApplications

object []

必须

授权可单点登录的应用列表

item 类型:object

├─
name

string

必须

应用名

├─
applicationId

string

必须

应用ID

├─
applicationUuid

string

必须

应用uuid

├─
idpApplicationId

string

必须

应用模板ID

├─
logoUuid

string

必须

应用图标uuid

├─
startUrl

string

必须

应用单点登录地址

├─
createTime

string

必须

创建时间

├─
description

string

必须

应用模板描述

├─
enabled

boolean

必须

是否启用

├─
supportDeviceTypes

string []

必须

支持的设备类型

item 类型:string

├─

非必须

├─
existAccountLinking

boolean

必须

是否存在子账户

├─
enableTwoFactor

boolean

必须

应用是否开启二次认证

├─
display

boolean

必须

是否显示

├─
orderId

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

父组织名称

└email

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).上传插件 image.png 2).创建应用 image.png 3).获取秘钥 无需授权、启用应用、查看应用详细。
创建应用时,会自动生成 SM2密钥对、RSA密钥对、AES秘钥,使用对应秘钥调用认证接口

SM2:使用 idsmanager-sm2 包生成
RSA:使用 idsmanager-oidc 包生成(长度2048)
AES:随机生成(长度32)
image.png 4).调用接口 接口说明
通过解析加密值得到用户唯一标识,找到用户生成access_token返回。
请求URL
{Host}/api/public/bff/v1.2/application/plugin_mutualtrust/login
提交方式
POST
Content-Type: application/json
请求参数

参数名

参数值

必填

备注

algorithmType

加密算法

encryptedIdentity值的加密算法
值为:SM2、RSA、AES

encryptedIdentity

加密值

用户唯一标识加密后的值。
格式:13位时间戳+下划线+唯一标识,截取时使用第一个下划线分割,如果时间间隔大于10分钟则返回错误
例如:1637835035000_zhangsan

identityType

唯一标识类型

用户的身份类型:
USERNAME (用户名)
PHONE_NUMBER(用户手机号)
EXTERNAL_ID(用户外部ID)
EMAIL (用户邮箱)

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文档参考步骤修改对应参数进行测试。