一、前期准备
// 安装脚手架
npm install -g @vue/cli
// 新建项目
vue create -p dcloudio/uni-preset-vue 项目名字
"scripts": {
"serve": "npm run dev:h5",
"build": "npm run build:h5",
// 在"build:h5"中添加UNI_OUTPUT_DIR='build'即可
"build:h5": "cross-env UNI_OUTPUT_DIR='build' NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service uni-build",
"dev:h5": "cross-env NODE_ENV=development UNI_PLATFORM=h5 vue-cli-service uni-serve"
},
// gbc.json
{
"type":"build-config",
"version":"0.0.1",
"outputPath":"dist"
}
{
"name" : "xxxxxxxx",
"appid" : "",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
"h5" : {
"publicPath" : "./", // 修改1 不修改此处会出现应用白屏的情况
"router" : {
"base" : "./", // 修改2 不修改此处会出现图片拿不到的情况
"mode" : "hash" // 修改3 浙里办只支持hash路由
}
}
}
二、浙里办-单点登录功能
const sUserAgent = window.navigator.userAgent.toLowerCase()
// 浙里办APP
const bIsDtDreamApp = sUserAgent.indexOf('dtdreamweb') > -1
// 浙里办支付宝小程序
const bIsAlipayMini = sUserAgent.indexOf('miniprogram') > -1 && sUserAgent.indexOf('alipay') > -1
isLoad: () => {
if (bIsAlipayMini) {
window.location.href = "https://puser.zjzwfw.gov.cn/sso/alipay.do?action=ssoLogin&servicecode=【接入代码】&redirectUrl=【附带跳转地址,以sp参数返回】";
} else {
window.location.href = "https://puser.zjzwfw.gov.cn/sso/mobile.do?action=oauth&scope=1&servicecode=【接入代码】&redirectUrl=【附带跳转地址,以sp参数返回】";
}
// 或者使用replace()
// window.location.replace('https://puser.zjzwfw.gov.cn/sso/alipay.do?action=ssoLogin&servicecode=【接入代码】&redirectUrl=【附带跳转地址,以sp参数返回】');
}
接入码是跟浙里办申请服务接入通过之后给的,redirectUrl参数是单点登录跳转的回调地址,如果不指定就以后台设置的sp参数作为回调地址跳转。(sp参数自己无权限修改,必须找服务接入对接人设置,且不设置单点登录登录成功将显示“无权访问业务系统”的提示字样)
Tips:新版使用AK/SK(servicecode 等同于 AccessKey(简称 AK),servicepwd 等同于 SecretKey(简称 SK)),获取地址:https://csss.zj.gov.cn/verifyComList/applyNew ,需政务内网访问。
2. 二次回退问题
使用pageshow事件解决二次回退问题
// 浙里办APP判断页面进入状态
watchApp: () => {
window.onpageshow = (event) => {
if (event.persisted || (window.performance && window.performance.navigation.type == 2)) {
ZWJSBridge.close();
}
that.isLoad();
}
},
// 支付宝浙里办小程序判断页面进入状态
watchApply: () => {
window.onpageshow = (event) => {
if (event.persisted || (window.performance && (window.performance.navigation.type == 1 || window.performance.navigation.type == 0))) {
that.isLoad();
} else {
my.navigateBack();
}
}
}
使用vue router的守卫导航解决二次回退问题
// loading 中转页面
// 单点登录成功后调用 that.$router.push('/index')
beforeRouteEnter(to, from, next) {
if(to.meta.name === 'loading' && from.meta.name === 'index') {
if(bIsDtDreamApp){
ZWJSBridge.close();
} else {
my.navigateBack();
}
}
}
// 路由配置
{path: '/index', component: () => {'./pages/index/index.vue'}, name: 'index', meta: {name: 'index'}}
{path: '/loading', component: () => {'./pages/index/loading.vue'}, name: 'loading', meta: {name: 'loading'}}
// 用法 ==> that.getQuery('ticket')
getQuery: (name) => {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
}
因为uniapp只能处理应用内的跳转及参数获取,应用内的参数可以在onLoad()及onInit()这两个生命周期函数获取,而单点登录成功的回调属于重定向,参数只能通过location对象获取。
4. 完整逻辑
init: () => {
sUserAgent = window.navigator.userAgent.toLowerCase();
bIsDtDreamApp = sUserAgent.indexOf("dtdreamweb") > -1; // 浙里办APP
bIsAlipayMini = sUserAgent.indexOf("miniprogram") > -1 && sUserAgent.indexOf("alipay") > -1;
if (that.getQuery('ticket') == null && that.ticket == "") {
uni.showLoading({
title: '登录中',
mask: true
});
if (bIsDtDreamApp) {
that.watchApp();
} else if (bIsAlipayMini) {
that.watchApply();
}
} else {
that.ticket = that.getQuery('ticket');
uni.reLaunch({
url: `./index?ticket=${that.ticket}`
})
}
}
wxAPP: () => {
if (ZWJSBridge.ssoTicket) {
const ssoFlag = await ZWJSBridge.ssoTicket({});
if (ssoFlag && ssoFlag.result === true) {
//使用 IRS“浙里办”单点登录组件
if (ssoFlag.ticketId) {
that.ticket = ssoFlag.ticketId;
} else {
//当“浙里办”单点登录失败或登录态失效时调用 ZWJSBridge.openLink 方法重新获取 ticketId。
ZWJSBridge.openLink({
type: "reload"
}).then(res => {
that.ticket = res.ticketId;
})
}
} else {
that.init();
}
} else {
that.init();
}
}
// !!!!!!非常重要的内容!!!!!!
// 微信小程序需要使用最新版本的ZWJSBridge-1.1.0
//
三、获取用户信息
Postman调用脚本见下文附件
Ⅰ、普通request调取用户数据
根据ticket获取token,POST方法queryParams传参
getTonken: () => {
new Promise((resolve, reject) => {
uni.request({
url: `https://appapi.zjzwfw.gov.cn/sso/servlet/simpleauth?method=ticketValidation${that.queryParams(that.getTonkenAndUserInfoParams())}`,
method: 'POST',
header:{
"Content-Type": "application/json"
},
success: (res) => {
if (res.statusCode == 200 && res.data.result == 0) {
resolve(res.data);
} else {
uni.showToast({
title: res.data.result
})
reject(res);
}
},
fail: (err) => {
uni.showToast({
title: '网络错误,请稍后重试!'
})
reject(err.errMsg);
}
})
}
}
根据token获取用户信息
getUserInfo: () => {
new Promise((resolve, reject) => {
uni.request({
url: `https://appapi.zjzwfw.gov.cn/sso/servlet/simpleauth?method=getUserInfo${that.queryParams(that.getTonkenAndUserInfoParams(that.token))}`,
method: 'POST',
header:{
"Content-Type": "application/json"
},
success: (res) => {
if (res.statusCode == 200 && res.data.result == 0) {
resolve(res.data);
} else {
uni.showToast({
title: res.data.result
})
reject(res);
}
},
fail: (err) => {
uni.showToast({
title: '网络错误,请稍后重试!'
})
reject(err.errMsg);
}
})
}
}
统一获取参数
getTonkenAndUserInfoParams: ( token = '') => {
let mTime = that.timeFormat(new Date(), 'yyyymmddhhMMss');
let data = {
servicecode: 【接入码】,
time: mTime,
sign: md5Libs.md5(`${【接入码】}${【接入码密码】}${mTime}`),
datatype: 'json'
}
if (token == '') {
data.st = that.ticket;
} else {
data.token = token;
}
return data;
}
时间格式化
function timeFormat(dateTime = null, fmt = 'yyyy-mm-dd') {
// 如果为null,则格式化当前时间
if (!dateTime) dateTime = Number(new Date());
// 如果dateTime长度为10或者13,则为秒和毫秒的时间戳,如果超过13位,则为其他的时间格式
if (dateTime.toString().length == 10) dateTime *= 1000;
let date = new Date(dateTime);
let ret;
let opt = {
"y+": date.getFullYear().toString(), // 年
"m+": (date.getMonth() + 1).toString(), // 月
"d+": date.getDate().toString(), // 日
"h+": date.getHours().toString(), // 时
"M+": date.getMinutes().toString(), // 分
"s+": date.getSeconds().toString() // 秒
// 有其他格式化字符需求可以继续添加,必须转化成字符串
};
for (let k in opt) {
ret = new RegExp("(" + k + ")").exec(fmt);
if (ret) {
fmt = fmt.replace(ret[1], (ret[1].length == 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0")))
};
};
return fmt;
}
参数格式化
function queryParams(data = {}, isPrefix = true, arrayFormat = 'brackets') {
let prefix = isPrefix ? '?' : ''
let _result = []
if (['indices', 'brackets', 'repeat', 'comma'].indexOf(arrayFormat) == -1) arrayFormat = 'brackets';
for (let key in data) {
let value = data[key]
// 去掉为空的参数
if (['', undefined, null].indexOf(value) >= 0) {
continue;
}
// 如果值为数组,另行处理
if (value.constructor === Array) {
// e.g. {ids: [1, 2, 3]}
switch (arrayFormat) {
case 'indices':
// 结果: ids[0]=1&ids[1]=2&ids[2]=3
for (let i = 0; i < value.length; i++) {
_result.push(key + '[' + i + ']=' + value[i])
}
break;
case 'brackets':
// 结果: ids[]=1&ids[]=2&ids[]=3
value.forEach(_value => {
_result.push(key + '[]=' + _value)
})
break;
case 'repeat':
// 结果: ids=1&ids=2&ids=3
value.forEach(_value => {
_result.push(key + '=' + _value)
})
break;
case 'comma':
// 结果: ids=1,2,3
let commaStr = "";
value.forEach(_value => {
commaStr += (commaStr ? "," : "") + _value;
})
_result.push(key + '=' + commaStr)
break;
default:
value.forEach(_value => {
_result.push(key + '[]=' + _value)
})
}
} else {
_result.push(key + '=' + value)
}
}
return _result.length ? prefix + _result.join('&') : ''
}
Tips 最新版本单点登录参数签名生成代码详见下文代码
这是在前端直接处理用户信息的方式,但是这样直接调用会引发另一个问题,就是浏览器的CROS跨域问题,这个时候就需要使用其他方式去实现功能,一种是将ticket传回后端,让后端获取用户信息然后回传前端;第二种方式就是使用浙里办提供的RPC网关去实现前端获取用户数据。
npm i --save @aligov/[email protected]
// 代码中引用
import { mgop } from '@aligov/jssdk-mgop';
调用方法改造
Tips 最新版本单点登录参数签名生成代码详见下文第7小点代码
getTonkenAndUserInfo: (data) => {
return new Promise((resolve, reject) => {
mgop({
api: '【API名称】',
host: 'https://mapi.zjzwfw.gov.cn/',
data: data,
dataType: 'JSON',
type: 'POST',
appKey: '【应用APPKEY】',
onSuccess: res => {
if (res.data.result && res.data.result == 0) {
resolve(res.data);
}
},
onFail: err => {
reject(err);
}
});
})
},
// 安装插件HMAC-SHA256签名插件crypto-js
// npm install crypto-js
// 2022最新规范示例
// Tips 最新版本单点登录参数签名生成代码详见下文代码
import CryptoJS from 'crypto-js/crypto-js';
getTonkenAndUserInfo: (data, method = 'ticketValidation') => {
let xTime = new Date().toGMTString();
return new Promise((resolve, reject) => {
mgop({
api: '【API名称】',
host: 'https://mapi.zjzwfw.gov.cn/',
data: data,
dataType: 'JSON',
type: 'POST',
appKey: '【应用APPKEY】',
// getHeader()方法在下方,参数说明
//获取Token填 自己的token
header: that.getHeader(),
onSuccess: res => {
if (res.data.result && res.data.result == 0) {
resolve(res.data);
}
},
onFail: err => {
reject(err);
}
});
})
},
getTonkenAndUserInfoParams: (method = 'ticketValidation', token = '') => {
let mTime = that.timeFormat(new Date(), 'yyyymmddhhMMss');
let data = {
method: method,
servicecode: 【接入码】,
time: mTime,
sign: md5Libs.md5(`${【接入码】}${【接入码密码】}${mTime}`),
datatype: 'json'
}
if (token == '') {
data.st = that.ticket;
} else {
data.token = token;
}
return data;
}
具体引用实现
that.getTonkenAndUserInfo(that.getTonkenAndUserInfoParams()).then(data => {
return that.getTonkenAndUserInfo(that.getTonkenAndUserInfoParams('getUserInfo', data.token));
}).then(data => {
that.userInfo = data;
}).catch(err => {
console.log(err);
});
最新版本单点登录参数签名生成代码
let accessKey = "xxx"; //替换成自己的ak
let secret="xxxx"; //替换成自己的sk
let path = pm.request.url.getPath();
let query=pm.request.url.query;
let queryArray=[];
for(index in query.members){
let member= query.members[index];
if(member["disabled"]==true){
continue;
}
let value = member["value"];
if(member["value"]==null){
value = ""
}
let queryKeyValue = encodeURIComponent(member["key"])+"="+encodeURIComponent(value);
queryArray.push(queryKeyValue);
}
queryArray.sort();
let queryString = queryArray.join("&");
let date = (new Date()).toGMTString();
let singString = pm.request.method+"\n"+path+"\n"+queryString+"\n"+accessKey+"\n"+date+"\n";
let hash = CryptoJS.HmacSHA256(singString, secret);
let hashInBase64 = CryptoJS.enc.Base64.stringify(hash);
pm.environment.set("X-BG-HMAC-ACCESS-KEY",accessKey);
pm.environment.set("X-BG-HMAC-SIGNATURE",hashInBase64);
pm.environment.set("X-BG-HMAC-ALGORITHM","hmac-sha256");
pm.environment.set("X-BG-DATE-TIME",date);
getHeader方法提取
getHeader: (subPath = '自己的Token') => {
let accessKey = 【ak】;
let secret = 【sk】;
let path = `/restapi/prod/${subPath}/sso/servlet/simpleauth`;
let queryString = "";
let xTime = (new Date()).toGMTString();
let singString = "POST"+"\n"+path+"\n"+queryString+"\n"+accessKey+"\n"+xTime+"\n";
let hash = CryptoJS.HmacSHA256(singString, secret);
let hashInBase64 = CryptoJS.enc.Base64.stringify(hash);
return {
'Content-Type': 'application/x-www-form-urlencoded',
'X-BG-HMAC-ACCESS-KEY': accessKey,
'X-BG-HMAC-SIGNATURE': hashInBase64,
'X-BG-HMAC-ALGORITHM': 'hmac-sha256',
'X-BG-DATE-TIME': xTime,
};
}
如果遇到应用上架检测外链,请用以下写法,官方白名单
// 法人用户
window.location.href = 'https://esso.zjzwfw.gov.cn/opensso/spsaehandler/metaAlias/sp?spappurl=' + url;
window.location.href = 'https://essotest.zjzwfw.gov.cn/opensso/spsaehandler/metaAlias/sp?spappurl=' + url;
// 个人用户
window.location.href = 'https://puser.zjzwfw.gov.cn/sso/usp.do?action=zfRedirect&servicecode' + code;
四、埋点操作
Ⅰ、旧版本aplus.js埋点
1、首先在public/index.html中引入埋点的api脚本,为防止出现https://https://这种双协议头调用,除脚本自身外,还需先填写基础PV日志的埋点函数,如果不需要单点登录采集用户数据则不需要最后的BLOCK,其他均为固定值,照抄就行。
2、页面PV日志采集,aplus函数在取得用户经纬度、用户数据、用户类型等数据后再进行调用。如没有单点登录获取用户数据,则仅需要留’aplus.sendPV’这一个。
aplus: () => {
aplus_queue.push({
'action': 'aplus.sendPV',
'arguments': [{
is_auto: false
}, {
miniAppId: '【APPID】',
miniAppName: '【APPNAME】',
long: that.longitude,
lati: that.latitude,
userType: that.userType,
}]
});
aplus_queue.push({
action: "aplus.setMetaInfo",
arguments: ["_user_nick", that.userInfo.username]
});
aplus_queue.push({
action: "aplus.setMetaInfo",
arguments: ["_user_id", that.userInfo.userid]
});
aplus_queue.push({
action: 'aplus.setMetaInfo',
arguments: ['_hold', 'START']
});
}
Ⅱ、新版本版本zwlog.js埋点
1、首先在public/index.html中引入埋点的api脚本
2、创建并声明zwlog对象实例,在创建zwlog实例时下述参数为必传,如需在没有用户信息时进行埋点则使用当前应用信息,如有用户信息则可使用用户信息
// 在创建zwlog实例时下述参数为必传,如需在没有用户信息时进行埋点则使用当前应用信息,如有用户信息则可使用用户信息
initLog: () => {
const zwlog = new ZwLog({
_user_id: "用户 ID", // 或应用appId
_user_nick: "用户昵称" // 或应用名称
})
}
3、日志事件发送
zwlog: () => {
zwlog.onReady(() => {
// sendPV⽅法将发送⼀条页面⽇志
zwlog.sendPV({
miniAppId: 'IRS 服务侧应用 appid',
Page_duration: '用户从进入到离开当前页面的时长',
t2: '页面启动到加载完成的时间',
t0: '页面启动到页面响应完成的时间',
log_status: 'IRS 服务侧应用 appid'
});
// record⽅法将发送⼀条事件⽇志,它接收三个参数:
// trackerEventCode:为注册的事件编码.当上报的事件为 PV 事件时,trackerEventCode 可传空值或'PageView'常量;
// eventType:事件类型 取值为'EXP':⾃定义曝光事件,'CLK':⾃定义点击事件,'OTHER': 其他⾃定义事件;
// eventParams: 为本次事件中上报的事件参数. 其取值为⼀个 JSON 对象(平铺的简单对象,不能多层嵌套);
// JSON 中的 key 不能是以下保留属性:
// uidaplus,spm-url,spm-pre,spm_cnt,pvid,_dev_id,_anony_id,_user_id,_user_nick,_session_id
zwlog.record('yourTrackerEventCode', 'CLK', {
Test1: '测试参数 1',
});
let { metaInfo } = zwlog; // SDK 元配置的当前设置
}
}
五、JSBridge的引入及使用
在浙里办的微应用中,很多地方需要使用到浙里办的一些封装API功能,也就是JSBridge,且所有JSBridgeAPI均支持Promise回调,这里主要讲解下常用的几个API以及引入。
JSBriage JS的引入,在public/index.html中引入
微信小程序需要使用最新版本的ZWJSBridge-1.1.0
JSBridge JS初始化,在App.vue中进行全局初始化
onLaunch: function() {
console.log('App Launch')
ZWJSBridge.onReady(() => {
console.log('初始化完成后,执行bridge方法')
})
},
初始化完成以后,就可以在单个页面使用 ZWJSBridge.API名称 调用功能了
常用的API
// 设置单页面标题
ZWJSBridge.setTitle({
"title": "标题"
}).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
// 获取用户类型
ZWJSBridge.getUserType({}).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
// 获取用户地理位置
ZWJSBridge.getLocation({}).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
//调用扫一扫
ZWJSBridge.scancode({}).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})