微信公众号开发——登录授权
前言
授权方式
微信H5开发有两种授权方式:
-
微信公众号授权登录方式
- 官方地址:mp.weixin.qq.com/
- 基于微信公众号方式在微信授权
-
微信开放平台授权登录方式
- 官方地址:open.weixin.qq.com/
- 基于网页形式扫码等登录授权
在本文中采用第一种方式来实现登录授权。
回调接口以及获取code方式:
公众号授权方式的回调接口,本文使用后端来接收回调接口的code,具体在下文中详细说明。
内网穿透
由于需要通过微信服务器访问我们的前后端,所以需要搭配内网穿透进行开发调试,此处本文不做过多介绍。
公众号设置
微信给开发者提供的测试账号平台:mp.weixin.qq.com/debug/cgi-b…
微信开发文档:developers.weixin.qq.com/doc/offiacc…
对于公众号测试账号的申请,本文不做不过多的展示,以上为开发过程中会用到两个连接。
授权回调接口设置
授权回调接口用于用户授权后获取code,继而获取用户信息,详细的授权流程在下文说明。
在测试平台中设置微信授权的回调接口,注意此处设置的是域名,不包括协议以及uri域名后的(需要与后文发送获取code请求中的redirect_uri一致)。
如:blog.querkecor.vip/archives/he…
域名只为其中的:blog.querkecor.vip
登录授权流程
用户授权流程
按照微信API文档介绍,用户授权流程分为下面几个步骤。
2 第二步:通过code换取网页授权access_token
4 第四步:拉取用户信息(需scope为 snsapi_userinfo)
登录授权流程图
以下为结合了微信授权流程的登录授权流程图:
第一步:进行授权判定:
由于授权过,再次访问系统微信会在URL中保留参数即路径中的token和openId。
判断是否授权:
- 第一种:URL中携带了token,openId即为授权过并且绑定了系统用户,正常放行访问目标地址;
- 第二种:URL中只携带了openId,授权过,但是没有绑定系统用户,弹出手机号绑定框;
- 第三种:没有token和openId,进行授权,请求后端授权接口。
App.vue前端代码展示:
说明:App.vue中的代码在每次构建页面时(进行重定向时)都会进行判断。
created() {
// 处理微信授权登录
this.wechatLogin();
},
methods: {
wechatLogin() {
// 处理微信授权登录
let token = this.getQueryString('token') || '';
let openId = this.getQueryString('openId') || '';
// token === '' && openId != '' 只要这种情况,未绑定账号
if ((token === '' || token === "null") && openId !== '') {
// 绑定账号
this.bindPhoneVo.openId = openId;
this.dialogVisible = true;
} else {
// 如果绑定了,授权登录直接返回token
if (token !== '' && token !== "null") {
window.localStorage.setItem('token', token);
}
token = window.localStorage.getItem('token') || '';
if (token === '' || token === "null") {
let url = window.location.href.replace('#', 'aug');
//url用于授权后跳转目标地址
window.location = 'http://{后端地址}/admin/wechat/authorize?returnUrl=' + url;
}
}
},
// 绑定手机号
saveBind() {
if (this.bindPhoneVo.phone.length != 11) {
alert('手机号码格式不正确');
return;
}
userInfoApi.bindPhone(this.bindPhoneVo).then(response => {
window.localStorage.setItem('token', response.data);
this.dialogVisible = false;
// 绑定完要去的地址
window.location = 'http://{后端地址}';
});
},
// 获取路径参数
getQueryString(paramName) {
if (window.location.href.indexOf('?') == -1) return '';
let searchString = window.location.href.split('?')[1];
let i, val, params = searchString.split("&");
for (i = 0; i < params.length; i++) {
val = params[i].split("=");
if (val[0] == paramName) {
return val[1];
}
}
return '';
}
}
}
第二步:构建授权地址:
根据微信API文档说明,微信授权地址为:
open.weixin.qq.com/connect/oau…
?appid=${APPID}
&redirect_uri=${REDIRECT_URI}
&response_type=code
&scope=${SCOPE}
&state=STATE //在state中我们放入用户进入系统时要跳转的目标地址
#wechat_redirect
| 参数 | 是否必须 | 说明 |
|---|---|---|
| appid | 是 | 公众号的唯一标识 |
| redirect_uri | 是 | 授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理 |
| response_type | 是 | 返回类型,请填写code |
| scope | 是 | 应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且, 即使在未关注的情况下,只要用户授权,也能获取其信息 ) |
| state | 否 | 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节 |
| #wechat_redirect | 是 | 无论直接打开还是做页面302重定向时候,必须带此参数 |
| forcePopup | 否 | 强制此次授权需要用户弹窗确认;默认为false;需要注意的是,若用户命中了特殊场景下的静默授权逻辑,则此参数不生效 |
后端授权接口代码:
使用wxMpService以来来构建微信授权地址,并且携带用户进入目标页面的地址,以便授权完跳转。
构建好地址,让前端进行重定向到授权页面引导用户进行授权,在用户授权完微信调用回调方法访问redirect_uri中设置的回调接口。
/**
* 前端拦截器发送授权
* @param returnUrl 用户点击菜单要进入的页面地址,授权后跳转到目标地址
* @param request
* @return
*/
@GetMapping("/authorize")
public String authorize(@RequestParam("returnUrl") String returnUrl, HttpServletRequest request) {
log.info("【微信网页授权】获取code,returnUrl={}", returnUrl);
String redirectUrl = null;
try {
redirectUrl = wxMpService.getOAuth2Service()
.buildAuthorizationUrl(userInfoUrl,
WxConsts.OAuth2Scope.SNSAPI_USERINFO,
URLEncoder.encode(returnUrl.replace("aug", "#"), "utf-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
log.info("【微信网页授权】获取code,redirectURL={}", redirectUrl);
return "redirect:" + redirectUrl;
}
第三步:获取用户信息,并进行业务逻辑判断,返回token
1. 获取access_token
在上一步说到授权后会调用回调接口,并且在请求中会携带code以及state参数。
拿到code后,根据微信API接口文档说明,我们通过code需要获取access_token。
下面为获取access_token地址:
| 参数 | 是否必须 | 说明 |
|---|---|---|
| appid | 是 | 公众号的唯一标识 |
| secret | 是 | 公众号的appsecret |
| code | 是 | 填写第一步获取的code参数 |
| grant_type | 是 | 填写为authorization_code |
返回说明
正确时返回的JSON数据包如下:
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE",
"is_snapshotuser": 1,
"unionid": "UNIONID"
}
| 参数 | 描述 |
|---|---|
| access_token | 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同 |
| expires_in | access_token接口调用凭证超时时间,单位(秒) |
| refresh_token | 用户刷新access_token |
| openid | 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID |
| scope | 用户授权的作用域,使用逗号(,)分隔 |
| is_snapshotuser | 是否为快照页模式虚拟账号,只有当用户是快照页模式虚拟账号时返回,值为1 |
| unionid | 用户统一标识(针对一个微信开放平台账号下的应用,同一用户的 unionid 是唯一的),只有当scope为"snsapi_userinfo"时返回 |
2.拉取用户信息(需scope为 snsapi_userinfo)
在获取到access_token以及openId后,需要再通过access_token获取到用户的微信信息(可根据自身业务逻辑做其他作用,本文中仅演示获取)。
获取用户信息地址:
http:GET(请使用https协议):
返回说明
正确时返回的JSON数据包如下:
{
"openid": "OPENID",
"nickname": NICKNAME,
"sex": 1,
"province":"PROVINCE",
"city":"CITY",
"country":"COUNTRY",
"headimgurl":"https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
"privilege":[ "PRIVILEGE1" "PRIVILEGE2" ],
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
| 参数 | 描述 |
|---|---|
| openid | 用户的唯一标识 |
| nickname | 用户昵称 |
| sex | 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知 |
| province | 用户个人资料填写的省份 |
| city | 普通用户个人资料填写的城市 |
| country | 国家,如中国为CN |
| headimgurl | 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。 |
| privilege | 用户特权信息,json 数组,如微信沃卡用户为(chinaunicom) |
| unionid | 只有在用户将公众号绑定到微信开放平台账号后,才会出现该字段。 |
下面为回调接口代码:
在获取完用户信息后,执行我们的业务逻辑;
通过之前步骤获取到的用户openId进行校验用户是否存在,如果存在则生成token,让前端进行重定向到state的地址,此地址为用户原始点击要进入的页面,并且在参数中携带token和openId,重定向让前端访问目标地(App.vue中的代码在每次重定向时都会执行进行判断,是否授权,判断token以及openId)。
@GetMapping("/userInfo")
public String userInfo(@RequestParam("code") String code,
@RequestParam("state") String returnUrl) {
try {
// 根据code获取微信openId
WxOAuth2AccessToken accessToken = wxMpService.getOAuth2Service().getAccessToken(code);
String openId = accessToken.getOpenId();
log.info("【微信网页授权】openId={}", openId);
WxOAuth2UserInfo wxMpUser = wxMpService.getOAuth2Service().getUserInfo(accessToken, null);
log.info("【微信网页授权】wxMpUser={}", JSON.toJSONString(wxMpUser));
System.out.println(returnUrl);
// 根据openId查数据库,判断当前用户是否绑定微信
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getOpenId, openId);
SysUser sysUser = sysUserService.getOne(wrapper);
// 如果用户已绑定微信生成token返回
if (sysUser != null) {
String token = JwtHelper.createToken(sysUser.getId(), sysUser.getUsername());
if (returnUrl.indexOf("?") == -1) {
log.info("最后重定向:"+ "redirect:" + returnUrl + "?token=" + token +"&openId="+ openId);
return "redirect:" + returnUrl + "?token=" + token +"&openId="+ openId;
} else {
return "redirect:" + returnUrl + "&token=" + token +"&openId="+ openId;
}
} else {
return "redirect:"+returnUrl+ "?openId=" + openId;
}
} catch (WxErrorException e) {
throw new RuntimeException(e);
}
}
3.绑定系统用户
如果在上一步根据openId没有找到系统用户,在重定向是只会携带openId。
return "redirect:"+returnUrl+ "?openId=" + openId;
在重定向时前端App.vue会执行弹出手机号绑定框。
if ((token === '' || token === "null") && openId !== '') {
// 绑定账号
this.bindPhoneVo.openId = openId;
this.dialogVisible = true;
}
<el-dialog title="绑定手机" :visible.sync="dialogVisible" width="80%">
<el-form ref="dataForm" :model="bindPhoneVo" size="small">
<h4>绑定你的手机号,建立系统关联关系</h4>
<el-form-item label="手机号码">
<el-input v-model="bindPhoneVo.phone" />
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button type="primary" icon="el-icon-check" @click="saveBind()" size="small">确 定</el-button>
</span>
</el-dialog>
输入手机号后点击确定向后端发送绑定请求。
saveBind() {
if (this.bindPhoneVo.phone.length != 11) {
alert('手机号码格式不正确');
return;
}
userInfoApi.bindPhone(this.bindPhoneVo).then(response => {
window.localStorage.setItem('token', response.data);
this.dialogVisible = false;
window.location = 'http://5193866jz4.goho.co';
});
},
后端绑定手机号接口:
根据BindPhoneVo 中的手机号判断是否有用户,如果有进行openId绑定,生成token后进行重定向到目标地址。重定向重复App.vue中的代码。
@PostMapping("bindPhone")
public String bindPhone(@RequestBody BindPhoneVo bindPhoneVo) {
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getPhone, bindPhoneVo.getPhone());
SysUser sysUser = sysUserService.getOne(wrapper);
if (null != sysUser) {
sysUser.setOpenId(bindPhoneVo.getOpenId());
sysUserService.updateById(sysUser);
String token = JwtHelper.createToken(sysUser.getId(), sysUser.getUsername());
return "redirect:" + bindPhoneVo.getReturnUrl() + "?token=" + token +"&openId="+ bindPhoneVo.getOpenId();
} else {
return "手机号码不存在,绑定失败";
}
}
4.重定向
至此用户授权登录结束,在最后重定向访问目标地址时执行App.vue中的代码将请求参数中的token放进本地。
// 如果绑定了,授权登录直接返回token
if (token !== '' && token !== "null") {
window.localStorage.setItem('token', token);
}
后记
使用后端作为回调接口,继而让前端重定向时携带token以及openId在路径中会有安全风险,本文只提供此种方式的一个思路。考虑到安全性可以让回调接口为前端,继而通过携带code请求后端接口,让后端在响应体中放回数据。