微信小程序登录

This commit is contained in:
zhangchenbiao
2023-05-29 15:28:30 +08:00
parent 112d874931
commit ba01f749c6
17 changed files with 721 additions and 1 deletions

View File

@@ -22,11 +22,17 @@ public class CommonConstants {
public static final int REFRESH_TOKEN_EXPIRE = 60*60*24*30;
public static final int THREE_DAY_SECONDS = 60*60*24*3;
/**
* 系统用户id
*/
public static final String SYSTEM_USER_ID = "system";
public static final String COMMA = ",";
public static final String MOSAICS = "#";
public static final String WX_APP_SECRET_KEY = "wx_app_secret_key:{0}";
public static final String MINI_PROGRAM_SESSION_KEY = "mini_program_session_key:{0}:{1}";
public static final int ZERO = 0;

View File

@@ -35,6 +35,10 @@ public enum ErrorCodeEnum {
ENTERPRISE_NOT_EXIST(1021020,"企业不存在",null),
USER_NOT_EXIST(1021021,"用户不存在",null),
USER_WAIT_AUDIT(1021018,"账号审核中,请联系企业管理员",null),
OPERATION_OVER_TIME(1021019, "您的操作过于频繁,休息一下~", null),
GET_APP_SECRET_ERROR(1021019, "获取secret异常", null),
WX_SERVICE_ERROR(1021020, "调用微信服务异常", null),
SESSION_KEY_ERROR(1021021, "sessionKey过期", null),
;

View File

@@ -0,0 +1,77 @@
package com.cool.store.utils;
import com.cool.store.exception.ServiceException;
import org.springframework.util.Assert;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.AlgorithmParameters;
import java.util.Arrays;
import java.util.Base64;
/**
* @author zhangchenbiao
* @FileName: AesUtil
* @Description:
* @date 2023-05-29 14:28
*/
public class AesUtil {
private static final Charset utf8 = StandardCharsets.UTF_8;
public static String genAesKey() {
return StringUtil.random(32);
}
public static String encrypt(String content, String aesTextKey) {
return Base64.getEncoder().encodeToString(encrypt(content.getBytes(utf8), aesTextKey.getBytes(utf8)));
}
public static String decrypt(String content, String aesTextKey) {
byte[] buffer = Base64.getDecoder().decode(content);
return new String(decrypt(buffer, aesTextKey.getBytes(utf8)), utf8);
}
public static byte[] encrypt(byte[] content, byte[] aesKey) {
Assert.isTrue(aesKey.length == 32, "IllegalAesKey, aesKey's length must be 32");
try {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
return cipher.doFinal(Pkcs7Encoder.encode(content));
} catch (Exception e) {
throw new ServiceException(e.getMessage());
}
}
public static byte[] decrypt(byte[] encrypted, byte[] aesKey) {
Assert.isTrue(aesKey.length == 32, "IllegalAesKey, aesKey's length must be 32");
try {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
return Pkcs7Encoder.decode(cipher.doFinal(encrypted));
} catch (Exception e) {
throw new ServiceException(e.getMessage());
}
}
public static String decryptWechat(String sessionKey, String encryptedData, String ivStr) {
try {
AlgorithmParameters params = AlgorithmParameters.getInstance("AES");
params.init(new IvParameterSpec(Base64.getDecoder().decode(ivStr)));
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(2, new SecretKeySpec(Base64.getDecoder().decode(sessionKey), "AES"), params);
return new String(Pkcs7Encoder.decode(cipher.doFinal(Base64.getDecoder().decode(encryptedData))), StandardCharsets.UTF_8);
} catch (Exception var5) {
throw new ServiceException("AES解密失败");
}
}
}

View File

@@ -0,0 +1,71 @@
package com.cool.store.utils;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
* @author zhangchenbiao
* @FileName: Pkcs7Encoder
* @Description:
* @date 2023-05-29 14:29
*/
public class Pkcs7Encoder {
private static int BLOCK_SIZE = 32;
private static final Charset CHARSET = StandardCharsets.UTF_8;
/**
* 获得对明文进行补位填充的字节.
*
* @param count 需要进行填充补位操作的明文字节个数
* @return 补齐用的字节数组
*/
public static byte[] encode(int count) {
// 计算需要填充的位数
int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
// 获得补位所用的字符
char padChr = chr(amountToPad);
StringBuilder tmp = new StringBuilder();
for (int index = 0; index < amountToPad; index++) {
tmp.append(padChr);
}
return tmp.toString().getBytes(CHARSET);
}
public static byte[] encode(byte[] src) {
int count = src.length;
// 计算需要填充的位数
int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
if (amountToPad == 0) {
amountToPad = BLOCK_SIZE;
}
// 获得补位所用的字符
byte pad = (byte) (amountToPad & 0xFF);
byte[] pads = new byte[amountToPad];
for (int index = 0; index < amountToPad; index++) {
pads[index] = pad;
}
int length = count + amountToPad;
byte[] dest = new byte[length];
System.arraycopy(src, 0, dest, 0, count);
System.arraycopy(pads, 0, dest, count, amountToPad);
return dest;
}
public static byte[] decode(byte[] decrypted) {
int pad = decrypted[decrypted.length - 1];
if (pad < 1 || pad > BLOCK_SIZE) {
pad = 0;
}
if (pad > 0) {
return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
}
return decrypted;
}
private static char chr(int a) {
byte target = (byte) (a & 0xFF);
return (char) target;
}
}

View File

@@ -1372,7 +1372,16 @@ public class RedisUtilPool {
return setnx.equals(1L) ;
}
}.getResult();
}
public Boolean lock(String key){
return new Executor<Boolean>(shardedJedisPool) {
@Override
Boolean execute() {
Long setnx = jedis.setnx(key, System.currentTimeMillis() + "");
return setnx.equals(1L) ;
}
}.getResult();
}
/**

View File

@@ -0,0 +1,223 @@
package com.cool.store.utils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.HtmlUtils;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Pattern;
/**
* @author zhangchenbiao
* @FileName: AesUtil
* @Description:字符串相关操作
* @date 2023-05-29 14:28
*/
public class StringUtil extends org.apache.commons.lang3.StringUtils {
private static final char UPPER_A = 'A';
private static final char LOWER_A = 'a';
private static final char UPPER_Z = 'Z';
private static final char LOWER_Z = 'z';
private static final byte[] DIGITS = {
'0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', 'a', 'b',
'c', 'd', 'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F',
'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P', 'Q', 'R',
'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z'
};
/**
* 特殊字符正则sql特殊字符和空白符
*/
private static final Pattern SPECIAL_CHARS_REGEX = Pattern.compile("[`'\"|/,;()-+*%#·•<C2B7> \\s]");
/**
* 随机字符串因子
*/
private static final String INT_STR = "0123456789";
private static final String STR_STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private static final String ALL_STR = INT_STR + STR_STR;
/**
* 首字母变小写
*
* @param str 字符串
* @return {String}
*/
public static String firstCharToLower(String str) {
char firstChar = str.charAt(0);
if (firstChar >= UPPER_A && firstChar <= UPPER_Z) {
char[] arr = str.toCharArray();
arr[0] += (LOWER_A - UPPER_A);
return new String(arr);
}
return str;
}
/**
* 首字母变大写
*
* @param str 字符串
* @return {String}
*/
public static String firstCharToUpper(String str) {
char firstChar = str.charAt(0);
if (firstChar >= LOWER_A && firstChar <= LOWER_Z) {
char[] arr = str.toCharArray();
arr[0] -= (LOWER_A - UPPER_A);
return new String(arr);
}
return str;
}
public static String appendParams(String url, Map<String, Object> params) {
if (StringUtil.isBlank(url)) {
return "";
} else if (CollectionUtils.isEmpty(params)) {
return url.trim();
} else {
StringBuilder sb = new StringBuilder(200);
params.forEach((k, v) -> sb.append(k).append("=").append(v).append("&"));
sb.deleteCharAt(sb.length() - 1);
url = url.trim();
int length = url.length();
int index = url.indexOf("?");
if (index > -1) {
if ((length - 1) == index) {
url += sb.toString();
} else {
url += "&" + sb.toString();
}
} else {
url += "?" + sb.toString();
}
return url;
}
}
/**
* 生成uuid采用 jdk 9 的形式,优化性能
*
* @return UUID
*/
public static String getUUID() {
ThreadLocalRandom random = ThreadLocalRandom.current();
long lsb = random.nextLong();
long msb = random.nextLong();
byte[] buf = new byte[32];
formatUnsignedLong(lsb, buf, 20, 12);
formatUnsignedLong(lsb >>> 48, buf, 16, 4);
formatUnsignedLong(msb, buf, 12, 4);
formatUnsignedLong(msb >>> 16, buf, 8, 4);
formatUnsignedLong(msb >>> 32, buf, 0, 8);
return new String(buf, StandardCharsets.UTF_8);
}
private static void formatUnsignedLong(long val, byte[] buf, int offset, int len) {
int charPos = offset + len;
int radix = 1 << 4;
int mask = radix - 1;
do {
buf[--charPos] = DIGITS[((int) val) & mask];
val >>>= 4;
} while (charPos > offset);
}
/**
* 转义HTML用于安全过滤
*
* @param html html
* @return {String}
*/
public static String escapeHtml(String html) {
return HtmlUtils.htmlEscape(html);
}
/**
* 清理字符串清理出某些不可见字符和一些sql特殊字符
*
* @param txt 文本
* @return {String}
*/
@Nullable
public static String cleanText(@Nullable String txt) {
if (txt == null) {
return null;
}
return SPECIAL_CHARS_REGEX.matcher(txt).replaceAll(StringUtil.EMPTY);
}
/**
* 获取标识符,用于参数清理
*
* @param param 参数
* @return 清理后的标识符
*/
@Nullable
public static String cleanIdentifier(@Nullable String param) {
if (param == null) {
return null;
}
StringBuilder paramBuilder = new StringBuilder();
for (int i = 0; i < param.length(); i++) {
char c = param.charAt(i);
if (Character.isJavaIdentifierPart(c)) {
paramBuilder.append(c);
}
}
return paramBuilder.toString();
}
/**
* 随机数生成
*
* @param count 字符长度
* @return 随机数
*/
public static String random(int count) {
return random(count, RandomType.ALL);
}
/**
* 随机数生成
*
* @param count 字符长度
* @param randomType 随机数类别
* @return 随机数
*/
public static String random(int count, RandomType randomType) {
if (count == 0) {
return "";
}
Assert.isTrue(count > 0, "Requested random string length " + count + " is less than 0.");
final ThreadLocalRandom random = ThreadLocalRandom.current();
char[] buffer = new char[count];
for (int i = 0; i < count; i++) {
if (RandomType.INT == randomType) {
buffer[i] = INT_STR.charAt(random.nextInt(INT_STR.length()));
} else if (RandomType.STRING == randomType) {
buffer[i] = STR_STR.charAt(random.nextInt(STR_STR.length()));
} else {
buffer[i] = ALL_STR.charAt(random.nextInt(ALL_STR.length()));
}
}
return new String(buffer);
}
public enum RandomType {
/**
* INT STRING ALL
*/
INT, STRING, ALL;
}
}

