一、引言
全网最全的前后端分离微信网页授权解决方案。如果有更好的优化方案,欢迎多多交流
二、网页授权的步骤
- 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
}
}
至此大功告成,在微信开发者工具中即可获取用户信息,亲测有效。