项目结构
- 前端:Vue
- 后端:Koa
- 数据库:MySQL
问题
- SPA单页应用如何解决微信授权时的跳转?
- SPA单页应用应该在那个环节获取微信授权呢?
微信网页授权
微信网页授权操作流程
- 用户同意授权以获取code
- 通过code换取网页授权的access_token
首先需要明确的是,前后端分离的项目,是不能直接在前端使用AJAX调用微信接口获取到用户信息的。为什么?前端是基于浏览器,浏览器具有同源策略限制。使用AJAX直接获取本域名外的地址,是需要跨域处理的。虽然前端提供了JSONP,但需要服务端配置。你在前端使用JSONP访问微信的接口,微信的程序员是不会配合你的。所以除了获取code直接使用跳转到微信,微信在跳转会你的前端页面是可行的。不过需要将前端的地址配置到公众后台。否则redirect_uri不会被微信识别。至于网页授权的其他接口,直接使用AJAX访问,微信则会提示跨域错误。因此,必须将这些接口放到后端来实现。
用户授权获取code,发起授权何时触发呢?
- 前端获取code传递给后端
向微信发起授权时需传递一个重定向的 redirectUri 地址,该地址用于获取微信返回的 code。当微信接收到授权请求后会将 code 拼接到 redirectUri 链接后面并重定向回来。我们可以拿到code之后再传递给后端,后端利用 code 去获取access_token。
若前后端分离且使用不同的URL地址,此时需要在微信公众平台配置前后端两套域名,因为前端也调用了微信接口,不然会出现 redirect_uri 参数错误的提示。
- 前端传递授权地址,后端调用微信接口
前端首先获取当前地址并传递给后端,后端通过微信授权接口,将此地址作为微信授权中的redirect_uri
参数,重定向到前端后获取code。
前端首先使用路由拦截,然后先后端发起请求。后端向微信发起获取code的请求,微信重定向到前端授权页面,在前端授权页面中获取code值。前端获取code值后向后端发送请求,后端向微信发起授权请求并获取用户数据,返回给前端。
- 后端认证授权返回前端地址
前后端约定授权成功后返回的前端地址,用户直接访问后端微信授权的地址。后端与微信交互时前端不参与。当授权成功后,后端在跳转到前端指定地址。若前端地址修改,则后端必须更改。
这里采取的方式是在前端通过微信接口获取code,再请求后端通过微信接口获取access_token,通过access_token获取用户信息,完成用户关注后自动注册。
需要注意的,由于前端使用微信接口,针对使用vue做了前后端分离的项目,需要提前为前端配置域名,并在微信公众平台中添加,这样前端才能访问微信接口,否则会出现 redirect_uri 错误的提示。
微信网页授权获取票据code
微信的code参数作为换取access_token
的票据,每次用户同意授权后,所携带的code值都不一样。code只能使用一次,若5分钟未被使用将自动过期。
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE&connect_redirect=1#wechat_redirect
由于微信接口授权操作安全等级较高,在发起授权请求时,微信会对授权链接做正则匹配校验。若链接参数顺序错误,授权页面将无法正常访问。
接口参数 | 必填 | 描述 |
---|---|---|
appid | 是 | 微信公众号开发者唯一编号 |
redirect_uri | 是 | 微信授权后重定向的回调链接地址,必须使用urlencode对链接进行处理。 |
response_type | 是 | 返回类型,默认为code。 |
scope | 是 | 应用授权作用域,分为snsapi_base和snsapi_user_info。 |
state | 否 | 重定向后会携带state参数,开发者可自定义参数值,128个字节。 |
#wechat_redirect | 是 | - |
确保微信公众号拥有授权作用域即scope
参数的权限下,使用授权接口。微信服务号获取高级接口后,默认拥有scope
参数中的snsapi_base
和snsapi_userinfo
。
应用授权作用域 | 描述 |
---|---|
snsapi_base | 静默授权,不弹出授权界面,直接跳转,只能获取用户openid,无法获取用户信息。 |
snsapi_userinfo | 主动授权,弹出授权界面,可通过openid获取昵称、性别、所在地。 |
静默授权由于是自动授权并跳转的,因此对用户来说是无感知的,但这种授权方式只能获取用户的openid,无法获取用户其他信息。在只需要标识用户而无需收集其他信息的场景中使用。比如投票、点赞等。
主动授权是当用户进入页面后会弹出授权窗口,需手动同意。因此可获取用户的基本信息。对于已关注微信公众号的用户,当用户从公众号会话或自定义菜单进入授权页面时,即使使用snsapi_userinfo也是静默授权的。
当用户同意授权后页面间跳转到redirect_uri/?code=CODE&state=STATE
页面。
vue中使用微信授权获取code值
$ vim Index.vue
在vue的craeted方法中,首先判断运行环境是否为生产环境,然后判断是否为微信浏览器,通过获取当前连接地址中的code参数来判断,是否使用微信接口来授权。如果没有code则跳转授权。若获取到code值则根据code值去请求后台微信接口获取用户信息。
created(){
const env = process.env.NODE_ENV;
if(env === "production"){
if(!this.isWechat()){
Toast("请在微信中打开");
return;
}
const code = this.$route.query.code;
//console.log(code, !code, !!code, code===undefined);
if(code === undefined){
const appId = process.env.VUE_APP_WX_APPID;
//const redirectUrl = process.env.VUE_APP_URL;
const redirectUrl = window.location.href;//当前地址
this.auth(appId, redirectUrl);
}else{
//微信授权获取用户信息
const url = `${this.apiUrl}/api/grant?code=${code}`;
this.axios.get(url).then(res=>{
const ret = res.data;
if(ret.code !== 200){
Toast(ret.message);
return;
}
}).catch(err=>{
//console.error(err);
});
}
}
this.env = env;
},
methods:{ //微信浏览器判断
isWechat(){
const ua = window.navigator.userAgent.toLowerCase();
return ua.indexOf('micromessenger') !== -1;
},
//微信授权
auth(appId, redirectUrl, state="STATE"){
const redirectUri = encodeURIComponent(redirectUrl);
let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`;
console.log(url);
window.location.href = url;
},
}
是否需要缓存code呢?
当用户进入目标URL首先会获取code,然后将code传递给后端以获取用户信息并返回。缓存code会出现的问题是,如果code缓存时间过长则会出现重复code。这里面涉及的问题是code是否能够被重复消费呢?如果使用code作为缓存键值,则需要定时清理。当使用code能获取到缓存则走缓存,否则重新获取。为避免出现code出现重复,因此需要定理清理缓存。因为code是一次性的,若重复消费则在获取微信openid时会出现为空的现象。
{
“errcode”:40163,
"errmsg":"code been used"
}
如何解决刷新网页出现code重复消息的问题呢?
通过code获取openid时,code只能使用一次。因此,可以在第一次获取到opennid后就将其缓存起来。刷新时判断openid是否存在,若不存在则通过code获取openid。
JavaScript封装
//网页授权 获取临时凭证code
exports.authorize = (appid, redirect_uri, state="STATE", scope="snsapi_userinfo")=>{
const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirect_uri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
return url;
};
access_token
微信开发中access_token分为两种,一种是普通的access_token即公众号接口全局唯一票据,是调用接口访问的凭证。另一种是网页授权时通过code换取的access_token,用于获取微信用户信息。若两种access_token发生混淆则会出现"invalid access_token"的错误。
网页授权凭证 access_token
当用户在微信客户端中访问第三方页面时,微信公众号可通过微信网页授权机制,获取用户的基本信息。
微信网页授权是通过OAuth2.0机制实现的,在用户授权给公众号后,公众号可以获得一个网页授权特有的接口调用凭证access_token,通过此access_token可以在授权后调用接口获取微信用户基本信息。
网页授权凭证access_token
有效期为7200秒,用于获取对应微信用户的信息,与微信用户是一对一的关系且调用次数无限制。使用网页授权时,不管用户是否关注过微信公众号,均可获取对应用户的个人信息。
网页授权接口地址GET
GET https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
接口参数 | 必须 | 描述 |
---|---|---|
appid | 是 | 微信公众号唯一标识,开发者账号。 |
secret | 是 | 微信公众号开发者密钥 |
code | 是 | - |
grant_type | 是 | 授权类型,为authorization_code。 |
取网页授权凭证时会遇到跨域问题,解决的方法是利用代理进行中转。
跨域是浏览器的同源策略问题而衍生出的需求,跨域请求是指服务器A的页面去请求服务器2的资源,服务器1和2之间只要域名、端口、IP不同都属于跨域。
在Node.js环境中,可以使用SuperAgent,SuperAgent是一个轻量级、灵活的、易读的、低学习曲线的客户端请求代理模块。
成功返回
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE"
}
成功参数 | 描述 |
---|---|
access_token | 网页授权接口调用凭证 |
expires_in | 网页授权接口调用凭证超时时间,单位为秒。 |
refresh_token | 用户刷新access_token |
openid | 用户唯一标识 |
scope | 用户授权作用域 |
当用户未关注公众号时,访问需网络授权的页面时,也会产生一个用户和公众号唯一的openid。
错误返回
{
errcode: 40125,
errmsg: 'invalid appsecret, view more at http://t.cn/RAEkdVq, hints: [ req_id: YhmAs.5ce-Jw_PAa ]'
}
出现错误码40125,提示非法的AppSecret。核对32位AppSecret并未发现错误,可微信公众平台重置AppSecret。并在代码中对应位置错误处理。出现这种问题,可能是微信公众平台中的AppSecret被重置导致原始AppSecret失效造成的。
JavaScript封装
//网页授权 根据code换取access_token。
exports.accessToken = (appid, secret, code)=>{
const url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appid}&secret=${secret}&code=${code}&grant_type=authorization_code`;
return this.httpGet(url);
};
刷新访问凭证
关于refresh_token为什么需要刷新呢?
因为access_token的使用周期是7200秒(2小时),若超时则失效就无法再使用。此时就需要进行刷新操作,根据原来获取access_token时返回的refresh_token来刷新。
refresh_token的有效期为30天,但refresh_token失效后则需用户重新授权。
https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN
接口参数 | 必须 | 描述 |
---|---|---|
appid | 是 | 微信公众号唯一标识 |
grant_type | 是 | 授权类型refresh_token |
refresh_token | 是 | 通过access_token获取的refresh_token |
成功返回
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE"
}
返回参数 | 描述 |
---|---|
access_token | 网页授权接口调用凭证 |
expires_in | access_token接口调用凭证超时秒数 |
refresh_token | 用户刷新的access_token |
openid | 用户唯一标识 |
scope | 用户授权作用域,多个使用逗号分隔 |
错误返回
{
"errcode":40029,
"errmsg":"invalid code"
}
JS封装
//网页授权 access_token超时刷新
exports.refreshToken = (appid, refresh_token)=>{
const url = `https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=${appid}&grant_type=refresh_token&refresh_token=${refresh_token}`;
return this.httpGet(url);
};
接口访问凭证access_token
access_token是微信公众号全局唯一票据,是公众号调用接口访问的凭证。
access_token存储至少要保存512个字符空间,有效期为2小时7200秒,需定时刷新。当access_token过期时,才需要再次调用接口获取。理想情况下,一个7x24小时运行的系统,每天需要获取12次access_token,即每隔2小时获取一次。若在有效期内再次获取access_token将会导致上次获取的access_token失效。
获取access_token接口调用频率限制为2000次/天,因此需要将获取到的access_token存储起来,然后定期调用access_token更新,以保证随时取出的access_token都是有效的。
接口地址GET
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
接口参数 | 必须 | 描述 |
---|---|---|
grant_type | 是 | 获取access_token使用client_credential |
appid | 是 | 第三方用户唯一凭证 |
secret | 是 | 第三方用户唯一凭证密钥,即appsecret。 |
成功返回
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200
}
成功返回参数 | 描述 |
---|---|
access_token | 接口访问凭证 |
expires_in | 凭证有效时长,单位为秒。 |
错误返回
{
"errcode":40013,
"errmsg":"invalid appid"
}
错误返回参数 | 描述 |
---|---|
errcode | 错误编码 |
errmsg | 错误信息 |
JS封装方法
//获取微信接口访问凭证
exports.token= async (appid, secret)=>{
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}`;
return this.httpGet(url);
};
获取微信用户信息
网页授权后若需拉取用户信息,则必须在获取code值将scope接口访问作用域设置为snsapi_userinfo。在通过code获取access_token和openid后,在通过接口获取用户信息。
接口地址
GET https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
接口参数 | 描述 |
---|---|
access_token | 网页授权接口调用凭证 |
openid | 用户唯一标识 |
lang | 返回国家地区语言版本,zh_CN简体 / zh_TW繁体 / en英语 |
成功返回
{
"openid": "OPENID",
"nickname": "NICKNAME",
"sex": "1",
"province": "PROVINCE"
"city": "CITY",
"country": "COUNTRY",
"headimgurl": "http://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
"privilege": [ "PRIVILEGE1" "PRIVILEGE2" ],
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
返回参数 | 描述 |
---|---|
opoenid | 用户唯一标识 |
unionid | 微信公众号绑定到微信开放平台后才会出现此字段 |
nickname | 微信用户昵称 |
headimgurl | 微信用户头像 |
sex | 微信用户性别 0未知 / 1男性 / 2女性 |
province | 省份 |
city | 城市 |
country | 国家,CN中国 |
privilege | 微信用户特权信息,JSON格式。 |
//网页授权 获取微信用户信息
exports.userinfo = (access_token, openid, lang="zh_CN")=>{
const url = `https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openid}&lang=${lang}`;
return this.httpGet(url);
};
微信自定义分享
微信自定义分享主要是分享网页,是将自己的页面分享到微信中以卡片形式展现,含有标题、摘要、缩略图,而非单纯的分享链接。
- 在微信公众号管理平台中设置JS接口安全域名,域名需通过ICP备案。
- 微信自定义分享需通过微信认证
- 微信自定义分享只能定义分享内容和格式,不能为DOM元素绑定事件来执行分享,只能点击微信右上角的分享才有效果。
- 微信自定义分享需首先引入JSSDK的JS文件
微信自定义分享JSSDK文件
http://res.wx.qq.com/open/js/jweixin-1.2.0.js
http://res.wx.qq.com/open/js/jweixin-1.4.0.js
http://res2.wx.qq.com/open/js/jweixin-1.4.0.js
...
使用JSSDK的页面首先必须注入配置信息,否则将无法调用,每个URL仅需调用一次,对于变化URL的SPA的WebApp可以在每次URL变化时进行调用。
注入配置
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: '', // 必填,公众号的唯一标识
timestamp: , // 必填,生成签名的时间戳
nonceStr: '', // 必填,生成签名的随机串
signature: '',// 必填,签名
jsApiList: [] // 必填,需要使用的JS接口列表
});
配置参数可使用后台接口获取,需要获取的参数包括signature、nonceStr、timestamp,最核心的参数是signature签名。
生成签名
- 后台获取微信接口全局访问凭证 access_token,接口调用单日2000次限制。
- 使用access_token获取jsapi_ticket
- 生成签名
获取接口全局访问凭证
//微信接口 获取微信接口访问凭证 access_token
exports.token = async (appid, secret)=>{
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}`;
return this.httpGet(url);
};
成功返回
成功返回参数 | 描述 |
---|---|
access_token | 接口访问凭证 |
expires_in | 凭证有效时长,单位为秒,默认7200秒即2小时。 |
由于access_token有效期为2小时,每日使用上限为2000次。需要缓存并定时刷新,缓存的方式可以使用设置过期时间的Session,也可以使用设置过期时间的Redis。另外,最好添加上时间标识。
使用Redis保存access_token时,由于每个appid会对应一个不同的access_token,因此使用hash哈希形式保存对象,以appid作为key。
jsapi_ticket
jsapi_ticket 是微信公众号用于调用微信JS接口的临时票据,正常情况下,jsapi_ticket的有效期是7200秒即2小时。jsapi_ticket可通过全局接口访问票据access_token来获取。由于获取jsapi_ticket的API接口调用次数具有每日上限,频繁刷新jsapi_ticket将会导致API调用受限,影响自身业务,因此开发者必须在自己的服务器中全局缓存jsapi_ticket。
接口地址
GET https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
//微信接口 获取授权页ticket
exports.getTicket = (access_token, type="jsapi")=>{
const url = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${access_token}&type=${type}`;
return this.httpGet(url);
};
成功返回
{
"errcode":0,
"errmsg":"ok",
"ticket":"bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA",
"expires_in":7200
}
封装
由于项目是前后端分离,前端使用vue后端使用koa,后端使用koa做HTTP服务器,用于提供API接口使用。当前端获取到code时,会见code传递给后端。
后端封装微信接口的处理方法
$ npm i -S superagent
$ vim wechat.js
const crypto = require("crypto");
const superagent = require("superagent");
//微信接口错误信息
const error = {
"-1":"系统繁忙,请稍后再试。",
"0":"请求成功",
"40001":"AppSecret错误",
"40002":"请确保grant_type为client_credential",
"40164":"调用接口的IP不在白名单内"
};
//SHA1加密
exports.sha1 = code=>crypto.createHash("sha1").update(code).digest("hex");
//生成随机字符串
exports.noncestr = _=>Math.random().toString(36).substr(2, 15);
//生成秒级时间戳
exports.timestamp = _=>parseInt(new Date().getTime() / 1000);
//生成签名
exports.signature = args=>{
const raw = `jsapi_ticket=${args.jsapi_ticket}&noncestr=${args.noncestr}×tamp=${args.timestamp}&url=${args.url}`;
console.log(raw);
const sha1 = this.sha1(raw);
console.log(sha1);
return sha1;
};
//微信接口 获取微信接口访问凭证 access_token
exports.token = async (appid, secret)=>{
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}`;
return this.httpGet(url);
};
//微信接口 获取授权页ticket
exports.getTicket = (access_token, type="jsapi")=>{
const url = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${access_token}&type=${type}`;
return this.httpGet(url);
};
//网页授权 获取临时凭证code
exports.authorize = (appid, redirect_uri, state="STATE", scope="snsapi_userinfo")=>{
const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirect_uri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
return url;
};
//网页授权 根据code换取access_token。
exports.accessToken = (appid, secret, code)=>{
const url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appid}&secret=${secret}&code=${code}&grant_type=authorization_code`;
return this.httpGet(url);
};
//网页授权 access_token超时刷新
exports.refreshToken = (appid, refresh_token)=>{
const url = `https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=${appid}&grant_type=refresh_token&refresh_token=${refresh_token}`;
return this.httpGet(url);
};
//网页授权 获取微信用户信息
exports.userinfo = (access_token, openid, lang="zh_CN")=>{
const url = `https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openid}&lang=${lang}`;
return this.httpGet(url);
};
//CURL发送HTTP协议GET请求
exports.httpGet = async url=>{
console.log(url);
const response = await superagent.get(url);
const result = JSON.parse(response.text);
//console.log(result);
return result;
};