Nccloud单点手册 wangmo 2020年07月01日
本方案适用于version:Nccloud1909+,不支持1903,1903有其他实现方案;
功能支持:单点登陆,单点审批,单点联查
单点登陆
1:业务系统向Nccloud注册accesstoken
请求参数:usercode=wangmo 模拟地址:http://localhost/service/registAccessToken?usercode=wangmo
成功响应:{"status":"1","result":{"accesstoken":"20011b791553a8338e4b9a7c8936111c"}} 失败响应:{"status":"0","result":"nccloud系统中没有usercode对应的用户,请联系管理员同步人员并生成用户!"}
2:业务系统携带Token访问Nccloud登陆页面
请求地址:http://localhost/nccloud/resources/uap/rbac/thirdpartylogin/main/index.html?accesstoken=20011b791553a8338e4b9a7c8936111c
成功响应:将直接转到首页http://localhost/nccloud/resources/workbench/public/common/main/index.html 失败响应:未找到第三方系统 ...等
单点审批
1:前提条件
待办任务已经发送到第三方系统 task = sm_msg_approve.pk_message select * from sm_msg_approve where PK_MESSAGE='1001ZZ10000000007JMA'
2:第三方系统请求注册Token和获取任务模拟审批中心的地址
请求参数:usercode=wangmo&task=1001ZZ10000000007JMA 模拟地址:http://localhost/service/registAccessToken?usercode=wangmo&task=1001ZZ10000000007JMA 描述:通过usercode和task确定用户应用页面模板等信息,用于模拟审批中心地址
成功响应: { "status":"1", "result": { "accesstoken":"2cd633f56a5a8422740f48b37a9d07a3", "redirect":"ifr=%252Fuap%252Fmsgcenter%252Fmessage%252FapproveDetail%252Findex.html%2523%252F%253Fpk_message%253D1001ZZ10000000007JMA%2526pageMsgType%253Dapprove%2526checknote%253D批准%2526p%253DHTNZ-Cxx-99%2526c%253D81H10200A%2526n%253D合同拟制审批" } } 失败响应: {"status":"0","result":"不存在的流程任务Task:1001ZZ10000000007JMA1"}
3:携带Token和redirect请求中转页面
请求参数:accesstoken=2cd633f56a5a8422740f48b37a9d07a3&type=approve&redirect=data.result.redirectURL 参数说明:accesstoken 用于登陆 type 用于判断是否(单点登陆,单点联查,单点审批) redirect 注册时返回的地址 模拟地址:http://localhost/nccloud/resources/uap/rbac/thirdpartylogin/main/index.html?accesstoken=2cd633f56a5a8422740f48b37a9d07a3&type=approve&redirect_uri=ifr=%252Fuap%252Fmsgcenter%252Fmessage%252FapproveDetail%252Findex.html%2523%252F%253Fpk_message%253D1001ZZ10000000007JMA%2526pageMsgType%253Dapprove%2526checknote%253D%E5%8D%95%E7%82%B9%E5%AE%A1%E6%89%B9%2526c%253D81H10200A
成功响应:跳转到审批界面 失败响应:请发布场景应用 ...等
单点联查
1:前提条件
单据信息已经发送到第三方系统 主要信息 billid billtype
2:业务系统向Nccloud注册accesstoken
请求参数:usercode=wangmo 模拟地址:http://localhost/service/registAccessToken?usercode=wangmo
成功响应:{"status":"1","result":{"accesstoken":"20011b791553a8338e4b9a7c8936111c"}} 失败响应:{"status":"0","result":"nccloud系统中没有usercode对应的用户,请联系管理员同步人员并生成用户!"}
3:携带Token和billid billtype请求中转页面
请求参数:accesstoken=2cd633f56a5a8422740f48b37a9d07a3&type=link&billid=billid&billtype=billtype 参数说明:accesstoken 用于登陆 type 用于判断是否(单点登陆,单点联查,单点审批) billid billtype单据信息 模拟地址:http://localhost/nccloud/resources/uap/rbac/thirdpartylogin/main/index.html?accesstoken=2cd633f56a5a8422740f48b37a9d07a3&type=link&billid=1001ZZ10000000007JLY&billtype=HTNZ
成功响应:跳转到单据卡片界面浏览态 失败响应:没有应用权限 ...等
ncc_for_uap_debug: 单据联查数据权限sql=select distinct ht_contract_h.pk_contract_h from ht_contract_h left join ht_contract_history on ht_contract_h.pk_contract_h = ht_contract_history.pk_contract_h left join ht_contract_review on ht_contract_h.pk_contract_h = ht_contract_review.pk_contract_h left join ht_contract_measure on ht_contract_h.pk_contract_h = ht_contract_measure.pk_contract_h left join ht_contract_exe on ht_contract_h.pk_contract_h = ht_contract_exe.pk_contract_h left join ht_contract_materiel on ht_contract_h.pk_contract_h = ht_contract_materiel.pk_contract_h where 1=1 and ht_contract_h.pk_org='GLOBLE00000000000000' and ht_contract_h.dr=0 and ht_contract_h.pk_contract_h='1001ZZ10000000007JLY' ncc_for_uap_debug:该单据无小应用81H10200的数据权限,或组织权限,请刷新后重试 appcode:81H10200 pk:1001ZZ10000000007JLY 解决方案:开发配置 应用管理 设置应用适用类型全局改为业务单元
代码支持
package ncc.sso.bs; import java.io.IOException; import java.util.List; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.alibaba.fastjson.JSONObject; import nc.bs.framework.adaptor.IHttpServletAdaptor; import nc.bs.framework.common.InvocationInfoProxy; import nc.bs.framework.common.NCLocator; import nc.bs.logging.Logger; import nc.itf.uap.rbac.IUserManageQuery; import nc.sso.bs.ISSOService; import nc.sso.bs.RandomUtils; import nc.sso.vo.SSOAuthenConfVO; import nc.vo.pub.BusinessException; import nc.vo.sm.UserVO; import nccloud.message.itf.IMessageQueryService; import nccloud.putitf.riart.billtype.IBillRelatedAppFinder; import nccloud.riart.pubimpl.billtype.BillRelatedAppFactory; /*** * @discription Ncc端单点登录注册类 * @author wangmo * @date 2020年06月10日 * @UPM <component accessProtected="false" name="registAccessToken" remote="false" singleton="true" tx="NONE"><implementation>ncc.sso.bs.ThirdPartyAccessTokenServlet</implementation></component> */ public class ThirdPartyAccessTokenServlet extends HttpServlet implements IHttpServletAdaptor { /** * 部分固定参数 */ private final static String Dsname = "design"; private final static String GroupId = "0001X1100000000003V9"; private final static String Language = "simpchn"; private final static String BizCenterCode = "develop"; protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doAction(req, resp); } protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doAction(req, resp); } public void doAction(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException { try { response.setContentType("application/json;charset=utf-8"); //TODO 此处省略系统之间的请求加密及安全校验过程 ; //securityValidate(request); environmentProxy(request); UserVO securityUser = validateUser(request.getParameter("usercode")); //NCLocator.getInstance().lookup(ISecurityTokenCallback.class).token("NCSystem".getBytes(), "pfxx".getBytes()); String access_token = registerSSOToken(securityUser); //@2.1判task的有效性 String taskId = request.getParameter("task"); if(!isEmpty(taskId)){ IMessageQueryService workListQuery = NCLocator.getInstance().lookup(nccloud.message.itf.IApproveMessageQueryService.class); JSONObject obj = workListQuery.queryNCMessageByPk(taskId, securityUser.getCuserid()); if(obj == null) { throw new BusinessException("不存在的流程任务Task:"+taskId); } IBillRelatedAppFinder appFinder = BillRelatedAppFactory.getInstance().createAppFinder(null); Map<String, String> appInfo = appFinder.getTargetURL("", obj.getString("billtype")); //TODO 获取对应节点信息 //走1级地址 注册Token时取地址 StringBuilder sb = new StringBuilder(); sb.append("ifr=%252Fuap%252Fmsgcenter%252Fmessage%252FapproveDetail%252Findex.html%2523%252F%253Fpk_message%253D"); sb.append(obj.getString("pk_message")); //1001ZZ10000000009WIL sb.append("%2526pageMsgType%253Dapprove"); sb.append("%2526checknote%253D批准"); sb.append("%2526p%253D"); sb.append(appInfo.get("pagecode")); sb.append("%2526c%253D"); sb.append(appInfo.get("appcode")); sb.append("%2526n%253D"); sb.append(appInfo.get("pagename")); response.getWriter().write("{\"status\":\"1\",\"result\":{\"accesstoken\":\""+access_token+"\",\"redirect\":\""+sb.toString()+"\"}}"); }else { response.getWriter().write("{\"status\":\"1\",\"result\":{\"accesstoken\":\""+access_token+"\"}}"); } } catch (Exception e) { response.getWriter().write("{\"status\":\"0\",\"result\":\""+e.getMessage()+"\"}"); Logger.error(e); }finally { response.getWriter().close(); } } /** * <p>Title: registerSSOToken</p> * <p>Description: 注册Token</p> * @param securityUser * @param access_token * @throws BusinessException */ private String registerSSOToken(UserVO securityUser) throws BusinessException { InvocationInfoProxy.getInstance().setUserId(securityUser.getCuserid()); String access_token = RandomUtils.genRandomNumber(0, ""); NCCSSORegInfo ssoInfo = new NCCSSORegInfo(); ssoInfo.setUsercode(securityUser.getUser_code()); ssoInfo.setLangCode(Language); ssoInfo.setBusiCenterCode(BizCenterCode==null?"01":BizCenterCode); ssoInfo.setAccess_token(access_token); INCCSSOService lookup = NCLocator.getInstance().lookup(INCCSSOService.class); lookup.registerSSOInfo(ssoInfo); return access_token; } /** * <p>Title: EnvironmentProxy</p> * <p>Description: EnvironmentProxy </p> * @param request * @throws Exception */ private void environmentProxy(HttpServletRequest request) throws Exception { InvocationInfoProxy.getInstance().setGroupId(GroupId); InvocationInfoProxy.getInstance().setUserDataSource(Dsname); InvocationInfoProxy.getInstance().setBizCenterCode(BizCenterCode); } /** * <p>Title: validateUser</p> * <p>Description: 校验用户</p> * @param usercode * @return UserVO * @throws Exception */ private UserVO validateUser(String usercode) throws Exception { UserVO ncuser = null; if (isEmpty(usercode)) { throw new RuntimeException("未传入必要参数人员编码 usercode"); }else{ ncuser = NCLocator.getInstance().lookup(IUserManageQuery.class).findUserByCode(usercode, InvocationInfoProxy.getInstance().getUserDataSource()); if(ncuser == null){ throw new RuntimeException("nccloud系统中没有usercode对应的用户,请联系管理员同步人员并生成用户!"); } } return ncuser; } /** * <p>Title: securityValidate</p> * <p>Description: IP白名单过滤</p> * @param request * @throws Exception */ protected void securityValidate(HttpServletRequest request) throws Exception { boolean flag = true; String host = request.getRemoteHost(); ISSOService service = NCLocator.getInstance().lookup(ISSOService.class); List<SSOAuthenConfVO> confList = service.getAuthenConfList(); String[] addrs = confList.get(0).getListValueMap().get("IPAddress"); for (String ip : addrs) { flag = (ip.trim().equals(host)) ? false : flag; } if (flag) { throw new ServletException("非法IP,无法注册ssoKey,请联系管理员!" + host); } } /** * <p>Title: isEmpty</p> * <p>Description: assert null</p> * @param str * @return */ private boolean isEmpty(String str) { return str == null || str.isEmpty(); } }
代码支持
说明: 获取轻量端 待办 预警 工作流 代码 // Request 参数按需定制 JSONObject params = new JSONObject(); params.put("msgType", "approve"); IMessageQueryService workListQuery = NCLocator.getInstance().lookup(nccloud.message.itf.IApproveMessageQueryService.class); List<JSONObject> NCworkList = workListQuery.queryNCMessages(params); params.put("msgType", "notice"); IMessageQueryService noticeQuery = NCLocator.getInstance().lookup(nccloud.message.itf.INoticeMessageQueryService.class); List<JSONObject> NCnotice = noticeQuery.queryNCMessages(params); params.put("msgType", "prealert"); IMessageQueryService prealertQuery = NCLocator.getInstance().lookup(nccloud.message.itf.IPrealertMessageQueryService.class); List<JSONObject> NCprealert = prealertQuery.queryNCMessages(params); params.clear(); //组合 params.put("approve", NCworkList); params.put("notice", NCnotice); params.put("prealert", NCprealert); //组合完成 HttpClient发送到第三方
前端代码支持
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { createPage, toast, cacheTools, ajax, Cipher ,base, viewModel ,pageTo} from 'nc-lightapp-front'; import './index.less'; import RSAUtils from "../../login/rsa/security.js"; const { NCSelect } = base; const NCOption = NCSelect.NCOption; const { setGlobalStorage, getGlobalStorage, removeGlobalStorage } = viewModel; const { opaqueEncrypt, opaqueDecrypt} = Cipher; class ThirdPartyLogin extends Component { //modify by wangmo 20200610 constructor(props) { super(props); let userAgent = navigator.userAgent; //判断是否IE浏览器 let url = window.location.href; let redirect = url.substring(url.indexOf("redirect")+9,window.location.href.length); //modify by wangmo if(userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1){ try { let shellOBJ = new ActiveXObject("WScript.Shell"); shellOBJ.Run("cmd.exe /c start chrome "+url,0,true); } catch (error) { toast({content:'建议使用Chrome谷歌浏览器68+,以获得最佳体验,请安装Google浏览器!',color:'danger'}); console.log(error); window.open(window.location.origin+"/nccloud/resources/download/Chrome64-20200623.exe") } } this.state = { loginprops:{}, accesstoken:props.getUrlParam('accesstoken'), billtype:props.getUrlParam('billtype'), billid:props.getUrlParam('billid'), baseIndex: window.location.origin, //add by wangmo 2020/06/12 connectType:props.getUrlParam('type'), redirect:redirect, busicenter:'', langcode:'', usercode:'', json:{}, inlt:null } } //扩展处理重定向地址 componentWillMount() { //处理时区 String.prototype.insert = function (index, item) { var temp = []; for (var i = 0; i < this.length; i++) { temp.push(this[i]); } temp.splice(index, 0, item); return temp.join("") }; //处理多语 let callback = (json, status, inlt) => { // json 多语json格式参数; status: 是否请求到json多语,可用来判断多语资源是否请求到并进行一些别的操作; inlt: 可用来进行占位符的一些操作 if (status) { this.setState({ json, inlt });// 保存json和inlt到页面state中并刷新页面 } else { console.log(未加载到多语资源); // 未请求到多语资源的后续操作/* 国际化处理: 未加载到多语资源,未加载到多语资源*/ } } this.props.MultiInit.getMultiLang({ 'moduleId': '1018-ssologin', 'domainName': 'uap', callback }); } componentDidMount() { this.onSubmit(); } initOption = (data) =>{ let body=[]; data.map((option,i) => ( body.push(<NCOption value={option.code}>{option.name}</NCOption>) )); return body; } handleChange = (value) => { this.setState({ busicenter:value }); }; /** * 判断浏览器类型 * add by wangmo 20200610 */ userAgent = () =>{ var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串 //判断是否Opera浏览器 if (userAgent.indexOf("Opera") > -1) { return "Opera"; } //判断是否Firefox浏览器 if (userAgent.indexOf("Firefox") > -1) { return "FF"; } //判断是否chorme浏览器 if (userAgent.indexOf("Chrome") > -1) { return "Chrome"; } //判断是否Safari浏览器 if (userAgent.indexOf("Safari") > -1) { return "Safari"; } //判断是否IE浏览器 if ( userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1 && !isOpera ) { return "IE"; } //判断是否Edge浏览器 if (userAgent.indexOf("Trident") > -1) { return "Edge"; } } //add by wangmo 20200610 getTimeZone = () => { let d = new Date(); let random = Math.floor(Math.random()*100); let y = d.getFullYear(); let m = (d.getMonth()+1) < 10 ? '0'+(d.getMonth()+1) : d.getMonth()+1 ; let day = d.getDate()<10?'0'+d.getDate():d.getDate(); let h = d.getHours()<10?'0'+d.getHours():d.getHours(); let i = d.getMinutes()<10?'0'+d.getMinutes():d.getMinutes(); let s = d.getSeconds()<10?'0'+d.getSeconds():d.getSeconds(); return ''+y+m+day+h+i+s+random; } //add by wangmo 20200610 doRedirect = (res) =>{ switch(this.state.connectType){ case 'link': pageTo.openAppByBilltype({ billpk: this.state.billid, //单据pk billtype: this.state.billtype //单据类型 }); let childUrl = window.location.href.split("#")[1]; childUrl && (window.location.href = `${this.state.baseIndex}/workbench/public/common/main/index.html#${window.location.href.split("#")[1]}`); break; case 'approve': //1级地址 单点审批 let kv = new Date(); let key = this.getTimeZone(); sessionStorage.setItem('NCCAPPURL', '{"'+key+'":"'+this.state.redirect+'"}'); window.location.href = this.state.baseIndex+'/nccloud/resources/workbench/public/common/main/index.html#/ifr?page='+key; //2级地址 单点审批 //url: '/nccloud/ssologin/base/indexQuery.do', break; default: window.location.href = this.state.baseIndex+'/'+res.data.index; } } onSubmit = ()=>{ try{ cacheTools.clear(); }catch(e){ } var that = this; ajax({ url: '/nccloud/riart/login/init.do', success: (res)=> { if (res.success) { function uuidv4() { return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } let key = RSAUtils.getKeyPair(res.data.exponent, '', res.data.modulus); let aeskeyCahe = uuidv4(); // 由于同一个浏览器 只有一个用户 let cowboy = getGlobalStorage('localStorage','cowboy'); aeskeyCahe = cowboy ? opaqueDecrypt(cowboy) : aeskeyCahe; let aeskey = RSAUtils.encryptedString(key, aeskeyCahe); // 重置 aes 请求 --bbqin localStorage.removeItem('rockin'); localStorage.removeItem('rockinlog'); ajax({ url: '/nccloud/baseapp/thirdPartyLogin/thirdPartyVerfiy.do', data:{ aeskey:aeskey, firstlogin:"Y", token:this.state.accesstoken, connecttype:this.state.connecttype, timezone: new Date().toString().substring(25,33).insert(6,":") }, success: (res) => { if (res.success) { // 发一个同步ajax // 如果浏览器器中出现了两个用户 以第一个用户的为主 保持同一个浏览器中唯一 setGlobalStorage('localStorage','cowboy', cowboy || opaqueEncrypt(aeskeyCahe)); // 当前的 ajax({ type: 'post', url: '/nccloud/platform/aes/switch.do', data: { sysParamJson: { busiaction: '查询请求aes加密开关' } }, async: false, success: function(asd) { // aesSwitchData asd = typeof asd === 'string' ? JSON.parse(asd) : asd; if (asd.data) { if (asd.data.success || asd.success) { if (asd.data) { setGlobalStorage('localStorage','rockin', true); setGlobalStorage('localStorage','rockinlog',167); } else { setGlobalStorage('localStorage','rockin', false); setGlobalStorage('localStorage','rockinlog',171); } } } }, error: function(res) { setGlobalStorage('localStorage','rockinlog',176); } }); let rslCode = res.data.rslCode; if(rslCode=="0" || rslCode=="5" ){//登录成功 or 强制登陆 modify by wangmo console.log(res); that.doRedirect(res); } if(rslCode=="6"){//token错误或者过期 toast({content:res.data.rslMsg,color:'danger'}); } if(rslCode=="7"){//没有可登录的系统 toast({content:res.data.rslMsg,color:'danger'}); } if(rslCode=="8"){//多账套 that.setState({ usercode:res.data.usercode, langcode:res.data.langcode, busicenter:res.data.rslMsg[0].code }); that.props.modal.show('busi',{ content:<div> <NCSelect defaultValue={res.data.rslMsg[0].code} onChange={that.handleChange.bind(this)} style={{ width: 200, marginRight: 2 }}> {that.initOption(res.data.rslMsg)} </NCSelect> </div>, hideRightBtn:true, beSureBtnClick: () => { ajax({ url: '/nccloud/baseapp/thirdPartyLogin/thirdPartyVerfiy.do', data:{ firstlogin:'N', usercode:that.state.usercode, langcode:that.state.langcode, busicenter:that.state.busicenter, timezone: new Date().toString().substring(25,33).insert(6,":"), }, success: function (res) { //window.location = that.state.redirect_uri; modify by wangmo window.location.href = that.state.baseIndex+'/'+res.data.index; } }) } }) } if(rslCode=="-1"){//登录异常 toast({content:res.data.rslMsg,color:'danger'}); } } }, error:function(res){ } }) } }, error:function(res){ } }); } render() { let { modal, data } = this.props; let { createModal } = modal; this.loginprops = data; return ( <div id= "login_div" className="content-right"> {createModal('busi', { title:this.state.json['1880000025-000002']/* 国际化处理: 请选择登录账套*/ })} </div> ); } } // 重置 aes 请求 --bbqin localStorage.removeItem('rockin'); localStorage.removeItem('rockinlog'); export default ThirdPartyLogin = createPage({ })(ThirdPartyLogin); ReactDOM.render(<ThirdPartyLogin />, document.querySelector('#app'));
wangmob@yonyou.com
用友网络科技有限公司四川分公司
来源:oschina
链接:https://my.oschina.net/u/4494332/blog/4331267