# 系统集成

HOS门户系统是BS系统,默认用谷歌浏览器或者东华医为浏览器(谷歌内核)登录使用。

BS系统集成到门户系统,根据项目需求,可以选择HOS内部窗口打开、浏览器新页签打开、浏览器新窗口打开或者指定浏览器打开(其他浏览器如ie等)。

下面以OA系统为例,介绍接入OAuth2的整个步骤,在实际接入中,需要将各系统的ip端口改为真实数据。

  • 统一用户服务地址:http://ip:8001
  • 统一认证服务地址:http://ip:8002
  • OA应用的前端地址:http://ip:8111

# 应用系统提供系统信息

应用系统需要向医为项目组提供系统信息,由医为项目组完成系统注册。应用系统需要提供:

  • 系统名称
  • 系统编码
  • 系统架构(BS)
  • 打开方式(包含内部窗口、浏览器新页签、浏览器新窗口、指定浏览器等,非必须,默认为内部窗口打开)
  • 访问地址(访问应用系统的地址)
  • 回调地址(应用系统用于接收授权码的地址,当应用系统重定向到统一认证获取授权码时,统一认证会回调该地址并携带授权码)
  • 单点退出地址(非必须)

注意:

  • 访问地址与回调地址可以相同也可以不相同,其流程不同,详情参考流程介绍
  • 回调地址不要包含“#”“?”特殊符号,回调地址也不要配置query参数。
  • 访问地址与回调地址相同时,该地址会被调用两次,需要应用系统进行区分,并分别进行操作:第一次调用没有授权码参数(code),应用系统需要重定向到统一认证地址,获取授权码;第二次统一认证重定向到该地址,会携带授权码参数(code),应用系统拿到授权码调用获取令牌接口(本文应用系统获取授权码有介绍)。
  • 当应用系统提供了退出接口,当门户退出时统一认证系统会调用各系统的退出接口,并将访问令牌作为参数传给应用系统,应用系统需要根据访问令牌进行自身系统的退出处理(详情见应用系统退出)。

# 提供应用系统注册信息和测试账号

应用系统提供系统信息给医为项目组后,项目组会分别在统一用户平台和门户中注册该应用系统,并提供如下信息:

  • AppId(客户端注册的AppId,对应接口参数中的client_id)
  • AppSecret(客户端注册的AppSecret,对应接口参数中的client_secret)
  • 门户系统登录地址
  • 门户系统登录测试账号和密码

提供信息示例如下:

  • AppId:B8270EB95xU9
  • AppSecret:105w43q5v3X44L2c
  • 门户登录地址:https://192.168.1.101:8000(项目组会提供实际的ip信息)
  • 测试账号和密码:ys01/123456(以项目组实际提供的测试账号密码为准)

# 门户中点击应用系统图标

应用系统开发人员使用本文上一步骤中提供的测试账号密码,登录门户系统,进入单点登录页面,找到应用系统(本文以oa协同办公系统为例),鼠标左键点击图标,跳转应用系统页面,这里门户系统会跳转本文应用系统提供系统信息中应用系统提供的访问信息,并携带门户默认传递的参数,如下图所示:

跳转OA应用系统访问地址示例:

http://ip:8111?windowId=b2669876cf398abe760cbd10b0c05543&showTitle=true&&sessionKey=admin_4103&language=zh&MACAddr=9C:6B:00:48:B1:98&IPAddress=10.1.30.51&DNSHostName=administrator

问号后为门户系统传递的参数,各应用系统根据需要选择是否使用。

参数说明:

  • windowId:内部窗口id
  • showTitle:内部窗口是否展示标题(true:展示标题;false:隐藏标题)
  • sessionKey:HOS缓存区缓存key值
  • Language:语言编码
  • MACAddr:客户端mac地址
  • IPAddress:客户端ip
  • DNSHostName:客户端名称

# 应用系统获取授权码

在上一步骤中点击应用系统图标,浏览器跳转URL到应用系统,应用系统接收到访问请求后,判断当前用户是否已经登录,如果已登录,跳过本步骤,直接进入应用系统根据用户信息登录完成单点登录;如果当前用户未登录,需要获取授权码参数(code),根据访问地址与回调地址是否相同,分别进行以下如下处理。