View File

@@ -0,0 +1,23 @@
package com.cool.store.dto.wx;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
/**
* @author zhangchenbiao
* @FileName: CodeSessionDTO
* @Description:
* @date 2023-05-29 14:28
*/
@Data
public class CodeSessionDTO extends WXBaseResultDTO{
@JSONField(name = "session_key")
private String sessionKey;
@JSONField(name = "openid")
private String openid;
@JSONField(name = "unionid")
private String unionId;
}

View File

@@ -0,0 +1,27 @@
package com.cool.store.dto.wx;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* @author zhangchenbiao
* @FileName: MiniProgramLoginDTO
* @Description:
* @date 2023-05-29 14:28
*/
@Data
public class MiniProgramLoginDTO {
@NotBlank(message = "appid不能为空")
private String appid;
@NotBlank(message = "jsCode不能为空")
private String jsCode;
@NotBlank(message = "用户encryptedData不能为空")
private String encryptedData;
@NotBlank(message = "ivStr不能为空")
private String ivStr;
}

View File

@@ -0,0 +1,27 @@
package com.cool.store.dto.wx;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* @author zhangchenbiao
* @FileName: MiniProgramMsgDTO
* @Description:
* @date 2023-05-29 14:28
*/
@Data
public class MiniProgramMsgDTO {
@NotBlank(message = "appid不能为空")
private String appid;
@NotBlank(message = "encryptedData不能为空")
private String encryptedData;
@NotBlank(message = "ivStr不能为空")
private String ivStr;
@NotBlank(message = "openid不能为空")
private String openid;
}

