苍穹应用菜单触发 | 在新浏览器窗口打开第三方带会话信息页面
# 关键词:单点登录、页面集成、页面跳转、应用菜单点击、新浏览器窗口
# 一、需求
苍穹与第三方系统进行集成。用户登录苍穹之后,在应用首页点击某个菜单之后,系统自动单点登录并以新的浏览器窗口打开第三方系统的指定页面,然后用户可自由进行业务操作。此外,需补充说明的是,第三方系统不支持身份认证功能,而本案例需求涉及到单点登录功能,故需要有一个用作统一身份认证服务的系统。
# 二、思路与方案
本案例需求的关键点包含2点:第一,用户点击应用菜单实现跳转到第三方系统;第二,苍穹跳转第三方系统页面时需支持单点登录。
首先,关于第二点,考虑到第三方系统不支持身份认证功能,故需要借助一个统一身份认证系统来实现苍穹与第三方系统的单点登录,但前提需使苍穹、第三方系统、统一身份认证服务系统之间的用户数据保持一致。关于该点功能开发,社区已存在丰富资料,可自行查阅参考开发。
其次,第一点的难点在于需要在用户点击应用菜单时进行干预,但我们不清楚该从哪里入手去进行二开。这里,我们进入应用首页后,在菜单位置点击鼠标,通过快捷键 **ctrl+alt+g** 进入鼠标焦点所在的应用首页的设计器界面,然后通过表单上注册的插件 BizAppHomePlugin 的事件 treeMenuClick(TreeNodeEvent) 分析可知:苍穹应用菜单上必须配置待打开的页面才行。但考虑到我们是需要打开第三方系统的页面,在苍穹系统里面并没有,故可开发一个空白动态表单页面,并将其配置应用菜单上,然后在用户点击应用菜单的时候取消打开该动态表单,转而通过重写纵向导航栏控件的 treeMenuClick(TreeNodeEvent) 事件来实现跳转第三方系统页面。
# 三、实现过程
## 3.1 准备工作
苍穹系统:http://172.20.14.30:8080/ierp
第三方系统:本案例以另一套苍穹环境进行模拟,其访问地址:http://172.20.240.88:8080/ierp
统一身份认证服务系统:https://api.kingdee.com
## 3.2 注册应用
登录[金蝶云平台](https://cloud.kingdee.com/)注册应用,设置回调地址为苍穹系统的IP&端口,用于苍穹系统登录认证
*备注:
如需复现案例效果,请自行前往金蝶云平台注册应用后,配置回调地址,并修改 Parameter.java & CloudPlatformSSOAuth.java 文件中 CLIENT_ID & CLIENT_SECRET 的值。*
![云平台应用-苍穹.webp](/download/0100d98fa9f344ab4bbea99e2e0e8dbdc0e0.webp)
## 3.3 在苍穹系统中开发单点登录金蝶云平台插件(CldPlatformSSOPlugin)
1.先判断系统是否登录,若没有,则返回;若已登录,则根据金蝶云平台返回的授权码获取access_token,再获取用户信息进行认证,认证成功则登录苍穹系统。
```language
@Override
public UserAuthResult getTrdSSOAuth(HttpServletRequest request, HttpServletResponse response) {
UserAuthResult result = new UserAuthResult();
result.setSucess(false);
// 判断是否成功登录金蝶云平台, 通过code进行判断
String code = request.getParameter("code");
String state = request.getParameter("state");
if (StringUtils.isEmpty(code) || StringUtils.isEmpty(state)) {
result.setErrDesc("单点登录失败");
return result;
}
// 获取构造缓存
DistributeSessionlessCache cache = CacheFactory.getCommonCacheFactory().getDistributeSessionlessCache(KEY_CACHE_SSOLOGIN);
String infoStr = cache.get(state);
JSONObject paramObj;
if (StringUtils.isNotEmpty(infoStr)) {
paramObj = JSONObject.parseObject(infoStr);
} else {
paramObj = new JSONObject();
}
// 重定向到指定页面
String queryParam = paramObj.getString("queryparam");
if (!request.getQueryString().contains("flag=1")) {
try {
String url = String.format("%1$s/?code=%2$s&state=%3$s&flag=1", Parameter.COSMIC_HOME_URL, code, URLEncoder.encode(state, "UTF-8"));
if (StringUtils.isNotEmpty(queryParam)) {
url = String.format("%1$s&%2$s", url, queryParam);
}
response.sendRedirect(url);
} catch (Exception e) {
logger.error(String.format("重定向失败: %s", ExceptionUtils.getExceptionStackTraceMessage(e)));
}
return result;
}
// 从云平台获取access_token
JSONObject responseObj = SSOUtil.getAccessToken(code);
Integer errCode = responseObj.getInteger("errcode");
String accessToken = null;
if (errCode != null && errCode == 0) {
accessToken = responseObj.getJSONObject("data").getString("access_token");
request.setAttribute("kdcloudaccesstoken", accessToken);
} else {
String errDesc = StringUtils.isEmpty(responseObj.getString("description")) ? "未获取到access_token信息!" : responseObj.getString("description");
result.setErrDesc(errDesc);
logger.error(errDesc);
return result;
}
// 从云平台获取登录用户基本信息
JSONObject userInfoObj = SSOUtil.getUserInfo(accessToken);
errCode = userInfoObj.getInteger("errcode");
if (errCode != null && errCode == 0) {
JSONObject data = userInfoObj.getJSONObject("data");
result.setUser(data.getString("phone"));
result.setUserType(UserProperType.Mobile);
result.setSucess(true);
} else {
String errDesc = StringUtils.isEmpty(userInfoObj.getString("description")) ? "未查询到用户信息!" : userInfoObj.getString("description");
logger.error(errDesc);
throw new KDException(LoginErrorCode.loginBizException, "系统错误,请联系系统管理员。" + errDesc);
}
return result;
}
```
2.若用户未登录,则跳转到金蝶云平台的登录页,成功登陆之后系统重定向到苍穹系统门户首页。若用户退出系统、或者再次访问之前已登录的地址,则通过重定向到苍穹默认登录页转而重定向到金蝶云平台的登录页
```language
@Override
public void callTrdSSOLogin(HttpServletRequest request, HttpServletResponse response, String backUrl) {
// 退出处理
if (request.getRequestURI().contains("logout.do")) {
this.logout(request);
this.sendRedirect(response, Parameter.COSMIC_HOME_URL);
return;
}
if (!response.isCommitted()) {
// 用户未登录, 且访问链接包含以前请求的code & state 参数, 则重定向到本系统的homeUrl发起访问
String code = request.getParameter("code");
String state = request.getParameter("state");
if (StringUtils.isNotEmpty(code) && StringUtils.isNotEmpty(state)) {
this.sendRedirect(response, Parameter.COSMIC_HOME_URL);
return;
}
}
// sso插件重定向处理
if (request.getQueryString() != null && request.getQueryString().contains("flag=1")) {
return;
}
// 正常登录访问苍穹,构造缓存数据
String state = ID.genStringId();
DistributeSessionlessCache cache = CacheFactory.getCommonCacheFactory().getDistributeSessionlessCache(KEY_CACHE_SSOLOGIN);
JSONObject paramObj = new JSONObject();
paramObj.put("queryparam", request.getQueryString());
cache.put(state, JSONObject.toJSONString(paramObj), 60);
// 根据sso地址构造,登录地址 重定向到云平台
try {
// 成功登录之后重定向到系统门户首页
String redirectUri = String.format("%1$s%2$s", SSOUtil.getHomeUrl(Parameter.COSMIC_HOME_URL), "index.html");
String url = String.format("%1$s?client_id=%2$s&response_type=%3$s&redirect_uri=%4$s&state=%5$s",
String.format("%1$s%2$s", SSOUtil.getHomeUrl(Parameter.AUTH_CENTER_URL), "auth/oauth2/authorize"),
Parameter.CLIENT_ID,
"code",
URLEncoder.encode(redirectUri, "UTF-8"),
URLEncoder.encode(state, "UTF-8"));
response.sendRedirect(url);
} catch (IOException e) {
logger.error(String.format("重定向失败: %s", ExceptionUtils.getExceptionStackTraceMessage(e)));
}
}
```
3.成功登陆之后,将 access_token 放入分布式缓存中,后续在跳转第三方系统页面时使用
```language
@Override
public void processSucceedLogin(HttpServletRequest request, String globalSessionId) {
// 将从金蝶云平台获取的 access_token 存入分布式缓存, 1天有效期. 后续跳转第三方系统页面时取出使用
String accessToken = (String) request.getAttribute("kdcloudaccesstoken");
DistributeSessionlessCache cache = CacheFactory.getCommonCacheFactory().getDistributeSessionlessCache(KEY_CACHE_SSOLOGIN);
cache.put(globalSessionId, "kdcloudaccesstoken", accessToken, 1 * 24 * 60 * 60);
ThirdSSOAuthHandler.super.processSucceedLogin(request, globalSessionId);
}
```
## 3.4 配置应用菜单(ToTrdSysPageFormPlugin)
*备注:
当用户点击应用菜单后,会触发应用首页上默认注册的插件 BizAppHomePlugin,并执行 treeMenuClick(TreeNodeEvent) 方法。分析该方法代码可知:应用菜单必须绑定一个苍穹平台内的页面才能实现在点击菜单后跳转对应页面。*
*因此,我们可考虑通过为菜单绑定一个空白动态表单,然后在点击应用菜单的时候取消打开该表单页面,转而通过重写应用首页上纵向导航栏控件的 treeMenuClick(TreeNodeEvent) 事件来实现跳转第三方系统页面。*
*其次,由于不能通过该方法提供的接口setCancel(true)去取消打开表单页面,故只能在表单页面上注册插件来进行取消。*
### 1.开发空白动态表单,其表单设计器如图所示
![kdec_totrdpagebyappmenu.webp](/download/0100f2040ce973124353a70c17820b7a1c81.webp)
### 2.在上述页面注册插件取消其打开(kdec_totrdpagebyappmenu)
```language
@Override
public void preOpenForm(PreOpenFormEventArgs e) {
// 取消打开页面
e.setCancel(true);
super.preOpenForm(e);
}
```
### 3.菜单配置
![菜单跳转1-菜单配置.webp](/download/010049cf734884bd4c8d8f8d0abbb56c028a.webp)
## 3.5 在应用首页上注册插件二开干预应用菜单的点击事件(CustomBizAppHomePlugin2)
```language
@Override
public void treeMenuClick(TreeNodeEvent evt) {
// 点击指定菜单跳转第三方系统页面
Object appMenuNodeId = evt.getNodeId();
AppMenuInfo appMenuInfo = AppMetadataCache.getAppMenuInfo(KEY_APPNUMER, appMenuNodeId.toString());
if (appMenuInfo != null) {
if (StringUtils.equalsIgnoreCase("kdec_totrdpagebyappmenu", appMenuInfo.getFormId())) {
// 以新浏览器窗口打开第三方系统页面
//this.gotoApp();
}
}
}
```
## 3.6 (单点登录第三方系统并)返回待打开页面的URL(OpenTrdPageUtils)
```language
/**
* 获取待打开的第三方页面地址
* @param isStaticPage 待打开的第三方页面是否为静态页面
* @return
*/
public static String getTrdSysTargetUrl(boolean isStaticPage) {
String trdSysTargetUrl = null;
if (isStaticPage) {
// 跳转第三方静态页面, 亦可通过苍穹参数配置实现
trdSysTargetUrl = Parameter.TRDSYS_HOME_URL;
} else {
// 跳转第三方系统页面(带登录会话信息)
DistributeSessionlessCache cache = CacheFactory.getCommonCacheFactory().getDistributeSessionlessCache("ssoLogin");
String accessToken = cache.get(RequestContext.get().getGlobalSessionId(), "kdcloudaccesstoken");
if (StringUtils.isEmpty(accessToken)) {
logger.error("系统从金蝶云平台获取授权码(auth_code)时access_token为空!");
throw new KDBizException("请重新登录系统!");
}
// 获取第三方系统的默认访问地址
trdSysTargetUrl = getTrdSysHomeUrl("kdec_tstapp2");
if (StringUtils.isEmpty(trdSysTargetUrl)) {
logger.error("系统参数为空!");
throw new KDBizException("系统配置不完善, 请联系系统管理员操作!");
}
// 单点登录成功之后打开第三方系统(另一套苍穹系统)门户首页
trdSysTargetUrl = String.format("%1$s%2$s", trdSysTargetUrl, "?formId=pc_main_console");
// 单点登录成功之后打开第三方系统(另一套苍穹系统)中【系统服务云】下应用【基础资料】的首页
// trdSysTargetUrl = String.format("%1$s%2$s", trdSysTargetUrl, "?formId=pc_main_console&appNumber=basedata");
// 单点登录成功之后打开第三方系统(另一套苍穹系统)中的指定页面(此处以币别列表界面为例)
// trdSysTargetUrl = String.format("%1$s%2$s", trdSysTargetUrl, "index.html?billFormId=bd_currency&formId=bos_list");
if (StringUtils.isNotEmpty(accessToken)) {
String authCode = SSOUtil.getAuthCode(accessToken);
// 如果可从金蝶云平台获取到auth_code, 则系统自动单点登录到第三方系统页面; 否则需用户手动登录
if (StringUtils.isNotEmpty(authCode)) {
String tmpStr = trdSysTargetUrl.endsWith("/") ? "?" : "&";
trdSysTargetUrl = String.format("%1$s%2$sauth_code=%3$s", trdSysTargetUrl, tmpStr, authCode);
}
}
}
return trdSysTargetUrl;
}
```
## 3.7 在新浏览器窗口中打开第三方系统页面(CustomBizAppHomePlugin2)
```language
/**
* 点击指定菜单单点登录第三方系统并以新浏览器页签打开页面. 根据实际业务修改
*/
private void gotoApp() {
try {
// 以新浏览器窗口打开第三方系统页面
String trdSysTargetUrl = OpenTrdPageUtils.getTrdSysTargetUrl(false);
if (StringUtils.isNotEmpty(trdSysTargetUrl)) {
OpenTrdPageUtils.openTrdSysPageWithNewWindow(getView(), trdSysTargetUrl);
}
} catch (KDBizException e) {
logger.error(e.getMessage());
this.getView().showMessage(e.getMessage());
}
}
```
## 3.8 在第三方系统中开发单点登录金蝶云平台插件(CloudPlatformSSOAuth)
*备注:
1.考虑到不同项目、产品的第三方系统各不相同,此处代码仅作参考。
2.该案例代码控制第三方系统只能从苍穹登录后再跳转,即使访问第三方系统登录页也会重定向到苍穹系统的默认访问地址。*
```language
@Override
public void callTrdSSOLogin(HttpServletRequest request, HttpServletResponse response, String backUrl) {
// 将第三方系统(即:本苍穹系统)登录&退出请求, 全部重定向到苍穹系统的默认访问地址
this.sendRedirect(response, COSMIC_HOME_URL);
return;
}
private void sendRedirect(HttpServletResponse response, String targetUrl) {
try {
response.sendRedirect(targetUrl);
} catch (IOException e) {
logger.error(String.format("重定向失败: %s", ExceptionUtils.getExceptionStackTraceMessage(e)));
}
}
@Override
public UserAuthResult getTrdSSOAuth(HttpServletRequest request, HttpServletResponse response) {
UserAuthResult result = new UserAuthResult();
result.setSucess(false);
DistributeSessionlessCache cache = CacheFactory.getCommonCacheFactory().getDistributeSessionlessCache("ssologin");
String requestUrl = request.getRequestURI();
if (!requestUrl.endsWith("/ierp/")) {
String userInfoStr = cache.get("userinfo");
if (StringUtils.isNotEmpty(userInfoStr)) {
JSONObject userInfo = JSONObject.parseObject(userInfoStr);
result.setUser(userInfo.getString("phone"));
result.setUserType(UserProperType.Mobile);
result.setSucess(true);
} else {
throw new KDException(LoginErrorCode.loginBizException, "系统错误,请联系系统管理员。未获取到用户信息!");
}
return result;
}
String responseStr = null;
// 根据 auth_code 判断是否为外部系统登录本系统
String authCode = request.getParameter("auth_code");
if (StringUtils.isNotEmpty(authCode)) {
// 从外部系统登录
try {
String url = String.format("%1$s%2$s", getHomeUrl(AUTH_CENTER_URL), "auth/user/auth_code/validation");
Map<String, Object> body = new HashMap<>();
body.put("client_id", CLIENT_ID);
body.put("client_secret", CLIENT_SECRET);
body.put("auth_code", authCode);
responseStr = HttpClientUtils.post(url, null, body);
} catch (IOException e) {
logger.error("校验授权码失败: %s", ExceptionUtils.getExceptionStackTraceMessage(e));
}
} else {
return result;
}
JSONObject responseObj = StringUtils.isNotEmpty(responseStr) ? JSONObject.parseObject(responseStr) : new JSONObject();
Integer errCode = responseObj.getInteger("errcode");
if (errCode != null && errCode == 0) {
String accessToken = responseObj.getJSONObject("data").getString("access_token");
try {
String url = String.format("%1$s%2$s?access_token=%3$s", getHomeUrl(AUTH_CENTER_URL), "account/user_info", accessToken);
responseStr = HttpClientUtils.get(url);
} catch (Exception e) {
logger.error("查询用户信息出错: %s", ExceptionUtils.getExceptionStackTraceMessage(e));
}
JSONObject userInfoObj = StringUtils.isNotEmpty(responseStr) ? JSONObject.parseObject(responseStr) : new JSONObject();
errCode = userInfoObj.getInteger("errcode");
if (errCode != null && errCode == 0) {
String phone = userInfoObj.getJSONObject("data").getString("phone");
result.setUser(phone);
result.setUserType(UserProperType.Mobile);
result.setSucess(true);
// 缓存用户信息
cache.put("userinfo", userInfoObj.getJSONObject("data").toString(), 8 * 60 * 60);
} else {
String errDesc = StringUtils.isEmpty(responseObj.getString("description")) ? "未获取到用户信息!" : responseObj.getString("description");
throw new KDException(LoginErrorCode.loginBizException, String.format("系统错误,请联系系统管理员。%s", errDesc));
}
} else {
String errDesc = StringUtils.isEmpty(responseObj.getString("description")) ? "未获取到access_token信息!" : responseObj.getString("description");
result.setErrDesc(errDesc);
logger.error(errDesc);
}
return result;
}
```
# 四、效果图
## 4.1 登录苍穹系统
![苍穹登录.webp](/download/0100dab6f2465fcd4d1bb617e8f007284c6a.webp)
![云平台登录页-1.webp](/download/0100cc8d97a64c944c2380c22ba2518b2ef8.webp)
## 4.2 应用菜单点击
### 1.旧版
![菜单跳转1-旧版-效果图1.webp](/download/010031c91fff1a4c42c9948e59eb10060b4a.webp)
### 2.新版
![菜单跳转1-新版-效果图1.webp](/download/0100fc7c0405e6ab4e60a4321f684d65e11a.webp)
## 4.3 第三方系统
![菜单跳转1-新版-效果图2.webp](/download/010028cb38292541475e80f83fd67ce457a6.webp)
# 五、开发环境版本
V5.0.011
# 六、注意事项
- 在开发单点登录功能前,需先同步苍穹与第三方系统的人员数据,使其保持一致(组织数据可根据实际业务确定)。考虑该功能点可独立开发,本案例中不予实现。
- 应用首页页面需根据开平台内应用 —【高级信息】—【首页设置】的配置确定。
![首页配置.webp](/download/0100a206fbe471654ca2970092d212ba39f5.webp)
- 针对上一点,若应用首页绑定默认首页界面,则用户点击应用菜单,一定会触发注册在页面上的插件 BizAppHomePlugin,从而会打开该菜单绑定的页面,故需开发插件去取消,即第 $3.4 节内容。
- 需调整第三方系统前端界面UI与苍穹保持一致。
- 如需复现本案例效果,必须先在金蝶云平台注册应用并修改案例代码中的相关参数,详见第$3.2节内容。
- 文章中代码展示不全,**附件中包含案例所有页面元数据、Java源码**,如有需要,请自行下载。
# 七、参考资料
[开发平台](https://vip.kingdee.com/knowledge/specialDetail/218022218066869248?productLineId=29)
[学习成长中心](https://developer.kingdee.com/school?productLineId=29)
[登录认证专题](https://vip.kingdee.com/knowledge/specialDetail/228892721203874816?productLineId=29)
[苍穹产品目录](https://developer.kingdee.com/knowledge?productLineId=29#tabMain) —— 系统服务云 —— 系统管理 —— 登录认证
[单点登录集成](https://developer.kingdee.com/school/243812482044022016?productLineId=29)(视频)
[金蝶云平台oauth2流程说明](https://cloud.kingdee.com/help/document/detail?item=448&doc=3023)
[【统一身份认证】第三方集成单点登录](https://developer.kingdee.com/article/359709414254707456?share_fromuid=&productLineId=29&islogin=true)
[单点登录插件,访问指定的数据中心,金蝶云单点登录](https://developer.kingdee.com/article/215788160662549248?productLineId=29)
苍穹应用菜单触发 | 在新浏览器窗口打开第三方带会话信息页面
# 关键词:单点登录、页面集成、页面跳转、应用菜单点击、新浏览器窗口# 一、需求苍穹与第三方系统进行集成。用户登录苍穹之后,在应用首...
点击下载文档
本文2024-09-23 01:15:10发表“云苍穹知识”栏目。
本文链接:https://wenku.my7c.com/article/kingdee-cangqiong-144760.html
您需要登录后才可以发表评论, 登录登录 或者 注册
最新文档
热门文章