# 访问地址与回调地址相同

应用系统接收到URL访问请求后,需要判断访问请求中是否携带授权码(code)参数,根据是否携带进行分别如下处理:

  • 没有携带授权码

如果浏览器请求中没有携带授权码(code)参数,需要应用系统发起重定向请求到统一认证地址来获取授权码,统一认证接收到请求后,会携带授权码再次访问(url重定向)应用系统的回调地址,具体接口细节参考本章节获取授权码重定向获取授权码。

  • 携带授权码

如果浏览器请求中携带授权码(code)参数,则可跳过本章节,直接进行应用系统获取访问令牌获取访问令牌的步骤。

# 访问地址与回调地址不同

应用系统接收到访问地址URL的访问请求后,直接重定向到统一认证地址,去获取授权码,统一认证接收到请求后,会携带授权码再次访问(url重定向)应用系统的回调地址。

# 【获取授权码】

  • 基本信息

用途:由应用系统重定向到该地址,用于向统一认证请求获取授权码,统一认证完成登录后,由统一认证系统跳转到应用系统的回调地址并携带授权码。 请求地址:https://统一认证服务ip:port/oauth/authorize

  • 请求方式 拼接好Query参数,然后重定向到拼接后的地址
参数字段 必填 示例 字段信息
response_type code 固定值
client_id 19sexv587l02 应用标识,应用注册时生成的AppId信息
redirect_uri http://192.168.101.100:8111
/api/security/login/oauth2
/code/messaging-client-oidc
回调地址,此地址用来接收授权码
scope openid 固定值
state ypdy2eBAHqtn0xv5s7MbLRh
9WQrQj0DAKJxFcDq5ceg%3D
自定义参数,一般作为维护应用系统跳转统一认证前状态的值;返回授权码码时,会同时原值返回
  • 请求结构示例
拼接后的完整请求url:
http://统一认证服务ip:port/oauth/authorize?response_type=code&client_id=140m9Bweb0a87a3&scope=openid%20profile&state=J18BgGcpbVy5goIcnxZbhyBlrE5gz6Fj2s221DM7Ltk%3D&redirect_uri=http://192.168.101.100:8111/api/security/login/oauth2/code/messaging-client-oidc&nonce=Fi3Pwy_nLH3tZofp26fh84kWBCpvKhty-6OL5fhU9UM
前端请求方式:window.location.href =url
后端请求方式:response.sendRedirect(url)
  • 正常跳转示例

正常情况下请求统一认证成功后,统一认证系统会携带code参数回跳到应用系统的回调地址,其中code参数即为授权码,示例如下:

http://ip:8111/api/security/login/oauth2/code/messaging-client-oidc?code=jgJQbH6wroTw-Ob_Caa-txmbcI8kj8SKeyu7K5zYGHIJEZbRdS5YKK2II5q4Hw_hsHbhDVsMz-VFWwr3_fX7aSp93rNLAJhKyLgCa6ZCil2SLSsvVsbbaqqd_G8JQfyN&state=jhWHgas4u2EA3ov85C3QbYVJ-5ra_C40UciuDxClWgc%253D
  • 异常返回页面示例

异常情况下会跳转到统一认证的异常页面,具体异常如下:

(1)client_id错误

{
	"code":   "102-001-001-002""msg":   "无效的应用,client_id参数有误""data":  null,
	"uri":  null
}

(2)应用系统认证配置的回调地址配置时包含特殊符号,或者接口中传递的redirect_uri与认证配置的回调地址不一致

{
    "code": "102-001-001-011""msg": "回调地址异常,redirectUri参数有误""data": null,
    "uri": null
}

(3)scope传的值和应用系统的认证配置的作用域值不一致

{
    "code": "102-001-001-015""msg": "不支持的Scope类型""data": null,
    "uri": null
}

(4)scope传的值不是openid

{
    "code": "102-001-001-030""msg": "scope参数不能为空且必须包含openid""data": null,
    "uri": null
}

# 应用系统获取访问令牌

在本文上一步骤中,成功获取到授权码(code)之后,应用系统需要使用获取到的授权码,调用统一认证系统的接口,获取访问令牌。需要注意的是:授权码参数只能使用一次,调用完【获取访问令牌】接口之后,不管是否成功,授权码都会失效,失效后需要重新获取。

