一、引言
全网最全的前后端分离微信网页授权解决方案。如果有更好的优化方案,欢迎多多交流
二、网页授权的步骤
-
1 第一步:用户同意授权,获取code
-
2 第二步:通过code换取网页授权access_token
-
3 第三步:刷新access_token(如果需要)
-
4 第四步:拉取用户信息(需scope为 snsapi_userinfo)
-
5 附:检验授权凭证(access_token)是否有效
注意:这里的access_token属于网页授权access_token,而非普通授权的access_token,官方给出的解释如下:
关于网页授权access_token和普通access_token的区别 1、微信网页授权是通过OAuth2.0机制实现的,在用户授权给公众号后,公众号可以获取到一个网页授权特有的接口调用凭证(网页授权access_token),通过网页授权access_token可以进行授权后接口调用,如获取用户基本信息; 2、其他微信接口,需要通过基础支持中的“获取access_token”接口来获取到的普通access_token调用。
但是没有讲得很明白。其实两者的区别就是:
-
第一,网页授权access_token只要用户允许后就可以获取用户信息,可以不关注公众号,而普通access_token没有关注公众号,获取用户信息为空;
-
第二,两者的每日限制调用频次不同,普通access_token每日2000次,获取网页授权access_token不限次数,获取用户信息每日5万次。
三、后端接入
后端采用开源工具weixin-java-tools
3.1 pom.xml引入jar包
<dependency><groupId>com.github.binarywang</groupId><artifactId>weixin-java-mp</artifactId><version>3.8.0</version></dependency>
3.2 application.yml添加配置
这里换成自己的appid和appsecret
# 微信公众号wechat: mpAppId: appid mpAppSecret: appsecret
3.3 新建读取配置文件WechatMpProperties.java
package com.hsc.power.dm.wechat.config;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;/** * 微信公众号配置文件 * * @author liupan * @date 2020-05-26 */@Data@Component@ConfigurationProperties(prefix = "wechat")public class WechatMpProperties { private String mpAppId; private String mpAppSecret;}
3.4 新建自定义微信配置WechatMpConfig.java
package com.hsc.power.dm.wechat.config;import me.chanjar.weixin.mp.api.WxMpService;import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;import me.chanjar.weixin.mp.config.WxMpConfigStorage;import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.stereotype.Component;/** * 微信公众号配置 * * @author liupan * @date 2020-05-26 */@Componentpublic class WechatMpConfig { @Autowired private WechatMpProperties wechatMpProperties; /** * 配置WxMpService所需信息 * * @return */ @Bean // 此注解指定在Spring容器启动时,就执行该方法并将该方法返回的对象交由Spring容器管理 public WxMpService wxMpService() { WxMpService wxMpService = new WxMpServiceImpl(); // 设置配置信息的存储位置 wxMpService.setWxMpConfigStorage(wxMpConfigStorage()); return wxMpService; } /** * 配置appID和appsecret * * @return */ @Bean public WxMpConfigStorage wxMpConfigStorage() { // 使用这个实现类则表示将配置信息存储在内存中 WxMpDefaultConfigImpl wxMpDefaultConfig = new WxMpDefaultConfigImpl(); wxMpDefaultConfig.setAppId(wechatMpProperties.getMpAppId()); wxMpDefaultConfig.setSecret(wechatMpProperties.getMpAppSecret()); return wxMpDefaultConfig; }}
3.5 新建微信用户Bean
package com.hsc.power.dm.wechat.vo;import lombok.Data;import me.chanjar.weixin.mp.bean.result.WxMpUser;@Datapublic class WechatUser { public WechatUser(WxMpUser wxMpUser, String accessToken) { this.setAccessToken(accessToken); this.setOpenid(wxMpUser.getOpenId()); this.setUnionId(wxMpUser.getUnionId()); this.setNickname(wxMpUser.getNickname()); this.setLanguage(wxMpUser.getLanguage()); this.setCountry(wxMpUser.getCountry()); this.setProvince(wxMpUser.getCity()); this.setCity(wxMpUser.getCity()); this.setSex(wxMpUser.getSex()); this.setSexDesc(wxMpUser.getSexDesc()); this.setHeadImgUrl(wxMpUser.getHeadImgUrl()); } private String openid; private String accessToken; private String unionId; private String nickname; private String language; private String country; private String province; private String city; private Integer sex; private String sexDesc; private String headImgUrl;}
3.6 授权接口WechatController.java
-
/auth:获取授权跳转地址
-
/auth/user/info:初次授权获取用户信息
-
/token/user/info:静默授权获取用户信息
package com.hsc.power.dm.wechat.web;import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;import com.hsc.power.core.base.ret.Rb;import com.hsc.power.dm.wechat.vo.WechatUser;import lombok.extern.slf4j.Slf4j;import me.chanjar.weixin.common.api.WxConsts;import me.chanjar.weixin.common.error.WxErrorException;import me.chanjar.weixin.mp.api.WxMpService;import me.chanjar.weixin.mp.bean.result.WxMpOAuth2AccessToken;import me.chanjar.weixin.mp.bean.result.WxMpUser;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import java.net.URLEncoder;/** * 微信公众号接口 * * @author liupan * @date 2020-05-26 */@Slf4j@RestController@RequestMapping("/wechat")public class WechatController { @Autowired private WxMpService wxMpService; /** * 获取code参数 * * @param returnUrl 需要跳转的url * @return */ @GetMapping("/auth") public Rb<String> authorize(@RequestParam String authCallbackUrl, @RequestParam String returnUrl) { // 暂时将我们的回调地址硬编码在这里,方便一会调试 // 获取微信返回的重定向url String redirectUrl = wxMpService.oauth2buildAuthorizationUrl(authCallbackUrl, WxConsts.OAuth2Scope.SNSAPI_USERINFO, URLEncoder.encode(returnUrl)); log.info("【微信网页授权】获取code,redirectUrl = {}", redirectUrl); return Rb.ok(redirectUrl); } /** * 初次授权获取用户信息 * * @param code * @param returnUrl * @return */ @GetMapping("/auth/user/info") public Rb<WechatUser> userInfo(@RequestParam("code") String code, @RequestParam("state") String returnUrl) { WxMpOAuth2AccessToken wxMpOAuth2AccessToken; WxMpUser wxMpUser; try { // 使用code换取access_token信息 wxMpOAuth2AccessToken = wxMpService.oauth2getAccessToken(code); wxMpUser = wxMpService.oauth2getUserInfo(wxMpOAuth2AccessToken, null); } catch (WxErrorException e) { log.error("【微信网页授权】异常,{}", e); throw ExceptionUtils.mpe(e.getError().getErrorMsg()); } // 从access_token信息中获取到用户的openid String openId = wxMpOAuth2AccessToken.getOpenId(); log.info("【微信网页授权】获取openId,openId = {}", openId); WechatUser wechatUser = new WechatUser(wxMpUser, wxMpOAuth2AccessToken.getAccessToken()); return Rb.ok(wechatUser); } /** * 静默授权获取用户信息,判断accessToken是否失效,失效即刷新accecssToken * @param openid * @param token * @return */ @GetMapping("/token/user/info") public Rb<WechatUser> getUserInfo(@RequestParam String openid, @RequestParam String token) { WxMpOAuth2AccessToken wxMpOAuth2AccessToken = new WxMpOAuth2AccessToken(); wxMpOAuth2AccessToken.setOpenId(openid); wxMpOAuth2AccessToken.setAccessToken(token); boolean ret = wxMpService.oauth2validateAccessToken(wxMpOAuth2AccessToken); if (!ret) { // 已经失效 try { // 刷新accessToken wxMpOAuth2AccessToken = wxMpService.oauth2refreshAccessToken(wxMpOAuth2AccessToken.getRefreshToken()); } catch (WxErrorException e) { log.error("【微信网页授权】刷新token失败,{}", e.getError().getErrorMsg()); throw ExceptionUtils.mpe(e.getError().getErrorMsg()); } } // 获取用户信息 try { WxMpUser wxMpUser = wxMpService.oauth2getUserInfo(wxMpOAuth2AccessToken, null); WechatUser wechatUser = new WechatUser(wxMpUser, wxMpOAuth2AccessToken.getAccessToken()); return Rb.ok(wechatUser); } catch (WxErrorException e) { log.error("【微信网页授权】获取用户信息失败,{}", e.getError().getErrorMsg()); throw ExceptionUtils.mpe(e.getError().getErrorMsg()); } }}
四、前端接入
4.1 路由拦截
noAuth配置是否需要授权页面
router.beforeEach((to, from, next) => { // 微信公众号授权 if (!to.meta.noAuth) { // 路由需要授权 if (_.isEmpty(store.getters.wechatUserInfo)) { // 获取用户信息 if ( !_.isEmpty(store.getters.openid) && !_.isEmpty(store.getters.accessToken) ) { // 存在openid和accessToken,已经授过权 // 判断accessToken是否过期,过期刷新token,获取用户信息 store.dispatch('getUserInfo') next() } else { // todo 跳转网页授权 // 记录当前页面url localStorage.setItem('currentUrl', to.fullPath) next({name: 'auth'}) } } else { // todo 已经存在用户信息,需要定期更新 next() } } else { // 路由不需要授权 next() }})
4.2 授权页面
{ path: '/auth', name: 'auth', component: resolve => { require(['@/views/auth/index.vue'], resolve) }, meta: { noAuth: true }},
<template></template><script>import config from '@/config'import WechatService from '@/api/wechat'export default { mounted() { WechatService.auth(config.WechatAuthCallbackUrl).then(res => { if (res.ok()) { // 获取授权页面后直接进行跳转 window.location.href = res.data } }) }}</script>
4.3 授权store
在vuex中进行授权和存储用户信息
import _ from 'lodash'import WechatService from '@/api/wechat'import localStorageUtil from '@/utils/LocalStorageUtil'export default { state: { unionId: '', openid: '', accessToken: '', wechatUserInfo: {} }, getters: { unionId: state => { return state.unionId || localStorageUtil.get('unionId') }, openid: state => { return state.openid || localStorageUtil.get('openid') }, accessToken: state => { return state.accessToken || localStorageUtil.get('accessToken') }, wechatUserInfo: state => { return state.wechatUserInfo || localStorageUtil.get('wechatUserInfo') } }, mutations: { saveWechatUserInfo: (state, res) => { state.wechatUserInfo = res // todo 保存到storage,设置一定日期,定期更新 state.unionId = res.unionId state.openid = res.openid state.accessToken = res.accessToken localStorageUtil.set('unionId', res.unionId) localStorageUtil.set('openid', res.openid) localStorageUtil.set('accessToken', res.accessToken) // 保存userInfo,设置有效时间,默认30天 localStorageUtil.set('wechatUserInfo', res, 30) } }, actions: { // 静默授权获取用户信息 async getUserInfo({ commit, getters }) { const openid = getters.openid const token = getters.accessToken if (!_.isEmpty(openid) && !_.isEmpty(token)) { // 存在openid和accessToken,已经授过权 // 判断accessToken是否过期,过期刷新token,获取用户信息 const res = await WechatService.getUserInfo(openid, token) if (res.ok()) { // todo 判断res.data是否有误 commit('saveWechatUserInfo', res.data) } } }, // 初次授权获取用户信息 async getAuthUserInfo({ commit }, { code, state }) { if (!_.isEmpty(code) && !_.isEmpty(state)) { const res = await WechatService.getAuthUserInfo(code, state) if (res.ok()) { commit('saveWechatUserInfo', res.data) } } } }}
4.4 自定义存储工具localStorageUtil.js
localStorageUtil.js:用于设置保存有效期
在这里,用户信息设置保存30天,根据前面4.1路由拦截判断,用户信息过期,需要重新进行授权认证。感觉这种方式不太好,但是获取用户信息每月限制5万次,不想每次都去调用接口获取用户信息,这里有更好的方案吗?
import _ from 'lodash'import moment from 'moment'export default { /** * 获取session-storage 中的值 * @param {*} key * @param {*} defaultValue */ get(key, defaultValue) { return this.parse(key, defaultValue) }, /** * 放入 session-storage 中,自动字符串化 obj * @param {*} key * @param {*} obj * @param {Integer} expires 过期时间:天 */ set(key, obj, expires) { if (expires) { const tmpTime = moment() .add(expires, 'days') .format('YYYY-MM-DD') const handleObj = { expires: tmpTime, value: obj } localStorage.setItem(key, JSON.stringify(handleObj)) } else { if (_.isObject(obj)) { localStorage.setItem(key, JSON.stringify(obj)) } else { localStorage.setItem(key, obj) } } }, /** * 从 session-storage 中移除key * @param {*} key */ remove(key) { localStorage.removeItem(key) }, /** * 从 session-storage 取出key并将值对象化 * @param {*} key * @param {*} defaultValue */ parse(key, defaultValue) { let value = localStorage.getItem(key) if (_.isObject(value)) { const valueObj = JSON.parse(value) if (valueObj.expires) { // 有过期时间,判断是否过期:在现在时间之前,过期 if (moment(valueObj.expires).isBefore(moment(), 'day')) { // 删除 this.remove(key) // 直接返回 return null } return valueObj.value } // 没有过期时间直接返回对象 return valueObj } // 不是对象,返回值 return value || defaultValue }}
至此大功告成,在微信开发者工具中即可获取用户信息,亲测有效。