微信公众号开发——登录授权

1,016 阅读9分钟

微信公众号开发——登录授权

前言

授权方式

微信H5开发有两种授权方式:

  • 微信公众号授权登录方式

  • 微信开放平台授权登录方式

在本文中采用第一种方式来实现登录授权。

回调接口以及获取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

image.png

登录授权流程

用户授权流程

按照微信API文档介绍,用户授权流程分为下面几个步骤。

1 第一步:用户同意授权,获取code

2 第二步:通过code换取网页授权access_token

3 第三步:刷新access_token(如果需要)

4 第四步:拉取用户信息(需scope为 snsapi_userinfo)

登录授权流程图

以下为结合了微信授权流程的登录授权流程图: image.png

第一步:进行授权判定:

由于授权过,再次访问系统微信会在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地址:

api.weixin.qq.com/sns/oauth2/…

参数是否必须说明
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_inaccess_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协议):

api.weixin.qq.com/sns/userinf…

返回说明

正确时返回的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请求后端接口,让后端在响应体中放回数据。