# 【获取访问令牌】接口

  • 基本信息

用途说明:应用系统调用该接口请求获取访问令牌

请求方式:POST + Basic认证 + Query传参

接口地址:https://统一认证服务ip:port/api/oauth2/token

  • 请求方式

请求头 :Basic认证

延申了解:Basic认证是最常见的HTTP认证方式之一。在Basic认证中,客户端发送请求时,会在请求头中包含一个"Authorization"字段,该字段包含了经过Base64编码的用户名(client_id)和密码(client_secret)。服务器收到请求后,会解码该字段并验证用户名和密码是否正确。

client_id:应用注册时生成的AppId信息
client_secret:应用注册时生成的AppSecret信息

java代码设置Basic认证示例如下:

import org.springframework.http.HttpHeaders;
// 设置Basic Auth参数
String authString = clientId + ":" + clientSecret;
String authHeaderValue = "Basic " + Base64.getEncoder().encodeToString(authString.getBytes(StandardCharsets.UTF_8));
// 设置请求头信息
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add("Accept",MediaType.APPLICATION_JSON.toString());
headers.add("Authorization",authHeaderValue);
  • Query参数
参数字段 必填 示例 字段信息
code jgJQbH6wroTw-Ob_Caa-
txmbcI8kj8SKeyu7K5zYGHIJEZbRd
S5YKK2II5q4Hw_hsHbhDVsMz-VF
Wwr3_fX7aSp93rNLAJhKyLgCa6Z
Cil2SLSsvVsbbaqqd_G8JQfyN
授权码,获取授权码返回的code值
redirect_uri http://192.168.101.100:8111
/api/security/login/oauth2
/code/messaging-client-oidc
获取授权码接口的回调地址保持一致
grant_type authorization_code 请求类型,固定authorization_code
  • 请求结构示例

以postman调用为例,传入Username和Password进行Basic认证(需要注意,postman是以下图方式进行Basic认证,java代码则以上边的示例为准)

Username:client_id(应用注册时生成的AppId信息)
Password: client_secret(应用注册时生成的AppSecret信息)

  • 返回参数
参数字段 字段类型 字段信息
code String 响应状态
msg String 响应信息
data Object 响应数据对象
access_token String 访问令牌
refresh_token String 刷新令牌,开启刷新模式才会返回
code String 授权码
scope String 权限
id_token String Oidc协议才会返回
token_type String Token类型
expires_in Long 过期时间
client_id String 客户端id
  • 返回结构示例

(1)正确返回时

