Spring Boot+Vue front and rear WeChat official account separation solution

1, Introduction

The most complete front and back end separation wechat web page authorization solution in the whole network. If there is a better optimization plan, welcome to communicate more. At the end of the article, there is the author's contact information, welcome to nag.

2, To authorize a web page

  • Step 1: users agree to authorize and obtain code
  • 2 step 2: exchange code for authorized access_token
  • 3 step 3: refresh access_token (if required)
  • Step 4: pull the user information (scope should be snsapi_userinfo)
  • 5 attachment: Access Certificate_ Whether token) is valid

Details reference Official documents

Note: access here_ Token belongs to webpage authorized access_token, not access authorized_ token, Official explanation As follows:

About web access authorization_ Token and common access_ The difference of token
1, WeChat web page authorization is realized through OAuth2.0 mechanism. After the user authorizes the official account, the official account can get a unique interface calling Certificate (access_). Token), access is authorized through the web page_ Token can be used for interface call after authorization, such as obtaining basic user information;
2. For other wechat interfaces, access needs to be obtained through "get access" in basic support_ Common access obtained by token interface_ Token call.

But not very clearly. In fact, the difference between the two is:

  • First, web access authorization_ Token users can get user information if they can, and they can not pay attention to the official account, but the ordinary access_. Token did not pay attention to the official account and was empty.
  • Second, the daily limit call times of the two are different, ordinary access_token 2000 times a day, access to authorized webpage_ The number of token is unlimited, and the user information is obtained 50000 times a day.

3, Back end access

Open source tools for back end weixin-java-tools

three point one pom.xml Introducing jar package

<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-mp</artifactId>
    <version>3.8.0</version>
</dependency>

three point two application.yml Add configuration

Here you can apply for your own appid and appsecret Test account

# WeChat official account
wechat:
  mpAppId: appid
  mpAppSecret: appsecret

3.3 new read profile WechatMpProperties.java

package com.hsc.power.dm.wechat.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * WeChat official account configuration file
 *
 * @author liupan
 * @date 2020-05-26
 */
@Data
@Component
@ConfigurationProperties(prefix = "wechat")
public class WechatMpProperties {
    private String mpAppId;
    private String mpAppSecret;
}

3.4 create a new custom wechat configuration 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;

/**
 * WeChat official account configuration
 *
 * @author liupan
 * @date 2020-05-26
 */
@Component
public class WechatMpConfig {

    @Autowired
    private WechatMpProperties wechatMpProperties;

    /**
     * Information required to configure WxMpService
     *
     * @return
     */
    @Bean  // This annotation specifies that when the Spring container is started, the method will be executed and the objects returned by the method will be managed by the Spring container
    public WxMpService wxMpService() {
        WxMpService wxMpService = new WxMpServiceImpl();
        // Set the storage location of configuration information
        wxMpService.setWxMpConfigStorage(wxMpConfigStorage());
        return wxMpService;
    }

    /**
     * Configure appID and appsecret
     *
     * @return
     */
    @Bean
    public WxMpConfigStorage wxMpConfigStorage() {
        // Using this implementation class means storing configuration information in memory
        WxMpDefaultConfigImpl wxMpDefaultConfig = new WxMpDefaultConfigImpl();
        wxMpDefaultConfig.setAppId(wechatMpProperties.getMpAppId());
        wxMpDefaultConfig.setSecret(wechatMpProperties.getMpAppSecret());
        return wxMpDefaultConfig;
    }
}

3.5 new wechat user 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 authorization interface WechatController.java

    1. /auth: get authorized jump address
    1. /auth/user/info: initial authorization to obtain user information
    1. /token/user/info: silent authorization to obtain user information
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;

/**
 * WeChat official account interface
 *
 * @author liupan
 * @date 2020-05-26
 */
@Slf4j
@RestController
@RequestMapping("/wechat")
public class WechatController {
    @Autowired
    private WxMpService wxMpService;

    /**
     * Get code parameters
     *
     * @param returnUrl url to jump
     * @return
     */
    @GetMapping("/auth")
    public Rb<String> authorize(@RequestParam String authCallbackUrl, @RequestParam String returnUrl) {
        // Hard code our callback address here for debugging
        // Get the redirect url returned by wechat
        String redirectUrl = wxMpService.oauth2buildAuthorizationUrl(authCallbackUrl, WxConsts.OAuth2Scope.SNSAPI_USERINFO, URLEncoder.encode(returnUrl));
        log.info("[Wechat website authorization] access code,redirectUrl = {}", redirectUrl);
        return Rb.ok(redirectUrl);
    }

