一、引言
全网最全的前后端分离微信 网页授权 解决方案。如果有更好的优化方案,欢迎多多交流,文末有作者联系方式,欢迎叨扰。
二、网页授权的步骤
- 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
*/
@Component
public 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;
@Data
public 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
}
}
至此大功告成,在微信开发者工具中即可获取用户信息,亲测有效。
开源不易,且用且珍惜!
赞助作者,互相交流
转载请注明:我的技术分享 » Spring Boot+Vue 前后端分离微信公众号网页授权解决方案