Java后端小程序登录
最近,公司在做小程序,提到小程序,那就绕不开小程序的登录,实话实说,小程序的文档写的真的不咋地,这里记录下自己做小程序登录时写的代码以及遇到的一些问题(具体的业务需求我会屏蔽掉),这里只涉及到后端代码,所以默认你是申请了一个小程序并且有了appid和secret。不管怎么说,还是放上官方的文档链接:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
官方时序图及简单说明

上图要仔细的看一遍,这里简单的做一个说明,首先由小程序端调用wx.login()去获取code,然后,再通过wx.getUserInfo()去获取用户信息(这里请求login和getUserInfo是一起的,把这两次请求的数据合并发给服务端的login接口),通过请求,把:
1.code //临时登入凭证
// 如果不同意获取用户信息,则下面四个参数获取不到
2.rawData //用户非敏感信息,头像和昵称之类的
3.signature //签名
4.encryteDate //用户敏感信息,需要解密,(包含unionID)
5.iv //解密算法的向量
给到服务端,服务端根据code+appid+appsecret去请求:
方法,获取到session_key和openid(这里无法获取unionID),通过session_key,iv来解密encrypteDate获取用户敏感信息和unionID,把用户信息保存到数据库。然后,我们把sesssoin_key和openid保存下来,与token(自定义登入状态)来进行关联,最后把小程序需要的数据返回给小程序端,以后就通过token来维护用户登入状态。
用户表结构设计
CREATE TABLE `wechat_user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`token` varchar(100) NOT NULL COMMENT 'token',
`nickname` varchar(100) DEFAULT NULL COMMENT '用户昵称',
`avatar_url` varchar(500) DEFAULT NULL COMMENT '用户头像',
`gender` int(11) DEFAULT NULL COMMENT '性别 0-未知、1-男性、2-女性',
`country` varchar(100) DEFAULT NULL COMMENT '所在国家',
`province` varchar(100) DEFAULT NULL COMMENT '省份',
`city` varchar(100) DEFAULT NULL COMMENT '城市',
`mobile` varchar(100) DEFAULT NULL COMMENT '手机号码',
`open_id` varchar(100) NOT NULL COMMENT '小程序openId',
`union_id` varchar(100) DEFAULT '' COMMENT '小程序unionId',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '插入时间',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` timestamp NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`),
KEY `idx_open_id` (`open_id`),
KEY `idx_union_id` (`union_id`),
KEY `idx_mobile` (`mobile`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='小程序用户表';
具体代码
说明,如果@Getter报错,那就删掉,自己加Getter,Setter,@Api开头的注解是swagger的注解,不需要的可以删掉
请求类
@ApiModel
@Getter
@Setter
public class WechatLoginRequest {
@NotNull(message = "code不能为空")
@ApiModelProperty(value = "微信code", required = true)
private String code;
@ApiModelProperty(value = "用户非敏感字段")
private String rawData;
@ApiModelProperty(value = "签名")
private String signature;
@ApiModelProperty(value = "用户敏感字段")
private String encryptedData;
@ApiModelProperty(value = "解密向量")
private String iv;
}
非敏感信息DO
@Getter
@Setter
public class RawDataDO {
private String nickName;
private String avatarUrl;
private Integer gender;
private String city;
private String country;
private String province;
}
用户DO
@Getter
@Setter
public class WechatUserDO {
private Integer id;
private String token;
private String nickname;
private String avatarUrl;
private Integer gender;
private String country;
private String province;
private String city;
private String mobile;
private String openId;
private String unionId;
private String createdAt;
private String updatedAt;
}
HttpClientUtils
public class HttpClientUtils {
final static int TIMEOUT = 1000;
final static int TIMEOUT_MSEC = 5 * 1000;
public static String doPost(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (paramMap != null) {
List<NameValuePair> paramList = new ArrayList<>();
for (Entry<String, String> param : paramMap.entrySet()) {
paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
throw e;
}
}
return resultString;
}
private static RequestConfig builderRequestConfig() {
return RequestConfig.custom()
.setConnectTimeout(TIMEOUT_MSEC)
.setConnectionRequestTimeout(TIMEOUT_MSEC)
.setSocketTimeout(TIMEOUT_MSEC).build();
}
}
service
public interface WechatService {
Map<String, Object> getUserInfoMap(WechatLoginRequest loginRequest) throws Exception;
}
Service impl
@Service
public class WechatServiceImpl implements WechatService {
private static final String REQUEST_URL = "https://api.weixin.qq.com/sns/jscode2session";
private static final String = "authorization_code";
@Override
public Map<String, Object> getUserInfoMap(WechatLoginRequest loginRequest) throws Exception {
Map<String, Object> userInfoMap = new HashMap<>();
// logger报错的话,删掉就好,或者替换为自己的日志对象
logger.info("Start get SessionKey,loginRequest的数据为:" + JSONObject.toJSONString(loginRequest));
JSONObject sessionKeyOpenId = getSessionKeyOrOpenId(loginRequest.getCode());
// 这里的ErrorCodeEnum是自定义错误字段,可以删除,用自己的方式处理
Assert.isTrue(sessionKeyOpenId != null, ErrorCodeEnum.P01.getCode());
// 获取openId && sessionKey
String openId = sessionKeyOpenId.getString("openid");
// 这里的ErrorCodeEnum是自定义错误字段,可以删除,用自己的方式处理
Assert.isTrue(openId != null, ErrorCodeEnum.P01.getCode());
String sessionKey = sessionKeyOpenId.getString("session_key");
WechatUserDO insertOrUpdateDO = buildWechatUserDO(loginRequest, sessionKey, openId);
// 根据code保存openId和sessionKey
JSONObject sessionObj = new JSONObject();
sessionObj.put("openId", openId);
sessionObj.put("sessionKey", sessionKey);
// 这里的set方法,自行导入自己项目的Redis,key自行替换,这里10表示10天
stringJedisClientTem.set(WechatRedisPrefixConstant.USER_OPPEN_ID_AND_SESSION_KEY_PREFIX + loginRequest.getCode(),
sessionObj.toJSONString(), 10, TimeUnit.DAYS);
// 根据openid查询用户,这里的查询service自己写,就不贴出来了
WechatUserDO user = wechatUserService.getByOpenId(openId);
if (user == null) {
// 用户不存在,insert用户,这里加了个分布式锁,防止insert重复用户,看自己的业务,决定要不要这段代码
if (setLock(WechatRedisPrefixConstant.INSERT_USER_DISTRIBUTED_LOCK_PREFIX + openId, "1", 10)) {
// 用户入库,service自己写
insertOrUpdateDO.setToken(getToken())
wechatUserService.save(insertOrUpdateDO);
userInfoMap.put("token", insertOrUpdateDO.getToken())
}
} else {
userInfoMap.put("token", wechatUser.getToken(
