更新登录结构

This commit is contained in:
LittleBoy 2022-11-20 23:46:21 +08:00
parent 7d67868635
commit 72c07d2a09
13 changed files with 369 additions and 38 deletions

View File

@ -35,6 +35,11 @@
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.14</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@ -72,6 +77,11 @@
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.57</version>
</dependency>
</dependencies>

View File

@ -0,0 +1,47 @@
package me.xiaoyan.point.api.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import me.xiaoyan.point.api.pojo.dto.ApiResult;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.annotation.Resource;
import java.util.List;
@RestControllerAdvice
public class AppResponseConfig implements ResponseBodyAdvice<Object> {
@Resource
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
long traceId = 0;
List<String> header = serverHttpResponse.getHeaders().get("x-trace-id");
if (header != null && header.size() > 0){
String xTraceId = header.get(0);
if (xTraceId != null) {
traceId = Long.valueOf(xTraceId);
}
}
if (o instanceof String) {
return objectMapper.writeValueAsString(ApiResult.success(o).setTraceId(traceId));
}
if (o instanceof ApiResult) {
return ((ApiResult) o).setTraceId(traceId);
}
return ApiResult.success(o).setTraceId(traceId);
}
}

View File

@ -0,0 +1,39 @@
package me.xiaoyan.point.api.config;
import me.xiaoyan.point.api.error.BizException;
import me.xiaoyan.point.api.pojo.dto.ApiResult;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
@RestControllerAdvice
public class ResponseExceptionConfig {
@ExceptionHandler(value = BizException.class) // 要捕获的异常类型
@ResponseBody
@ResponseStatus(HttpStatus.OK)
public ApiResult bizExceptionHandler(HttpServletRequest req, BizException e) {
return ApiResult.error(e.getCode(), e.getMessage());
}
// 参数异常
@ExceptionHandler(value = MethodArgumentNotValidException.class) // 要捕获的异常类型
@ResponseBody
@ResponseStatus(HttpStatus.OK)
public ApiResult bizExceptionHandler(HttpServletRequest req, MethodArgumentNotValidException e) {
System.out.println(e.getFieldError().getDefaultMessage());
return ApiResult.error(1201, e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
}
// 没有单独处理的异常都会被此方法处理
@ExceptionHandler(value = Exception.class) // 要捕获的异常类型
@ResponseBody
@ResponseStatus(HttpStatus.OK)
public ApiResult exceptionHandler(HttpServletRequest req, Exception e) {
return ApiResult.error(-1, "App Internal Error:" + e.getMessage());
}
}

View File

@ -1,29 +1,29 @@
package me.xiaoyan.point.api.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.springframework.beans.factory.annotation.Value;
import me.xiaoyan.point.api.pojo.UserInfo;
import me.xiaoyan.point.api.pojo.vo.UserLoginData;
import me.xiaoyan.point.api.service.UserInfoService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import javax.annotation.Resource;
import javax.validation.constraints.NotNull;
@RestController
public class WechatController {
@Resource
private UserInfoService userInfoService;
// code => openid等信息
// 使用wx.login获取到的code进行登录完成登录后下发用户登录凭证
@RequestMapping("/wechat/login")
@SneakyThrows
public String login(String code) {
return null;
public UserInfo login(@Validated @RequestBody UserLoginData data) {
//@NotNull(message = "登录code不能为空")
return userInfoService.login(data);
}
}

View File

@ -1,18 +1,23 @@
package me.xiaoyan.point.api.error;
import lombok.Data;
/**
* 自定义的业务异常类
*/
@Data
public class BizException extends RuntimeException {
/**
* 错误码
*/
private int code;
public BizException(int code, String message) {
private BizException(int code, String message) {
super(message);
this.code = code;
}
public BizException(Throwable e) {
super(e.getMessage());
this.code = 500;
public static BizException create(String message) {
return new BizException(-1, message);
}
public static BizException create(int code,String message) {
return new BizException(code, message);
}
}

View File

@ -0,0 +1,34 @@
package me.xiaoyan.point.api.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
import me.xiaoyan.point.api.error.BizException;
import java.io.Serializable;
@Data
@Accessors(chain = true)
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class ApiResult implements Serializable {
private int code;
private String message;
private Object data;
private long traceId;
public static ApiResult success(Object data) {
return new ApiResult(0, "success", data,0);
}
public static ApiResult error(int code, String message) {
return new ApiResult(code, message, null,0);
}
public static ApiResult error(BizException e) {
return new ApiResult(e.getCode(), e.getMessage(), null,0);
}
}

View File

@ -0,0 +1,33 @@
package me.xiaoyan.point.api.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CodeSessionData implements Serializable {
/**
* 用户唯一标识
*/
private String openid;
/**
* 会话密钥,用于签名或解密
*/
private String sessionKey;
/**
* 用户在开放平台的唯一标识符
*/
private String unionid;
private Integer errcode;
private String errmsg;
}

View File

@ -0,0 +1,33 @@
package me.xiaoyan.point.api.pojo.dto;
import lombok.*;
import lombok.experimental.Accessors;
import me.xiaoyan.point.api.pojo.UserInfo;
import java.io.Serializable;
@Data
@Accessors(chain = true)
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class WechatUserInfo implements Serializable {
private String openId;
private String unionId;
private String avatarUrl;
private String nickName;
private Integer gender;
private String city;
private String province;
private String country;
public UserInfo getUserinfo() {
return UserInfo.builder()
.province(province).city(city)
.nickname(nickName)
.headImage(avatarUrl)
.openId(openId)
.build();
}
}

View File

@ -0,0 +1,16 @@
package me.xiaoyan.point.api.pojo.vo;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
@Data
public class UserLoginData implements Serializable {
@NotEmpty(message = "encryptedData参数不能为空")
private String encryptedData;
@NotEmpty(message = "iv参数不能为空")
private String iv;
@NotEmpty(message = "code参数不能为空")
private String code;
}

View File

@ -2,13 +2,14 @@ package me.xiaoyan.point.api.service;
import com.baomidou.mybatisplus.extension.service.IService;
import me.xiaoyan.point.api.pojo.UserInfo;
import me.xiaoyan.point.api.pojo.vo.UserLoginData;
public interface UserInfoService extends IService<UserInfo> {
/**
* 用户的登录
* @param code 执行wx.login成功后的code
* @param data
* @return
*/
public UserInfo login(String code);
public UserInfo login(UserLoginData data);
}

View File

@ -4,11 +4,14 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.xiaoyan.point.api.util.WechatDecryptDataUtil;
import me.xiaoyan.point.api.error.BizException;
import me.xiaoyan.point.api.mapper.PointMapper;
import me.xiaoyan.point.api.mapper.UserInfoMapper;
import me.xiaoyan.point.api.pojo.Point;
import me.xiaoyan.point.api.pojo.UserInfo;
import me.xiaoyan.point.api.pojo.dto.CodeSessionData;
import me.xiaoyan.point.api.pojo.dto.WechatUserInfo;
import me.xiaoyan.point.api.pojo.vo.UserLoginData;
import me.xiaoyan.point.api.service.PointRecordService;
import me.xiaoyan.point.api.service.PointService;
import me.xiaoyan.point.api.service.UserInfoService;
@ -21,9 +24,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.util.Map;
@Service
@Slf4j
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo>
@ -63,16 +63,21 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo>
@Transactional
@Override
public UserInfo login(String code) {
String openId = getOpenIdByCode(code);
// TODO 获取用户的基本信息
public UserInfo login(UserLoginData data) {
// 使用code换取登录session相关数据
CodeSessionData sessionData = getOpenIdByCode(data.getCode());
// 使用openid查询用户信息
final UserInfo userInfo = this.getBaseMapper().selectOneByOpenId(openId);
final UserInfo userInfo = this.getBaseMapper().selectOneByOpenId(sessionData.getOpenid());
// 判断用户信息是否存在
if (userInfo == null) {
// 先解码获取用户数据
WechatUserInfo wechatUserInfo = WechatDecryptDataUtil.decryptData(
data.getEncryptedData(),
sessionData.getSessionKey(),
data.getIv()
);
// 不存在走 第一次登录流程
firstLogin(userInfo);
firstLogin(wechatUserInfo.getUserinfo());
}
// 首先
return userInfo;
@ -88,7 +93,7 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo>
// 使用code换取openId
@SneakyThrows
private String getOpenIdByCode(String code) {
private CodeSessionData getOpenIdByCode(String code) {
String url = CODE2SESSION + "?appid=" + app_id + "&secret=" + app_secret
+ "&js_code=" + code + "&grant_type=authorization_code";
try (CloseableHttpClient client = HttpClients.createDefault()) {
@ -96,11 +101,11 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo>
final CloseableHttpResponse response = (CloseableHttpResponse) client.execute(get);
final String content = EntityUtils.toString(response.getEntity());
ObjectMapper mapper = new ObjectMapper();
final Map map = mapper.convertValue(content, Map.class);
if ((Integer) map.get("errcode") != 0) {
throw new BizException(1101, "换取code失败(" + map.get("errmsg") + ")");
final CodeSessionData data = mapper.convertValue(content, CodeSessionData.class);
if (data.getErrcode() != 0) {
throw BizException.create(1101, "换取code失败(" + data.getErrmsg() + ")");
}
return map.get("openid").toString();
return data;
}
}
}

View File

@ -0,0 +1,82 @@
package me.xiaoyan.point.api.util;
import com.google.gson.Gson;
import me.xiaoyan.point.api.pojo.dto.WechatUserInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.util.StringUtils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.Security;
import java.util.Arrays;
import java.util.Base64;
/**
* 微信工具类
*/
public class WechatDecryptDataUtil {
public static WechatUserInfo decryptData(String encryptDataB64, String sessionKeyB64, String ivB64) {
String result = new String(
decryptOfDiyIV(
Base64.getDecoder().decode(encryptDataB64),
Base64.getDecoder().decode(sessionKeyB64),
Base64.getDecoder().decode(ivB64)
)
);
if(!StringUtils.hasLength(result)){
throw new RuntimeException("解码失败");
}
return (new Gson()).fromJson(result,WechatUserInfo.class);
}
private static final String KEY_ALGORITHM = "AES";
private static final String ALGORITHM_STR = "AES/CBC/PKCS7Padding";
private static Key key;
private static Cipher cipher;
private static void init(byte[] keyBytes) {
// 如果密钥不足16位那么就补足. 这个if 中的内容很重要
int base = 16;
if (keyBytes.length % base != 0) {
int groups = keyBytes.length / base + (keyBytes.length % base != 0 ? 1 : 0);
byte[] temp = new byte[groups * base];
Arrays.fill(temp, (byte) 0);
System.arraycopy(keyBytes, 0, temp, 0, keyBytes.length);
keyBytes = temp;
}
// 初始化
Security.addProvider(new BouncyCastleProvider());
// 转化成JAVA的密钥格式
key = new SecretKeySpec(keyBytes, KEY_ALGORITHM);
try {
// 初始化cipher
cipher = Cipher.getInstance(ALGORITHM_STR, "BC");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 解密方法
*
* @param encryptedData 要解密的字符串
* @param keyBytes 解密密钥
* @param ivs 自定义对称解密算法初始向量 iv
* @return 解密后的字节数组
*/
private static byte[] decryptOfDiyIV(byte[] encryptedData, byte[] keyBytes, byte[] ivs) {
byte[] encryptedText = null;
init(keyBytes);
try {
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivs));
encryptedText = cipher.doFinal(encryptedData);
} catch (Exception e) {
e.printStackTrace();
}
return encryptedText;
}
}

View File

@ -0,0 +1,26 @@
package me.xiaoyan.point.api;
import me.xiaoyan.point.api.pojo.dto.WechatUserInfo;
import me.xiaoyan.point.api.util.WechatDecryptDataUtil;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class WechatTests {
@DisplayName("解密用户信息")
@Test
void testDecode(){
String encryptedData = "Ezl1S3BHjBeg/HP0Nim/c9eLLYP5L1kgvqSpR+RQYFG6c3Qx0K5U6btEY5IByCw1OsnZ0hKDccNJn3VA/ZgpkxjDODt+XLcph3KqEL6LDU9BLDRFxCI7u+eHBorz5HWYzGITXiuyPb9NWmGAPXwwp0abFqqaycb5u4oii7I/tnh7NIRIcMxAft1YfbVdzQDjraRHFH5hg6Eh4RGSjy6rg1bG/sMecw4+XWM1psTjKBYNwtsG5oxBWja6DniPhmWU6ZwjVMfgJX5Z7wcw2vtmuDPOMCEE1SEcwmTcQ5YKLlPsPBAJhrF3Lxg9oTD1/IlZ";
String iv = "ckSRPBLA61WONzUDINIWMg==";
WechatUserInfo wechatUserInfo = WechatDecryptDataUtil.decryptData(
"SCK0Ik7THl+USkwTRqTQ9BYGe6rWlXosQ8fWA3I3AsFHTCuPnjbsjFooIEZVcS6mq911XeP5BJJBpPU6A1O3aNuC9L7ebXqTMQX83bVBtaDQySvCIlyyq26xhm8AbWWexl5994NJDpKkNml9ilbYia99bF8bXzXvLCksQQkz82EpZTqztzmCrTdFBZOIrJ+lDnl7rSBWJvVVtoagzgSq2Ux59LcJtxCukIUoZ8fz54//Hm4GhrLucO4zPKTi087f77Pd9K9Rz3LLJ79NMQHHQLtZ38Ws79IKoHBZ7xHXbl3O8xPeTBrrWeHbNfNs1CbOyoe0RwXVjs/fMR9451PeLVM5jg4fj3IDyTFjpx5aUzQRaIrSY/BjVFJoxU/viwQC6LsCBOyXl5uV5h+qIPC5suFbmhl5Q56eU07wbOjNxktEIJsIgbrT+GWRZVPba3dUo+6RoZySxCMA16TRKfuTwjXceV3oNueFdTNyw05s9N43OkrLeKcz1dFeNnpA9DHEVngB1J7MY4RkazdKaWzrld1DjxW6+rk01GgtAr3+H88=",
"PKX9EeH6y+pzz8qYrga2jQ==",
"Ql/m+Ksll5ziCCZj+07J6g=="
);
System.out.println(wechatUserInfo);
}
}