{
    "code": "200",
    "msg": "成功",
    "data": {
        "access_token": "eyJraWQiOiIwMWQ5Yjg0Zi0zN2QyLTQ1ODgtOGE5My1mYjZkMmI3MGE0YjYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6IjE0MG05QndlYjBhODdhMyIsIm5iZiI6MTY5NDE0ODg3NSwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSJdLCJpc3MiOiJodHRwOi8vMTE0LjI1MS4yMzUuOTo4MzQ0L2FwaSIsImV4cCI6MTY5NDE3NzY3NSwiaWF0IjoxNjk0MTQ4ODc1fQ.CMafcbaKDj5dAM_6aXH2rJ6xZoAGSvlG5-fnbcPMAEQrl2yXo-ICqz9PyiAjgpOHbfe1hATivzR_-FNNF71Z-GartP-2mUXBOKQ3AnKb-DE4ShNNJ9aOct6ljgpI38x75J6Up3ZYPgXCwvBX-n59lczKTfSPscODnfaX8oIPBVfXvODgJgeYossXLnZpCtAkO7fc7QGXMsUOoIX1rljKZbpundU2_Uh4BjLj8efjkYW8NzZGM4luIJI7lHRKKfgH8nLTXQ378m_HNJC9DmrVMn2SGMh9lcXOSp976fiygxVSNWy6oCV0vbQFQPBnXe4d2Y0kIUE1ZND0Mw4Z-hUkw",
        "refresh_token": "5-tyWCkCHJEM3mWOhEG59gZPrdITcIEqUEVClG4QR-E2WAxgBsgYkcdi7EgPU_OS7o6DCenfurN-PMZzY0it0MsE3T8iIqbprjdWamTVAy-oTg97E6d4zduqw-yYunw3",
        "code": "EZNGjWZ_lQUaxsiooiWZKJmGfBsSu2HNpmFxUYWHwb4IcKPS_-dHHiEyyxike5K0jIhexSToOu2yA29Dan-i-Us8SlkWtH4dSKrFIOFenml5h-6NtFNNi63wTPo5pwnY",
        "scope": "openid profile",
        "id_token": "eyJraWQiOiIwMWQ5Yjg0Zi0zN2QyLTQ1ODgtOGE5My1mYjZkMmI3MGE0YjYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6IjE0MG05QndlYjBhODdhMyIsImF6cCI6IjE0MG05QndlYjBhODdhMyIsImlzcyI6Imh0dHA6Ly8xMTQuMjUxLjIzNS45OjgzNDQvYXBpIiwiZXhwIjoxNjk0MTUwNjc1LCJpYXQiOjE2OTQxNDg4NzUsIm5vbmNlIjoiN1NmV243ODFQNWJhSjFKMjktd2F3RWxKTVRNN3dDajRmU1piTFhINGZmMCJ9.PI2PnuqfZjF34M4qZ7XEsmrJkZ62UsbLTu3TkFu4V8lc5UViv91fw_qLAWelhcx5RTwRm3xMtUO6r7sTtO9XfVDKdDug6b-I-4lIyUtrTkH0GmPHKspCezkKHBJ8vsdhqPrzCdwBokYVGu8RKeZ930FFwB-qad1BtWlQ9jcKTYpF2NB43sE5D4pJnYKMjNAPiTOtRAwuSx1REgXCR-VLRfO_Fd1UM6EwmcyXwUXHHC5vpne0wtMLTHKNSolmfaljdQZVE7x3syB7hCslI459LSX9clz6pxq4AdK-Esy3pwX4cXeoJqD4tGr9fVS8bz3EWp0pwox1Rwk79DlRTBQtgA",
        "token_type": "Bearer",
        "expires_in": "28800",
        "client_id": "140m9Bweb0a87a3"
    },
    "uri": null
}

(2)client_id无效

{
    "code": "102-001-001-002""msg": "无效的应用,client_id参数有误""data": null,
    "uri": null
}

(3)client_secret无效

{
    "code": "102-001-001-005""msg": "客户端认证失败,client_secret错误""data": null,
    "uri": null
}

(4)授权码无效

{
    "code": "102-001-001-007""msg": "授权码失效或者错误""data": null,
    "uri": null
}

(5)回调地址无效

{
    "code": "102-001-001-011""msg": "回调地址异常,redirectUri参数有误""data": null,
    "uri": null
}

# 应用系统获取用户信息

在本文上一步骤中,成功获取到访问令牌后,应用系统需要通过访问令牌,调用统一认证接口获取当前登录用户的信息。

# 【获取认证用户信息】接口

  • 基本信息 用途说明:应用系统调用该接口获取登录用户的详细信息

请求方式:POST + Bearer认证

接口地址:https://统一认证服务ip:port/api/security/oauth2/user/info

  • 请求方式

请求头:Bearer认证

延申了解:Bearer 认证的核心是 bearer token。bearer token 是一个加密字符串,通常由服务端根据密钥生成。客户端在请求服务端时,必须在请求头中包含 Authorization: Bearer 《token》。服务端收到请求后,解析出 《token》 ,并校验 《token》 的合法性。如果校验通过,则认证通过。以下为Bearer

认证格式,其中access_token:2.2.5接口获取到的访问令牌:

Authorization=Bearer access_token具体值

Bearer Token认证java示例:

import org.springframework.http.HttpHeaders;
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setBearerAuth(access_token具体值);
  • 请求结构示例

  • 返回参数

