需求:用户进入后台个人中心,可扫码绑定微信小程序,后续不再需要输入密码,可直接扫码登录后台
扫码登录:用户微信扫码进入小程序,选择已绑定的账号登录
进入正题
——————————————————————————————
所有需要的参数因为公司业务需求都放在数据库,所以就出现了这一行代码
Map<String, Object> map = this.getSysConfig(“wxMiniprogram”);
redis有的话就用,没有就删了
这里特别提示一下,下面两个方法是放在一个公共的Utils包里面,由于该类没有注入到spring管理,导致后台查询失效,于是在公共类里面添加了需要的bean
protected RedisService redisService = SpringContextHolder.getBean(RedisService.class);
private ConfigService configService = SpringContextHolder.getBean(ConfigService.class);
公共类方法-1:获取生成小程序码的access_token
固定值:
授权类型
grant_type:client_credential
Token获取地址
accessTokenUrl:https://api.weixin.qq.com/cgi-bin/token
appid和appSecret在微信小程序自行查看
public String getAccessToken() {
String access_token = null;
try {
access_token = (String) redisService.get("wxUserBindCodeAccesstoken");
if (StringUtils.isBlank(access_token)) {
Map<String, Object> map = this.getSysConfig("wxMiniprogram");
Map<String, String> requestParam = new HashMap<String, String>();
requestParam.put("grant_type", (String) map.get("grant_type"));
requestParam.put("appid", (String) map.get("appid"));
requestParam.put("secret", (String) map.get("appSecret"));
String accessTokenUrl = (String) map.get("accessTokenUrl");
Map<String, Object> extraParam = new HashMap<String, Object>();
extraParam.put("type", "getToken");
// 发起连接 sendPost 为一个请求各种接口而封装的函数,并转换JSON字符串为JSONObject
JSONObject jsonObject = JSON.parseObject(this.sendPost(accessTokenUrl, requestParam, extraParam));
access_token = jsonObject.getString("access_token");
// 缓存wxCodeAccesstoken
redisService.set("wxUserBindCodeAccesstoken", access_token, RedisConstants.WX_CODE_TOKEN_TIMEOUT);
}
} catch (Exception e) {
log.error("获取WXToken失败", e);
return null;
}
return access_token;
}
公共类方法-2:向指定 URL 发送POST方法的请求
/**
* 向指定 URL 发送POST方法的请求
*
* @param url 发送请求的 URL
* @param param 请求参数
* @Param extraParam 额外参数
* @return 所代表远程资源的响应结果
*/
public String sendPost(String url, Map<String, ?> paramMap, Map<String, Object> extraParam) {
// sendPost额外参数 判断用户是发起获取小程序码还是其他的连接
String type = (String) extraParam.get("type");
// 默认返回 HTTPS
// URLConnection conn = null;
// 默认返回HTTP,用接收返回数据
HttpURLConnection conn = null;
PrintWriter out = null;
InputStream in = null;
BufferedReader bufferedReader = null;
String result = "";
ByteArrayOutputStream bos = null;
String param = "";
if (type.equals("getQrCode")) {
param = JSON.toJSONString(paramMap);
} else {
param = "";
Iterator<String> it = paramMap.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
param += key + "=" + paramMap.get(key) + "&";
}
}
try {
URL realUrl = new URL(url);
// 打开和URL之间的连接
conn = (HttpURLConnection) realUrl.openConnection(); // 返回对象为 HttpsURLConnection
// 设置通用的请求属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 超时设置,防止网络异常的情况下,可能会导致程序僵死而不继续往下执行
conn.setConnectTimeout(3000);
conn.setReadTimeout(3000);
// 发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
// 获取URLConnection对象对应的输出流
out = new PrintWriter(conn.getOutputStream());
// 发送请求参数
out.print(param);
// flush输出流的缓冲
out.flush();
// 定义BufferedReader输入流来读取URL的响应
if (type.equals("getQrCode")) {
Integer width = (Integer) paramMap.get("width");
in = conn.getInputStream(); // 得到图片的二进制内容
int leng = in.available();
if (leng < 1000) { // 出现错误时,获取字符长度就一百不到,图片的话有几万的长度
// 定义BufferedReader输入流来读取URL的响应
bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
String line;
while ((line = bufferedReader.readLine()) != null) {
result += line;
}
log.error("调用微信小程序小程序码接口出错:" + result);
// 清空 access_token 缓存
redisService.del("wxUserBindCodeAccesstoken");
return null;
}
// 修改图片的分辨率,分辨率太大打印纸不够大
// BufferedInputStream in2 = new BufferedInputStream(conn.getInputStream());
// 将文件二进制流修改为图片流
Image srcImg = ImageIO.read(in);
// 构建图片流
BufferedImage buffImg = new BufferedImage(width, width, BufferedImage.TYPE_INT_RGB);
// 绘制改变尺寸后的图
buffImg.getGraphics().drawImage(srcImg.getScaledInstance(width, width, Image.SCALE_SMOOTH), 0, 0, null);
// 将图片流修改为文件二进制流
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(buffImg, "png", os);
in = new ByteArrayInputStream(os.toByteArray());
// 刷新,将重置为类似于首次创建时的状态
buffImg.flush();
srcImg.flush();
// 设null是告诉jvm此资源可以回收
buffImg = null; // 该io流不存在关闭函数
srcImg = null; // 该io流不存在关闭函数
os.close();
bos = new ByteArrayOutputStream();
byte[] b1 = new byte[1024];
int len = -1;
while ((len = in.read(b1)) != -1) {
bos.write(b1, 0, len);
}
byte[] fileByte = bos.toByteArray(); // 转换为字节数组,方便转换成base64编码
// 对字节数组转换成Base64字符串
result = Base64.encodeBase64String(fileByte); // import org.apache.commons.codec.binary.Base64;
return result;
}
bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
String line;
while ((line = bufferedReader.readLine()) != null) {
result += line;
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
// 使用finally块来关闭输出流、输入流
finally {
try {
if (bufferedReader != null) {
bufferedReader.close();
}
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
if (bos != null) {
bos.close();
}
if (conn != null) {
conn.disconnect();
conn = null;
}
// 让系统回收资源,但不一定是回收刚才设成null的资源,可能是回收其他没用的资源。
System.gc();
} catch (IOException ex) {
ex.printStackTrace();
}
}
return result; // 返回的为JSON字符串
}
获取绑定小程序码
- page为扫码后进入的小程序页面地址,小程序必须已发布
- WxUtils就是上面我说的自己写的公共类
- 这里返回的小程序码是base64String格式的,前端解码需要this.qrcode = ‘data:image/png;base64,’ + response.data,response.data就是后台返回过去的数据,qrCode才是完整的小程序码
this.qrcode = ‘data:image/png;base64,’ + response.data
固定值:
小程序码生成地址
qrCodeUrl:https://api.weixin.qq.com/wxa/getwxacodeunlimit
这里涉及绑定逻辑,生成一个随机字符串作为小程序码的参数scene,并将该参数与当前登录用户绑定,存入redis;
生成小程序码之后,这时候用户在后台使用微信扫码,进入小程序;
用户授权之后,后台通过code换取openId,将接收到的scene与openId一同传回到后台;
后台在缓存中通过该scene查找user,存在则将绑定关系插入数据库,绑定由于有业务逻辑就不放上来了,需要判断是否已绑定,redis中scene和user绑定的有效期
// 获取16位随机字符串
String scene = UUID.randomUUID().toString().replaceAll("-", “”).substring(0, 16);
// 获取当前登录用户
User user = (User) this.getUser();
// 在缓存中将scene与user绑定 有效时间两分钟
redisService.set(scene, user, RedisConstants.WX_BIND_SCENE_USER_TIMEOUT);
获取微信绑定小程序码
/**
* 获取微信绑定小程序码
*/
@Override
public Response<String> getBindQrcode() {
WxUtils wxUtils = new WxUtils();
// 从系统配置里获取小程序配置参数
Map<String, Object> map = wxUtils.getSysConfig("wxMiniprogram");
// sendPost方法的额外参数
Map<String, Object> extraParam = new HashMap<String, Object>();
// 小程序码宽度
Integer width = Integer.valueOf((String) map.get("bindQrCodeWidth"));
String access_token = wxUtils.getAccessToken();
if (access_token == null) {
return new Response<String>().fail("access_token为空!");
}
String qrCodeUrl = (String) map.get("qrCodeUrl");
// qrCodeUrl 获取小程序码的接口头部 拼接完整的URl
String url = qrCodeUrl + "?access_token=" + access_token;
// 小程序的参数可查看官方文档
Map<String, Object> requestParam = new HashMap<String, Object>();
// 扫码后需要跳转的页面
requestParam.put("page", (String) map.get("bindPage"));
// 获取16位随机字符串
String scene = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16);
// 获取当前登录用户
User user = (User) this.getUser();
// 在缓存中将scene与user绑定 有效时间两分钟
redisService.set(scene, user, RedisConstants.WX_BIND_SCENE_USER_TIMEOUT);
// 携带的参数
requestParam.put("scene", scene);
// 小程序码的宽度
requestParam.put("width", width);
log.info("绑定小程序小程序码scene:{}", scene);
// sendPost额外参数 判断用户是发起获取小程序码还是其他的连接
extraParam.put("type", "getQrCode");
// 发起连接
String base64String = wxUtils.sendPost(url, requestParam, extraParam);
return new Response<String>().success(base64String);
}
通过code换取openid
/**
* 通过code换取openid
*/
@Override
public Response<Object> getOpenidByCode(String code) {
WxUtils wxUtils = new WxUtils();
// 获取系统小程序配置
Map<String, Object> map = wxUtils.getSysConfig("wxMiniprogram");
RestTemplate restTemplate = new RestTemplate();
String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + map.get("appid") + "&secret="
+ map.get("appSecret") + "&js_code=" + code + "&grant_type=authorization_code";
String res = restTemplate.getForEntity(url, String.class).getBody();
logger.info("获取openid请求:{}, 返回结果:{}", url, res);
JSONObject resJson = JSONObject.parseObject(res);
// 判断请求结果
String errcode = resJson.getString("errcode");
if (StringUtils.isBlank(errcode) || "0".equals(errcode)) {
return new Response<Object>().success(resJson.getString("openid"));
} else {
return new Response<Object>().fail(resJson.getString("errcode") + ":" + resJson.getString("errmsg"));
}
}
扫码登录
这里登录的逻辑是生成一个随机字符串,同时作为二维码参数、后续的websocketId,将scene单独存入缓存;
此时生成小程序码base64字符串,用户扫码后进入小程序,通过code换取openId,再通过openId获取已绑定的用户列表(openId在绑定的时候已经跟用户一起存入数据库了),选择其中一个进行登录,将选中用户的userId与从后台获取的scene再传回后台,调用接口实现登录;
流程中所有的定时任务请自行编写
获取扫码登录小程序码
@Override
public Response<Object> getLoginQrcode() {
WxUtils wxUtils = new WxUtils();
// 从系统配置里获取小程序配置参数
Map<String, Object> map = wxUtils.getSysConfig("wxMiniprogram");
// sendPost方法的额外参数
Map<String, Object> extraParam = new HashMap<String, Object>();
// 二维码宽度
Integer width = Integer.valueOf((String) map.get("loginQrCodeWidth"));
String access_token = wxUtils.getAccessToken();
if (access_token == null) {
return new Response<Object>().fail("access_token为空!");
}
Map<String, Object> resultMap = new HashMap<String, Object>();
String qrCodeUrl = (String) map.get("qrCodeUrl");
// qrCodeUrl 获取小程序码的接口头部 拼接完整的URl
String url = qrCodeUrl + "?access_token=" + access_token;
// 小程序的参数可查看官方文档
Map<String, Object> requestParam = new HashMap<String, Object>();
// 扫码后需要跳转的页面
requestParam.put("page", map.get("loginPage"));
// 获取16位随机字符串
String scene = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16);
// 登录二维码场景值,后续作为websocketId
resultMap.put("scene", scene);
// 将随机参数存入缓存 有效时间两分钟
redisService.set(scene, "", RedisConstants.WX_LOGIN_TIMEOUT);
// 携带的参数
requestParam.put("scene", scene);
// 二维码的宽度
requestParam.put("width", width);
// sendPost额外参数 判断用户是发起获取二维码还是其他的连接
extraParam.put("type", "getQrCode");
// 发起连接
String base64String = wxUtils.sendPost(url, requestParam, extraParam);
resultMap.put("imgBase64", base64String);
return new Response<Object>().success(resultMap);
}
点击登录
/**
* 用户扫码选择登录用户后调用该接口实现登录
*/
@Override
public Response<String> signinByQrCode(String userId, String scene) {
// 检查scene是否有效
String sceneValue = (String) redisService.get(scene);
if (sceneValue != null) {
// 生成token并绑定user
User user = userService.getById(userId);
if (user != null) {
String token = generateToken(user);
redisService.set(scene, token, RedisConstants.WX_LOGIN_TIMEOUT);// 将随机参数存入缓存 有效时间两分钟
return new Response<String>().success("扫码登录成功");
} else {
return new Response<String>().fail("登录用户不存在,请重新选择");
}
} else {
return new Response<String>().fail("二维码已超时,请重新扫码");
}
}
——————————————————————————————————————
到这里基本上就结束了,前端使用的是vue+elementUi,我不怎么懂前端所以就不放代码了,放了反而会绕晕,不过东西也不多,说还缺什么吧大多就是一些定时任务了,前端的定时扫描啥的。
第一次写,如有错误请指正~