View File

@@ -0,0 +1,27 @@
package com.cool.store.dto.wx;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
/**
* @author zhangchenbiao
* @FileName: WXBaseResultDTO
* @Description:
* @date 2023-05-29 14:52
*/
@Data
public class WXBaseResultDTO {
private static final String SUCCESS_CODE = "0";
@JSONField(name = "errcode")
private String errCode;
@JSONField(name = "errmsg")
private String errMsg;
public boolean isSuccess() {
return this.errCode == null || this.errCode.isEmpty() || this.errCode.equals("0");
}
}

View File

@@ -0,0 +1,18 @@
package com.cool.store.vo.wx;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CodeSessionVO {
private String openid;
private String unionId;
}

View File

@@ -0,0 +1,18 @@
package com.cool.store.vo.wx;
import lombok.Data;
@Data
public class MiniProgramUserVO {
private String openId;
private String nickName;
private String gender;
private String language;
private String city;
private String province;
private String country;
private String avatarUrl;
private String unionId;
private String wxUnionId;
}

View File

@@ -5,7 +5,10 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* @author dong_gui on 2020/5/20.
* @author zhangchenbiao
* @FileName: RestTemplateConfig
* @Description:
* @date 2023-05-29 14:29
*/
@Configuration
public class RestTemplateConfig {

View File

@@ -0,0 +1,42 @@
package com.cool.store.http;
import com.alibaba.fastjson.JSONObject;
import com.cool.store.dto.enterprise.EnterpriseUserDTO;
import com.cool.store.dto.wx.CodeSessionDTO;
import com.cool.store.enums.ErrorCodeEnum;
import com.cool.store.exception.ServiceException;
import com.cool.store.utils.RestTemplateUtil;
import com.coolstore.base.dto.ResultDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* @author zhangchenbiao
* @FileName: WechatRest
* @Description:微信api
* @date 2023-05-29 14:49
*/
@Slf4j
@Service
public class WechatRest {
public CodeSessionDTO miniProgramJsCodeSession(String appId, String secret, String jsCode){
String url = "https://api.weixin.qq.com/sns/jscode2session";
ResponseEntity<CodeSessionDTO> responseEntity = null;
try {
responseEntity = RestTemplateUtil.loadGet(url, CodeSessionDTO.class);
log.info("url:{}, response:{}", url, JSONObject.toJSONString(responseEntity));
if(Objects.nonNull(responseEntity.getBody()) && responseEntity.getBody().isSuccess()){
return responseEntity.getBody();
}
} catch (Exception e) {
log.info("调用微信服务异常{}", e);
throw new ServiceException(ErrorCodeEnum.WX_SERVICE_ERROR);
}
return null;
}
}

View File

@@ -0,0 +1,21 @@
package com.cool.store.service;
import com.cool.store.dto.wx.MiniProgramLoginDTO;
import com.cool.store.dto.wx.MiniProgramMsgDTO;
import com.cool.store.vo.wx.CodeSessionVO;
import com.cool.store.vo.wx.MiniProgramUserVO;
/**
* @author zhangchenbiao
* @FileName: WechatMiniAppService
* @Description:
* @date 2023-05-29 14:28
*/
public interface WechatMiniAppService {
CodeSessionVO miniProgramLogin(MiniProgramLoginDTO param);
MiniProgramUserVO queryMiniProgramUser(MiniProgramMsgDTO param);
}

View File

@@ -0,0 +1,79 @@
package com.cool.store.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.openservices.shade.org.apache.commons.lang3.StringUtils;
import com.cool.store.constants.CommonConstants;
import com.cool.store.dto.wx.CodeSessionDTO;
import com.cool.store.dto.wx.MiniProgramLoginDTO;
import com.cool.store.dto.wx.MiniProgramMsgDTO;
import com.cool.store.enums.ErrorCodeEnum;
import com.cool.store.exception.ServiceException;
import com.cool.store.http.WechatRest;
import com.cool.store.service.WechatMiniAppService;
import com.cool.store.utils.AesUtil;
import com.cool.store.utils.RedisUtilPool;
import com.cool.store.vo.wx.CodeSessionVO;
import com.cool.store.vo.wx.MiniProgramUserVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.text.MessageFormat;
import java.util.Objects;
/**
* @author zhangchenbiao
* @FileName: WechatMiniAppServiceImpl
* @Description:
* @date 2023-05-29 14:29
*/
@Slf4j
@Service
public class WechatMiniAppServiceImpl implements WechatMiniAppService {
@Resource
private RedisUtilPool redisUtilPool;
@Resource
private WechatRest wechatRest;
@Override
public CodeSessionVO miniProgramLogin(MiniProgramLoginDTO param) {
String jsCode = param.getJsCode();
String lockKey = "codeSession:" + param.getAppid() + CommonConstants.MOSAICS + jsCode;
boolean lock = redisUtilPool.lock(lockKey);
if (!lock) {
throw new ServiceException(ErrorCodeEnum.OPERATION_OVER_TIME);
}
String appid = param.getAppid();
String secret = redisUtilPool.getString(MessageFormat.format(CommonConstants.WX_APP_SECRET_KEY, appid));
if(StringUtils.isBlank(secret)){
throw new ServiceException(ErrorCodeEnum.GET_APP_SECRET_ERROR);
}
CodeSessionDTO codeSession = wechatRest.miniProgramJsCodeSession(appid, secret, jsCode);
String openid = codeSession.getOpenid();
String sessionCacheKey = MessageFormat.format(CommonConstants.MINI_PROGRAM_SESSION_KEY, appid, openid);
redisUtilPool.setString(sessionCacheKey, codeSession.getSessionKey(), CommonConstants.THREE_DAY_SECONDS);
String unionId = codeSession.getUnionId();
log.info("小程序登录:{}", unionId);
//todo 保存授权信息 判断是否第一次授权
return CodeSessionVO.builder().openid(openid).unionId(unionId).build();
}
@Override
public MiniProgramUserVO queryMiniProgramUser(MiniProgramMsgDTO param) {
String sessionCacheKey = MessageFormat.format(CommonConstants.MINI_PROGRAM_SESSION_KEY, param.getAppid(), param.getOpenid());
String sessionKey = redisUtilPool.getString(sessionCacheKey);
if (StringUtils.isBlank(sessionKey)) {
throw new ServiceException(ErrorCodeEnum.SESSION_KEY_ERROR);
}
log.info("sessionKey {}", sessionKey);
String decryptUser = AesUtil.decryptWechat(sessionKey, param.getEncryptedData(), param.getIvStr());
log.info("解密用户信息:{}", decryptUser);
MiniProgramUserVO miniProgramUser = JSON.parseObject(decryptUser, MiniProgramUserVO.class);
if (Objects.isNull(miniProgramUser)) {
throw new ServiceException("获取小程序用户信息失败");
}
return miniProgramUser;
}
}

View File

@@ -0,0 +1,45 @@
package com.cool.store.controller;
import com.cool.store.dto.wx.MiniProgramLoginDTO;
import com.cool.store.dto.wx.MiniProgramMsgDTO;
import com.cool.store.response.ResponseResult;
import com.cool.store.service.WechatMiniAppService;
import com.cool.store.vo.wx.CodeSessionVO;
import com.cool.store.vo.wx.MiniProgramUserVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
/**
* @author zhangchenbiao
* @FileName: MiniProgramAppController
* @Description:
* @date 2023-05-29 14:28
*/
@Api(tags = "微信小程序app接口")
@RestController
@RequestMapping("/appApi/mini-program")
public class MiniProgramAppController {
@Resource
private WechatMiniAppService wechatMiniAppService;
@ApiOperation("小程序登录")
@PostMapping("/code/login")
public ResponseResult<CodeSessionVO> login(@RequestBody @Valid MiniProgramLoginDTO param) {
CodeSessionVO codeSessionVO = wechatMiniAppService.miniProgramLogin(param);
return ResponseResult.success(codeSessionVO);
}
@ApiOperation("获取小程序用户信息")
@PostMapping("/user")
public ResponseResult<MiniProgramUserVO> queryMiniProgramUser(@RequestBody @Valid MiniProgramMsgDTO param) {
MiniProgramUserVO miniProgramUserVO = wechatMiniAppService.queryMiniProgramUser(param);
return ResponseResult.success(miniProgramUserVO);
}
}