    /**
     * Initial authorization to obtain user information
     *
     * @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 {
            // Exchange code for access_token information
            wxMpOAuth2AccessToken = wxMpService.oauth2getAccessToken(code);
            wxMpUser = wxMpService.oauth2getUserInfo(wxMpOAuth2AccessToken, null);
        } catch (WxErrorException e) {
            log.error("[Wechat webpage authorization] exception,{}", e);
            throw ExceptionUtils.mpe(e.getError().getErrorMsg());
        }
        // From access_ Get the user's openid from the token information
        String openId = wxMpOAuth2AccessToken.getOpenId();
        log.info("[Wechat website authorization] access openId,openId = {}", openId);

        WechatUser wechatUser = new WechatUser(wxMpUser, wxMpOAuth2AccessToken.getAccessToken());
        return Rb.ok(wechatUser);
    }

    /**
     * Silently authorize to obtain user information, judge whether the accessToken is invalid, and refresh the accessToken if it is invalid
     * @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) {
            // Invalid
            try {
                // Refresh accessToken
                wxMpOAuth2AccessToken = wxMpService.oauth2refreshAccessToken(wxMpOAuth2AccessToken.getRefreshToken());
            } catch (WxErrorException e) {
                log.error("[Wechat page authorization] refresh token Failed,{}", e.getError().getErrorMsg());
                throw ExceptionUtils.mpe(e.getError().getErrorMsg());
            }
        }
        // Get user information
        try {
            WxMpUser wxMpUser = wxMpService.oauth2getUserInfo(wxMpOAuth2AccessToken, null);
            WechatUser wechatUser = new WechatUser(wxMpUser, wxMpOAuth2AccessToken.getAccessToken());
            return Rb.ok(wechatUser);
        } catch (WxErrorException e) {
            log.error("[Wechat web page authorization] failed to get user information,{}", e.getError().getErrorMsg());
            throw ExceptionUtils.mpe(e.getError().getErrorMsg());
        }
    }
}

4, Front end access

4.1 route interception

Whether authorization page is required for noAuth configuration

router.beforeEach((to, from, next) => {
  // WeChat official account authorization
  if (!to.meta.noAuth) {
    // Route requires authorization
    if (_.isEmpty(store.getters.wechatUserInfo)) {
      // Get user information
      if (
        !_.isEmpty(store.getters.openid) &&
        !_.isEmpty(store.getters.accessToken)
      ) {
        // openid and accessToken exist, authorized
        // Determine whether the accessToken expires, refresh the token after expiration, and obtain the user information
        store.dispatch('getUserInfo')
        next()
      } else {
        // todo jump page authorization
        // Record the current page url
        localStorage.setItem('currentUrl', to.fullPath)
        next({name: 'auth'})
      }
    } else {
      // todo already has user information and needs to be updated regularly
      next()
    }
  } else {
    // Route does not require authorization
    next()
  }
})

4.2 authorization page

{
  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()) {
        // Jump directly after obtaining the authorization page
        window.location.href = res.data
      }
    })
  }
}
</script>

4.3 authorized store

Authorization and storage of user information in 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 is saved to storage, set a certain date and update regularly
      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)
      // Save userInfo, set valid time, default 30 days
      localStorageUtil.set('wechatUserInfo', res, 30)
    }
  },
  actions: {
    // Silent authorization to obtain user information
    async getUserInfo({ commit, getters }) {
      const openid = getters.openid
      const token = getters.accessToken
      if (!_.isEmpty(openid) && !_.isEmpty(token)) {
        // openid and accessToken exist, authorized
        // Determine whether the accessToken expires, refresh the token after expiration, and obtain the user information
        const res = await WechatService.getUserInfo(openid, token)
        if (res.ok()) {
          // todo judgment res.data Is there any mistake
          commit('saveWechatUserInfo', res.data)
        }
      }
    },
    // Initial authorization to obtain user information
    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 custom storage tools localStorageUtil.js

localStorageUtil.js : used to set save validity

Here, the user information settings are saved for 30 days. According to the previous 4.1 route interception judgment, the user information expires and needs to be authorized again. I don't think it's a good way, but the access to user information is limited to 50000 times a month. I don't want to call the interface every time to get user information. Is there a better solution here?

import _ from 'lodash'
import moment from 'moment'
export default {
  /**
   * Get the value in session storage
   * @param {*} key
   * @param {*} defaultValue
   */
  get(key, defaultValue) {
    return this.parse(key, defaultValue)
  },
  /**
   * Put it into session storage and automatically string obj
   * @param {*} key
   * @param {*} obj
   * @param {Integer} expires Expiration time: days
   */
  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)
      }
    }
  },
  /**
   * Remove key from session storage
   * @param {*} key
   */
  remove(key) {
    localStorage.removeItem(key)
  },

  /**
   * Take the key from session storage and objectify the value
   * @param {*} key
   * @param {*} defaultValue
   */
  parse(key, defaultValue) {
    let value = localStorage.getItem(key)
    if (_.isObject(value)) {
      const valueObj = JSON.parse(value)
      if (valueObj.expires) {
        // If there is an expiration time, judge whether it expires: before the current time, it expires
        if (moment(valueObj.expires).isBefore(moment(), 'day')) {
          // delete
          this.remove(key)
          // Direct return
          return null
        }
        return valueObj.value
      }
      // Return objects directly without expiration time
      return valueObj
    }
    // Not object, return value
    return value || defaultValue
  }
}

So far, we have achieved success. We can get user information in wechat developer tools, which is effective through personal testing.

Open source is not easy to use and cherish!



Sponsor authors, communicate with each other

Please indicate: My technology sharing » Spring Boot+Vue front and rear WeChat official account separation solution

Tags: Java Session Lombok Spring

Posted on Fri, 05 Jun 2020 23:16:18 -0400 by john-iom