参数字段 上级编码 字段类型 字段名称
code String 状态编码 成功时返回200
msg String 提示信息
data String 具体返回的数据
accountCode data String 账号code
accountId data String 账号id
accountName data String 账号名
phoneNumber data String 手机号
caInfo data Object CA认证结果,使用CA登录时会返回,包含以下属性。
-CAToken caInfo String 手机票据 signToken
-CASTypeCode caInfo String 登录类型(BJCA-UKEY)
-CALogonType caInfo String 登录类型
-CACertNo caInfo String 证书唯一编号 certNo
-CAUserCertCode caInfo String 用户证书唯一标识 userCertCode
-ContainerName caInfo String 容器名(UKEY名称) certContainer
-ContainerPin caInfo String UEKY密码 caUkeyPin
postOneVO data Object 当前选择的岗位单元对象
-id postOneVO String 岗位单元id
-type postOneVO String unit代表岗位单元,post代表业务岗位
-name postOneVO String 岗位单元名称
-postRecords postOneVO list 业务岗位集合
--id postRecords String 统一认证平台业务岗位id
--name postRecords String 业务岗位名称
--postSourceId postRecords String 上游业务岗位id
--postCode postRecords String 业务岗位编码
--postName postRecords String 业务岗位名称
--buSourceId postRecords String 上游业务单元id
--buCode postRecords String 业务单元编码
--buName postRecords String 业务单元编码
--startTime postRecords String 排班开始时间
--endTime postRecords String 排班结束时间
--ip postRecords String 资源计划里配的绑定ip
postId data String
postName data String 当前选择的岗位单元的名称
  • 返回结构示例

(1)正确返回时

{
	"code": "200",
	"msg": "成功",
	"data": {
		"accountCode": "admin",
		"accountId": "42ba0988866e43e9a3086ef19ebe6806",
		"accountName": "超级管理员",
		"grantType": null,
		"initLoginTime": null,
		"phoneNumber": null,
		"postFirstPage": null,
		"tenantId": "000000",
		"password": "",
		"userName": "超级管理员",
		"activity": true,
		"locked": false,
		"unlockDate": null,
		"lastLoginDate": 1717052475000,
		"lastUpdatePassword": 1664767346000,
		"createTime": null,
		"needRestPassword": false,
		"loginDate": 1717057309762,
		"globalUniqueID": "dd7afd8ba50b4f7e901f7274e841606e",
		"startDate": 1715529600000,
		"endDate": 1796054400000,
		"caInfo": {
			"CAToken": "1324vnfvjvvc",
			"CASTypeCode": "BJCA-UKEY",
			"CALogonType": "UKEY",
			"CACertNo": "22ccvvvvvscz",
			"CAUserCertCode": "csaddefe",
			"ContainerName": "jihsjbxhsdb",
			"ContainerPin": "3rgvbgb"
		},
		"postOneVO": {
			"id": "1f983808872ce3f446259ceb91207211",
			"type": "unit",
			"name": "急诊护士岗位单元",
			"postNames": "急诊护士岗",
			"buNames": "急诊普通诊疗",
			"postRecords": [{
				"id": "318f3eaebc92a9612e8b03ae4509d28d",
				"name": "急诊护士岗",
				"postSourceId": "6ec7a946e4536b2f163715097d5ac89c",
				"postCode": "P-00032-BP-0008",
				"postName": "急诊护士岗",
				"buSourceId": "1b0af2f3e93b46c7843d85bb97ac69ed",
				"buCode": "bu-00021",
				"buName": "急诊普通诊疗",
				"startTime": "2024-05-30",
				"endTime": "2025-05-25",
				"ip": "192.168.1.101,192.168.1.102"
			}]
		}
	},
	"uri": null
}

(2)access_token无效

{
  "msg": "Invalid access token: 0edb4a06-761c-4584-9085-8245d9bffcb51""code": "401"
}

# 应用系统根据用户信息登录

在本文上一步骤接口返回值中获取当前登录的用户信息之后,应用系统就可以使用该用户信息登录进入本系统。

如果应用系统需要使用ca信息,且门户系统是使用ca扫码登录,则获取用户信息返回的用户信息中会携带ca相关信息,直接使用即可。至此单点登录对接完成。

# 应用系统退出【可选】

背景:用户在门户平台单点登录到应用系统后,在门户平台执行了退出操作,如果应用系统也需要执行退出操作(即所有系统保持同样的登录状态)时,则需要提供该接口。统一认证平台会去调用各系统的退出接口,该接口需要接收一个access_token参数(接口获取访问令牌返回的访问令牌),各系统接收参数方式如下:

单点退出接口中access_token值的获取:
String accessToken = request.getParameter("access_token");

各系统接收到请求后自发进行本系统的退出处理即可。