文章目录
【微信开发】SpringBoot 集成微信小程序授权登录
我这里采用了第三方的依赖,目前是最火的微信开发工具吧,WxJava
1、SprinBoot 后端
(1)准备工作
引入相关依赖
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.1.0</version>
</dependency>
配置application.yml
# ----------------------系统配置
# 业务配置
pay-platform:
# 微信
wx:
pay:
appId:
secret:
mchId:
mchKey:
keyPath:
notifyUrl:
(2)相关配置类
属性类
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* wxpay pay properties.
*
* @author Binary Wang
*/
@Data
@ConfigurationProperties(prefix = "pay-platform.wx.pay")
public class WxPayProperties {
/**
* 设置微信公众号或者小程序等的appid
*/
private String appId;
private String secret;
/**
* 微信支付商户号
*/
private String mchId;
/**
* 微信支付商户密钥
*/
private String mchKey;
/**
* 服务商模式下的子商户公众账号ID,普通模式请不要配置,请在配置文件中将对应项删除
*/
private String subAppId;
/**
* 服务商模式下的子商户号,普通模式请不要配置,最好是请在配置文件中将对应项删除
*/
private String subMchId;
/**
* apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定
*/
private String keyPath;
/**
* 支付回调地址
*/
private String notifyUrl;
}
属性配置类
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Binary Wang
*/
@Configuration
@ConditionalOnClass(WxPayService.class)
@EnableConfigurationProperties(WxPayProperties.class)
@AllArgsConstructor
public class WxPayConfiguration {
private WxPayProperties properties;
@Bean
@ConditionalOnMissingBean
public WxPayService wxService() {
WxPayConfig payConfig = new WxPayConfig();
payConfig.setAppId(StringUtils.trimToNull(this.properties.getAppId()));
payConfig.setMchId(StringUtils.trimToNull(this.properties.getMchId()));
payConfig.setMchKey(StringUtils.trimToNull(this.properties.getMchKey()));
payConfig.setSubAppId(StringUtils.trimToNull(this.properties.getSubAppId()));
payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId()));
payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath()));
// 可以指定是否使用沙箱环境
payConfig.setUseSandboxEnv(false);
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(payConfig);
return wxPayService;
}
}
(3)相关实体类
相关实体类,都可以使用json的map格式来处理,我这里是个人习惯
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 接口调用凭证
* https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html
*
* @author Tellsea
* @date 2021/05/20
*/
@Data
@Accessors(chain = true)
public class AccessToken {
private String accessToken;
/**
* 凭证有效时间,单位:秒。目前是7200秒之内的值。
*/
private Integer expiresIn;
private Integer errCode;
private String errMsg;
}
import lombok.Data;
import lombok.experimental.Accessors;
/**
* code换取openId
* https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html
*
* @author Tellsea
* @date 2021/05/20
*/
@Data
@Accessors(chain = true)
public class Code2Session {
private String openId;
private String sessionKey;
private String unionId;
private Integer errCode;
private String errMsg;
}
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 微信登录
*
* @author Tellsea
* @date 2021/05/19
*/
@Data
@Accessors(chain = true)
public class WeiXinLogin {
private String code;
private String encryptedData;
private String iv;
private String nickName;
private String avatarUrl;
private Integer gender;
}
lombok.Data;
import lombok.experimental.Accessors;
/**
* 微信 token
*
* @author Tellsea
* @date 2021/05/20
*/
@Data
@Accessors(chain = true)
public class WeiXinToken {
public static String token;
}
(4)处理后端逻辑
控制层
package com.ruoyi.business.appuser.controller;
import cn.hutool.core.lang.Validator;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.business.appuser.service.WeiXinService;
import com.ruoyi.business.appuser.vo.wx.Code2Session;
import com.ruoyi.business.appuser.vo.wx.OrderInfo;
import com.ruoyi.business.appuser.vo.wx.WeiXinLogin;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.ISysUserService;
import com.zhhy.tool.utils.IntegerUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.spec.AlgorithmParameterSpec;
import java.util.List;
import java.util.Set;
/**
* @author Tellsea
* @date 2021/11/09
*/
@Slf4j
@Api(value = "微信API", tags = {
"微信API"})
@RestController
@RequestMapping("/au/weiXin")
public class AuWeiXinController {
@Autowired
private ISysUserService userService;
@Autowired
private SysLoginService loginService;
@Autowired
private WeiXinService weiXinService;
@Autowired
private SysPermissionService permissionService;
@Autowired
private TokenService tokenService;
@ApiOperation("微信用户登录")
@PostMapping("login")
public AjaxResult login(@RequestBody WeiXinLogin dto) {
Code2Session code2Session = weiXinService.code2Session(dto.getCode());
if (StringUtils.isNotEmpty(code2Session.getOpenId())) {
// 解析电话号码
String phoneNumber;
byte[] byEncrypdata = Base64.decodeBase64(dto.getEncryptedData());
byte[] byIvdata = Base64.decodeBase64(dto.getIv());
byte[] bySessionkey = Base64.decodeBase64(code2Session.getSessionKey());
AlgorithmParameterSpec ivSpec = new IvParameterSpec(byIvdata);
try {
SecretKeySpec keySpec = new SecretKeySpec(bySessionkey, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
String phoneResult = new String(cipher.doFinal(byEncrypdata), StandardCharsets.UTF_8);
JSONObject phoneObject = JSONObject.parseObject(phoneResult);
phoneNumber = phoneObject.getString("phoneNumber");
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.error("手机号码解密失败");
}
// 根据openId查询是否存在这个用户
List<SysUser> list = userService.list(new LambdaQueryWrapper<SysUser>().eq(SysUser::getOpenId, code2Session.getOpenId())
.or().eq(SysUser::getUserName, phoneNumber).or().eq(SysUser::getPhonenumber, phoneNumber));
AjaxResult ajax = AjaxResult.success();
if (CollectionUtils.isEmpty(list)) {
// 添加新用户
String defaultPassword = "111111";
SysUser user = new SysUser()
.setOpenId(code2Session.getOpenId())
.setUserName(phoneNumber)
.setNickName(dto.getNickName())
.setDeptId(0L)
.setPassword(defaultPassword)
.setPhonenumber(phoneNumber)
.setAvatar(dto.getAvatarUrl());
if (IntegerUtils.eq(dto.getGender(), 0)) {
user.setSex("2");
} else if (IntegerUtils.eq(dto.getGender(), 1)) {
user.setSex("0");
} else if (IntegerUtils.eq(dto.getGender(), 2)) {
user.setSex("1");
}
if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(user.getUserName()))) {
return AjaxResult.error("手机号已被注册");
} else if (Validator.isNotEmpty(user.getPhonenumber())
&& UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) {
return AjaxResult.error("手机号已被使用");
}
user.setCreateBy(SecurityUtils.getUsername());
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
// 默认给角色用户
user.setRoleIds(new Long[]{
1L});
userService.insertUser(user);
String token = loginService.login(user.getUserName(), defaultPassword);
ajax.put(Constants.TOKEN, token);
return ajax;
} else if (list.size() == 1) {
// 更新用户信息:这里查询出的一个信息,可能是openId、userName、phonenumber三个字段其中某个查出来的
SysUser sysUser = list.get(0);
sysUser.setNickName(dto.getNickName());
sysUser.setAvatar(dto.getAvatarUrl());
if (IntegerUtils.eq(dto.getGender(), 0)) {
sysUser.setSex("2");
} else if (IntegerUtils.eq(dto.getGender(), 1)) {
sysUser.setSex("0");
} else if (IntegerUtils.eq(dto.getGender(), 2)) {
sysUser.setSex("1");
}
if (StringUtils.isEmpty(sysUser.getOpenId())) {
sysUser.setOpenId(code2Session.getOpenId());
}
userService.updateById(sysUser);
SysUser user = userService.selectUserByUserName(sysUser.getUserName());
LoginUser loginUser = new LoginUser(user, permissionService.getMenuPermission(user));
String token = tokenService.createToken(loginUser);
ajax.put(Constants.TOKEN, token);
return ajax;
} else {
return AjaxResult.error("用户信息异常,存在多个openId或电话号码");
}
} else {
return AjaxResult.error(code2Session.getErrMsg());
}
}
@ApiOperation("获取用户信息")
@GetMapping("getInfo")
public AjaxResult getInfo() {
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
SysUser user = loginUser.getUser();
AjaxResult ajax = AjaxResult.success();
Set<String> permission = permissionService.getRolePermission(user);
ajax.put("user", user);
ajax.put("roles", permission);
ajax.put("permissions", permissionService.getMenuPermission(user));
return ajax;
}
}
业务处理层
import com.ruoyi.business.appuser.vo.wx.AccessToken;
import com.ruoyi.business.appuser.vo.wx.Code2Session;
import com.ruoyi.business.appuser.vo.wx.OrderInfo;
import com.ruoyi.common.core.domain.AjaxResult;
/**
* @author Tellsea
* @date 2021/05/20
*/
public interface WeiXinService {
/**
* code换取openId
* https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html
*
* @param code
* @return
*/
Code2Session code2Session(String code);
/**
* 接口调用凭据
* https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html
*
* @return
*/
AccessToken getAccessToken();
}
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.github.binarywang.wxpay.bean.notify.WxPayNotifyResponse;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.github.binarywang.wxpay.bean.request.BaseWxPayRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.result.BaseWxPayResult;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.ruoyi.business.appuser.config.WxPayProperties;
import com.ruoyi.business.appuser.service.WeiXinService;
import com.ruoyi.business.appuser.vo.wx.AccessToken;
import com.ruoyi.business.appuser.vo.wx.Code2Session;
import com.ruoyi.business.appuser.vo.wx.OrderInfo;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.ServletUtils;
import com.zhhy.tool.utils.IntegerUtils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* @author Tellsea
* @date 2021/05/20
*/
@Slf4j
@Service
@AllArgsConstructor
public class WeiXinServiceImpl implements WeiXinService {
private WxPayService wxPayService;
private WxPayProperties wxPayProperties;
@Override
public Code2Session code2Session(String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session?" +
"appid=" + wxPayProperties.getAppId() +
"&secret=" + wxPayProperties.getSecret() +
"&js_code=" + code +
"&grant_type=authorization_code";
String result = HttpUtil.get(url);
JSONObject jsonObject = JSONObject.parseObject(result);
Code2Session code2Session = new Code2Session().setOpenId(jsonObject.getString("openid"))
.setSessionKey(jsonObject.getString("session_key"))
.setUnionId(jsonObject.getString("unionid"))
.setErrCode(jsonObject.getInteger("errcode"))
.setErrMsg(jsonObject.getString("errmsg"));
if (StringUtils.isEmpty(code2Session.getOpenId())) {
code2Session.setErrMsg("OpenId为空");
} else if (IntegerUtils.eq(code2Session.getErrCode(), -1)) {
code2Session.setErrMsg("系统繁忙,此时请开发者稍候再试");
} else if (IntegerUtils.eq(code2Session.getErrCode(), 40029)) {
code2Session.setErrMsg("code 无效");
} else if (IntegerUtils.eq(code2Session.getErrCode(), 45011)) {
code2Session.setErrMsg("频率限制,每个用户每分钟100次");
} else {
code2Session.setErrMsg("其他错误");
}
return code2Session;
}
@Override
public AccessToken getAccessToken() {
String url = "https://api.weixin.qq.com/cgi-bin/token?" +
"grant_type=client_credential" +
"&appid=" + wxPayProperties.getAppId() +
"&secret=" + wxPayProperties.getSecret();
String result = HttpUtil.get(url);
JSONObject jsonObject = JSONObject.parseObject(result);
AccessToken accessToken = new AccessToken().setAccessToken(jsonObject.getString("access_token"))
.setExpiresIn(jsonObject.getInteger("expires_in"))
.setErrCode(jsonObject.getInteger("errcode"))
.setErrMsg(jsonObject.getString("errmsg"));
if (StringUtils.isEmpty(accessToken.getAccessToken())) {
} else if (IntegerUtils.eq(accessToken.getErrCode(), -1)) {
accessToken.setErrMsg("系统繁忙,此时请开发者稍候再试");
} else if (IntegerUtils.eq(accessToken.getErrCode(), 40001)) {
accessToken.setErrMsg("AppSecret 错误或者 AppSecret 不属于这个小程序,请开发者确认 AppSecret 的正确性");
} else if (IntegerUtils.eq(accessToken.getErrCode(), 40002)) {
accessToken.setErrMsg("请确保 grant_type 字段值为 client_credential");
} else if (IntegerUtils.eq(accessToken.getErrCode(), 40013)) {
accessToken.setErrMsg("不合法的 AppID,请开发者检查 AppID 的正确性,避免异常字符,注意大小写");
} else {
accessToken.setErrMsg("其他错误");
}
return accessToken;
}
}
2、Uniapp 前端
(1)授权登录
一个页面直接搞定,授权登录,换取token,通过token查询用户信息。请求使用的是uview的官网工具类
<template>
<view>
<view class="header">
<image src="/static/login/wx_login.png" mode=""></image>
</view>
<view class='content'>
<view>申请获取以下权限</view>
<text>获得你的公开信息(昵称,头像、地区等)</text>
<text>获得你微信绑定的手机号</text>
</view>
<button class="bottom" type="primary" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
微授权登录
</button>
<u-toast ref="uToast" />
</view>
</template>
<script>
let that;
export default {
data() {
return {
form: {
// 换取openId
code: '',
// 解密手机号
encryptedData: '',
iv: '',
// 更新用户信息
nickName: '',
avatarUrl: '',
// 性别 0:未知、1:男、2:女
gender: '',
}
};
},
onLoad() {
that = this;
this.getCode();
},
methods: {
getCode() {
wx.login({
success(res) {
if (res.code) {
that.form.code = res.code;
} else {
that.$msg('登录失败:' + res.errMsg);
}
},
fail(err) {
that.$msg('code获取失败');
},
});
},
getPhoneNumber: function(e) {
that.getCode();
uni.showLoading({
title: '登录中...'
});
if (e.detail.errMsg != 'getPhoneNumber:ok') {
that.$refs.uToast.show({
type: 'error', title: '未授权手机号'});
uni.hideLoading();
return false;
}
that.form.encryptedData = e.detail.encryptedData;
that.form.iv = e.detail.iv;
// 检查登录态是否过期
wx.checkSession({
success() {
// 用户信息
wx.getUserInfo({
success: function(res) {
let userInfo = res.userInfo;
that.form.nickName = userInfo.nickName;
that.form.avatarUrl = userInfo.avatarUrl;
that.form.gender = userInfo.gender;
that.$u.post('/au/weiXin/login', that.form).then(res => {
uni.setStorageSync(that.$config.cachePrefix + 'token', res.token);
that.$u.get('/au/weiXin/getInfo').then(result => {
uni.setStorageSync(that.$config.cachePrefix + 'user', result.user);
that.$refs.uToast.show({
type: 'success',
title: '登录成功',
url: '/pages/index/index',
isTab: true
});
});
});
}
});
},
fail(err) {
wx.login({
success: res => {
that.form.code = res.code
}
});
}
})
},
}
}
</script>
<style scoped>
.header {
margin: 90rpx 0 90rpx 50rpx;
border-bottom: 1px solid #ccc;
text-align: center;
width: 650rpx;
height: 300rpx;
line-height: 450rpx;
}
.header image {
width: 200rpx;
height: 200rpx;
}
.content {
margin-left: 50rpx;
margin-bottom: 90rpx;
}
.content text {
display: block;
color: #9d9d9d;
margin-top: 40rpx;
}
.bottom {
border-radius: 80rpx;
margin: 70rpx 50rpx;
font-size: 35rpx;
}
</style>