在文章开始之前,推荐一些很值得阅读的好文章!感兴趣的也可以去看一下哦!
推荐文章 | 推荐原因 | 立即前往 |
---|---|---|
『前端必修课』视频文字特效 | 这篇文章是腾讯云开发者社区的BNTang的“前端必修课”系列之一 在这篇文章中,介绍了如何实现视频文字特效。文章总结了通过mix-blend-mode实现视频与文字融合特效的方法,以及如何通过CSS的object-fit属性让视频适应容器尺寸. 感兴趣的同学快来阅读吧~ |
halo 好久不见,我是杨不易呀,本片文章是写给热爱折腾的技术小伙伴们!
在我读书的时候就想玩这个功能很久了那个时候受限于这个功能需要企业或个体户去花费三百块认证服务号等方式, 反正企业或者个体户就难倒一大片了吧? 还要钱, 对于很多程序员是舍不得的, 那么不想认证又不想花费怎么办?
我看到腾讯云开发者社区的统一授权系统当中有一个扫码功能.
当我通过扫码进行授权后,我注意到它调用的并不是常见的微信授权, 而是小程序的授权。一般情况下,微信授权会直接跳转到微信授权页面,用户通过微信客户端完成授权, 但是在这个场景下, 扫码之后, 系统引导我进入的是一个小程序授权页面.
通过小程序来实现有什么好处?
嘿嘿! 好处就是不用企业认证、不用个体户认证、不用花费三百大洋, 更加舒服的还能给小程序带来流量支持, 如果你的小程序人流量五百以上还能开通流量主每天赚点小钱.
⚠️ 本期内容涉及较多, 本次的项目实战题材内容较多就本篇文章就有五万字所以我将分两个文章展示, 本篇文章为后端篇,web前端和uniapp移动端将在另外一篇呈现! 点我前往: PC+小程序篇
技术选型: 这个功能关系三端、PC 端、后端、移动端(小程序) 前端部分我不会讲的太深, 主要点是思路和后端 前端使用 Vue3 + Vite 搭建前端简易架构 后端使用 Java SpringBoot3 + Redis 框架搭建服务 移动端使用 UniApp + Vue3 搭建用户交互(可以集成你自己的小程序搞流量) 准备材料:已经注册好小程序拿到对应的 APPID 等信息 准备 Redis 中间件
说了这么多来看看我们接下来要实现的整个流程吧, 在此之前我已经写好了一个案例
正常授权: web 端请求二维码 ->> 用户微信扫码跳转到小程序授权页面,主动发起扫码已成功请求 ->> 点击授权 更新二维码状态为成功
取消授权: web 端请求二维码 ->> 用户微信扫码跳转到小程序授权页面,主动发起扫码已成功请求 ->> 点击取消授权 更新二维码授权失败
我这里画了整个业务的流程图, 先解析整个业务
在实现功能前我们肯定要先看对应的文档,第一步我们要构建小程序二维码哪么前往小程序官方文档当中找到小程序专区有没有获取二维码的开放接口.
可以看到提供了小程序码的接口,一共有三个, 下面我介绍一下三个的区别
根据三个特点肯定是第二个最好,没有限制, 所以我们就直接使用第二个获取不限制的小程序码.
可以看到该接口的一些注意事项以及二维码 SCENE
带入这个 scene 字段的值会作为 query 参数传递给小程序/小游戏,用户扫描该码进入小程序/小游戏后,开发者可以获取到二维码中的 scene 值,再做处理逻辑.
那么我们生成二维码的时候需要生成一个这个 scene 的参数,接下来搭建后端来实现这个接口
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.21</version>
</dependency>
根据文档的提示我们需要 POST 请求去调用 getwxacodeunlimit
接口, 完整如下:
https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN
可以看到接口后面的参数 access_token 鉴权不是放在请求头和 body 当中是放在 url 当中这个请注意
根据文档查看一下该接口的完整请求参数
属性 | 类型 | 必填 | 说明 |
---|---|---|---|
access_token | string | 是 | 接口调用凭证,该参数为 URL 参数,非 Body 参数。使用getAccessToken 或者 authorizer_access_token |
scene | string | 是 | 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式) |
page | string | 否 | 默认是主页,页面 page,例如 pages/index/index,根路径前不要填加 /,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面。scancode_time为系统保留参数,不允许配置 |
check_path | bool | 否 | 默认是true,检查page 是否存在,为 true 时 page 必须是已经发布的小程序存在的页面(否则报错);为 false 时允许小程序未发布或者 page 不存在, 但page 有数量上限(60000个)请勿滥用。 |
env_version | string | 否 | 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop"。默认是正式版。 |
width | number | 否 | 默认430,二维码的宽度,单位 px,最小 280px,最大 1280px |
auto_color | bool | 否 | 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调,默认 false |
可以看到 access_token 和 scene 参数是必填并且最大支持 32 个可见字符且限制了文本格式
其它的参数:
page: 默认主页,我们需要自定义跳转则 必填
check_path: 默认检查是否存在否则会报错, 我们就默认设置 false 自己把控就行
env_version: 打开的小程序版本,我们就先设置开发版或者体验版
width: 二维码宽度, 默认 430 的大小差不多了都是这个大小我们就默认就行了
上面提到了需要 token 调用跟着文档继续找一下 token 怎么拿?
点我前往: token 文档
可以看到请求方式为 get 接口英文名 getAccessToken
请求参数也就三个必填项最后请求的接口
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}
属性 | 类型 | 说明 |
---|---|---|
access_token | string | 获取到的凭证 |
expires_in | number | 凭证有效时间,单位:秒。目前是7200秒之内的值 |
注意:小程序唯一凭证,即 AppID,可在「微信公众平台 - 设置 - 开发设置」页中获得
巧妙使用腾讯云 AI 代码助手生成请求 ACCESSTOKEN 代码,
然后执行发送请求,可以正常拿到 token 参数
/**
* 获取访问令牌
*/
@Test
public void getAccessTokenTest() {
getAccessToken();
}
public String getAccessToken() {
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}";
String body = HttpUtil.get(StrUtil.format(url, appId, secret));
JSONObject object = JSONUtil.parseObj(body);
return object.getStr("access_token");
}
在上面我介绍了小程序码的请求接口如下:
https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN
在上面我介绍了请求参数都需要哪些这里就不多说直接上代码
/**
* 创建二维码
*/
@Test
public void createQrCode() {
// 组装请求参数
String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + getAccessToken();
JSONObject jsonParam = new JSONObject();
// 封装请求对象
// scene值 - 最大32字符 雪花算法够了
jsonParam.set("scene", IdUtil.getSnowflakeNextIdStr());
// 跳往的小程序页面,一般为认证界面
jsonParam.set("page", "/page/auth/index");
// 图片宽度,默认为 430
jsonParam.set("width", "430");
// 检测页面是否存在,默认为 true
jsonParam.set("check_path", false);
}
小程序码的接口有点特殊,请求成功它返回 buffer 参数但是你请求失败它返回的 JSON 所以我们要编写自定义发送请求工具
POST
请求获取二维码的功能,并且对响应进行处理。下面定义一个远程请求方法我将讲解该方法做了些什么操作
public String remoteQRCode(String url, JSONObject jsonParam)
remoteQRCode
,返回类型为 String
,接收两个参数:url
:一个字符串,表示请求的目标 URL。jsonParam
:一个 JSONObject
,包含请求体中需要发送的数据。POST
请求byte[] responseBytes;
try (cn.hutool.http.HttpResponse response = HttpUtil.createPost(url)
.body(jsonParam.toString())
.header("Content-Type", "application/json")
.execute()) {
Hutool
库中的 HttpUtil.createPost(url)
创建一个 POST
请求,并设置请求体(jsonParam.toString()
)和请求头(Content-Type: application/json
)。execute()
方法会发送请求,并返回一个 HttpResponse
对象,表示 HTTP 响应。try
语法糖(自动关闭资源)来确保 HttpResponse
在结束后会被正确关闭。if (!response.isOk()) {
log.error("[remoteQRCode] 请求失败,状态码: {}", response.getStatus());
throw new RuntimeException("暂未支持小程序扫码登录!");
}
response.isOk()
会检查响应的状态码是否表示成功(即状态码 200-299)。如果不是成功状态码,则记录错误日志,并抛出一个 RuntimeException
,表示未支持小程序扫码登录。responseBytes = response.bodyBytes();
response.bodyBytes()
获取响应的字节数据(即二维码图片的二进制数据)。String responseStr = new String(responseBytes, StandardCharsets.UTF_8);
if (JSONUtil.isTypeJSON(responseStr)) {
log.error("[remoteQRCode] 微信接口返回错误: {}", responseStr);
throw new RuntimeException("暂未支持小程序扫码登录!");
}
responseStr
,并检查它是否为合法的 JSON 格式。JSONUtil.isTypeJSON()
方法判断返回的数据是否是 JSON 格式。如果是 JSON 格式,说明接口返回了错误信息(比如错误码或错误消息),记录日志并抛出异常。Files.write(Paths.get("output.png"), responseBytes);
responseBytes
保存为一个本地文件 output.png
,用于确认二维码图片是否损坏或无法读取。return Base64.encode(responseBytes);
responseBytes
编码为 Base64 格式的字符串并返回。这样返回的字符串可以方便地嵌入到网页或其他地方进行显示。} catch (Exception e) {
log.error("获取小程序二维码异常", e);
throw new RuntimeException("暂未支持小程序扫码登录!");
}
RuntimeException
,提示 "暂未支持小程序扫码登录!"。 /**
* 创建二维码
*/
@Test
public void createQrCode() {
// 组装请求参数
String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + getAccessToken();
JSONObject jsonParam = new JSONObject();
// 封装请求对象
// scene值 - 最大32字符 雪花算法够了
jsonParam.set("scene", IdUtil.getSnowflakeNextIdStr());
// 跳往的小程序页面,一般为认证界面
jsonParam.set("page", "page/auth/index");
// 图片宽度,默认为 430
jsonParam.set("width", "430");
// 检测页面是否存在,默认为 true
jsonParam.set("check_path", false);
// 构建发起请求
String remotedQRCode = remoteQRCode(url, jsonParam);
System.out.println(remotedQRCode);
}
/**
* 发送post请求
*
* @param url 请求地址
* @param jsonParam 请求参数
* @return 响应流
*/
public String remoteQRCode(String url, JSONObject jsonParam) {
try {
// 发送POST请求
byte[] responseBytes;
try (cn.hutool.http.HttpResponse response = HttpUtil.createPost(url)
.body(jsonParam.toString())
.header("Content-Type", "application/json")
.execute()) {
// 检查响应状态
if (!response.isOk()) {
log.error("[remoteQRCode] 请求失败,状态码: {}", response.getStatus());
throw new RuntimeException("暂未支持小程序扫码登录!");
}
responseBytes = response.bodyBytes();
}
// 尝试解析为JSON,判断是否为错误响应
String responseStr = new String(responseBytes, StandardCharsets.UTF_8);
if (JSONUtil.isTypeJSON(responseStr)) {
log.error("[remoteQRCode] 微信接口返回错误: {}", responseStr);
throw new RuntimeException("暂未支持小程序扫码登录!");
}
// 保存图片到文件中以确认图片是否损坏
Files.write(Paths.get("output.png"), responseBytes);
return Base64.encode(responseBytes);
} catch (Exception e) {
log.error("获取小程序二维码异常", e);
throw new RuntimeException("暂未支持小程序扫码登录!");
}
}
可以看到成功拿到小程序码 Base64 数据,并且生成了本地图片提供查看是否是损坏,如果没有问题可以将这段代码删除
后端默认返回data:image/png;base64,
前端可直接渲染使用
/*
* 提示:该行代码过长,系统自动注释不进行高亮。一键复制会移除系统注释
* data:image/png;base64,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
*/
拿到 Base64 去浏览器访问看看, 也是可以正常显示没任何问题!!
在前面的请求参数描述当中 部分参数如果搞错了会返回 JSON
哪么测试一下是否返回 JSON 从而抛出我们自定义的异常
故意在前面打上 /
通过测试正常的抛出了我们的异常,并且打印了微信的错误信息我们就可以去文档查看对应 code 是什么信息
没问题,也是正确的,这样子一来如果出现错误就可以快速的排查
在 application.yml
配置当中编写 yml 变量
# 小程序登录配置
login:
enable: true
appId: xxxxxxxxxx
secret: xxxxxxxxxxxx
# 小程序认证界面,扫码后将跳转到这个界面
authPage: pages/oauth/index
# 图片宽度,默认为 430
width: 430
# 是否页面是否存在 默认是true
checkPath: false
新增 WechatLoginConfig
配置文件
/*
* 您可以更改此项目但请不要删除作者署名谢谢,否则根据中华人民共和国版权法进行处理.
* You may change this item but please do not remove the author's signature,
* otherwise it will be dealt with according to the Copyright Law of the People's Republic of China.
* yangbuyi Copyright (c) https://yby6.com 2024.
*/
package com.yby6.tencentwechatappletloginproject.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 小程序配置
*
* @author Yang Shuai
* Create By 2024/11/26
*/
@Configuration
@ConfigurationProperties(prefix = "login")
public class WechatLoginConfig {
/**
* 是否开启
*/
public static boolean enable;
/**
* 小程序appId
*/
public static String appId;
/**
* 小程序密钥
*/
public static String secret;
/**
* 小程序授权登录页面
*/
public static String authPage;
/**
* 二维码大小 默认430
*/
public static String width = "430";
/**
* 是否检查路径
*/
public static boolean checkPath;
public void setEnable(boolean enable) {
WechatLoginConfig.enable = enable;
}
public void setAppId(String appId) {
WechatLoginConfig.appId = appId;
}
public void setSecret(String secret) {
WechatLoginConfig.secret = secret;
}
public void setAuthpage(String authPage) {
WechatLoginConfig.authPage = authPage;
}
}
这样子就可以以静态的方式去调用参数了
创建文件夹 controller 新增 AppletAuthController
控制器
编写 createAppletQrCode
接口 该接口是创建唯一小程序码提供调用方使用
package com.yby6.tencentwechatappletloginproject.controller;
import com.yby6.tencentwechatappletloginproject.domain.R;
import com.yby6.tencentwechatappletloginproject.service.AppletAuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 小程序身份验证控制器
*
* @author Yang Shuai
* Create By 2024/11/26
*/
@RestController
@RequestMapping("applet")
@RequiredArgsConstructor
public class AppletAuthController {
private final AppletAuthService appletAuthService;
/**
* 小程序二维码认证
*
* @return {@code R<Object> }
*/
@PostMapping("createAppletQrCode")
public R<Object> createAppletQrCode() {
return appletAuthService.createAppletQrCode();
}
}
创建 domain
文件夹 新增 R
类 用于返回接口参数给前端
/*
* 您可以更改此项目但请不要删除作者署名谢谢,否则根据中华人民共和国版权法进行处理.
* You may change this item but please do not remove the author's signature,
* otherwise it will be dealt with according to the Copyright Law of the People's Republic of China.
* yangbuyi Copyright (c) https://yby6.com 2024.
*/
package com.yby6.tencentwechatappletloginproject.domain;
import java.io.Serial;
import java.io.Serializable;
/**
* 响应信息主体
*
* @author 杨不易呀
* Create By 2024/11/26
*/
public class R<T> implements Serializable {
/**
* 成功
*/
public static final int SUCCESS = 200;
/**
* 失败
*/
public static final int FAIL = 500;
/**
* 失败忽略
*/
public static final int FAILIGNORE = 201;
@Serial
private static final long serialVersionUID = 1L;
/**
* 消息状态码
*/
private int code;
/**
* 消息内容
*/
private String msg;
/**
* 数据对象
*/
private T data;
public static <T> R<T> ok() {
return restResult(null, SUCCESS, "操作成功");
}
public static <T> R<T> ok(T data) {
return restResult(data, SUCCESS, "操作成功");
}
public static <T> R<T> ok(String msg) {
return restResult(null, SUCCESS, msg);
}
public static <T> R<T> ok(String msg, T data) {
return restResult(data, SUCCESS, msg);
}
public static <T> R<T> fail() {
return restResult(null, FAIL, "操作失败");
}
public static <T> R<T> failIgnore() {
return restResult(null, FAILIGNORE, "操作失败");
}
public static <T> R<T> failIgnore(T data) {
return restResult(data, FAILIGNORE, "操作失败");
}
public static <T> R<T> fail(String msg) {
return restResult(null, FAIL, msg);
}
public static <T> R<T> fail(T data) {
return restResult(data, FAIL, "操作失败");
}
public static <T> R<T> fail(String msg, T data) {
return restResult(data, FAIL, msg);
}
public static <T> R<T> fail(int code, String msg) {
return restResult(null, code, msg);
}
public static <T> R<T> check(int row) {
return row > 0 ? ok() : fail();
}
public static <T> R<T> check(boolean isTure) {
return isTure ? ok() : fail();
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static <T> R<T> warn(String msg) {
return restResult(null, 601, msg);
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static <T> R<T> warn(String msg, T data) {
return restResult(data, 601, msg);
}
private static <T> R<T> restResult(T data, int code, String msg) {
R<T> r = new R<>();
r.setCode(code);
r.setData(data);
r.setMsg(msg);
return r;
}
public static <T> Boolean isError(R<T> ret) {
return !isSuccess(ret);
}
public static <T> Boolean isSuccess(R<T> ret) {
return R.SUCCESS == ret.getCode();
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
创建 service
文件夹 新增 AppletAuthService
服务层 提供业务逻辑支撑, 实现 createAppletQrCode
创建小程序码方法
package com.yby6.tencentwechatappletloginproject.service;
import com.yby6.tencentwechatappletloginproject.domain.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 小程序身份验证服务
*
* @author Yang Shuai
* Create By 2024/11/26
*/
@Slf4j
@Service
public class AppletAuthService {
/**
* 小程序二维码认证
*
* @return {@code R<Object> }
*/
public R<Object> createAppletQrCode() {
// .... TODO 创建二维码
return null;
}
}
接着把上面我们写好的获取小程序码测试代码直接复制过来
/**
* 带参数无限个数小程序码接口
* <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/qr-code/getUnlimitedQRCode.html">...</a>
*
* @param accessToken 访问令牌
* @param scene 场景
* @return {@code InputStream }
*/
private String getWechatQrCode(String accessToken, String scene) {
String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken;
JSONObject jsonParam = new JSONObject();
// 封装请求对象
// scene值
jsonParam.set("scene", scene);
// 跳往的小程序页面,一般为认证界面
jsonParam.set("page", WechatLoginConfig.authPage);
// 图片宽度,默认为 430
jsonParam.set("width", WechatLoginConfig.width);
// 检测页面是否存在,默认为 true
jsonParam.set("check_path", WechatLoginConfig.checkPath);
// 返回请求结果
return remoteQRCode(url, jsonParam);
}
/**
* 获取微信唯一凭证
*
* @param code 代码
* @return {@link String}
*/
private String getOpenId(String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session";
Map<String, Object> map = new HashMap<>();
map.put("appId", WechatLoginConfig.appId);
map.put("secret", WechatLoginConfig.secret);
map.put("js_code", code);
map.put("grant_type", "authorization_code");
String post = HttpUtil.post(url, map);
log.info("微信返回: {}", post);
JSONObject obj = JSONUtil.parseObj(post);
String openid = obj.getStr("openid");
if (StringUtils.isNoneBlank(openid)) {
return openid;
}
throw new RuntimeException("临时登录凭证错误");
}
/**
* 发送post请求
*
* @param url 请求地址
* @param jsonParam 请求参数
* @return 响应流
*/
private String remoteQRCode(String url, JSONObject jsonParam) {
try {
// 发送POST请求
byte[] responseBytes;
try (cn.hutool.http.HttpResponse response = HttpUtil.createPost(url)
.body(jsonParam.toString())
.header("Content-Type", "application/json")
.execute()) {
// 检查响应状态
if (!response.isOk()) {
log.error("[remoteQRCode] 请求失败,状态码: {}", response.getStatus());
throw new RuntimeException("暂未支持小程序扫码登录!");
}
responseBytes = response.bodyBytes();
}
// 尝试解析为JSON,判断是否为错误响应
String responseStr = new String(responseBytes, StandardCharsets.UTF_8);
if (JSONUtil.isTypeJSON(responseStr)) {
log.error("[remoteQRCode] 微信接口返回错误: {}", responseStr);
throw new RuntimeException("暂未支持小程序扫码登录!");
}
// 保存图片到文件中以确认图片是否损坏
// Files.write(Paths.get("output.png"), imageBytes);
String base64 = Base64.encodeBase64String(responseBytes);
// 返回带有 MIME 类型的 Base64 字符串
return "data:image/png;base64," + base64;
} catch (Exception e) {
log.error("获取小程序二维码异常", e);
throw new RuntimeException("暂未支持小程序扫码登录!");
}
}
/**
* 获取access_token 2小时
* <a href="https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html">...</a>
*
* @return access_token
*/
private String getAccessToken() {
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}";
String body = HttpUtil.get(StrUtil.format(url, WechatLoginConfig.appId, WechatLoginConfig.secret));
JSONObject object = JSONUtil.parseObj(body);
return object.getStr("access_token");
}
我们获取到的 AccessToken 它是有过期时间的, 我们这样子反复去重新拉取微信 TOKEN 可能会拉黑名单一段时间,所以我们需要缓存一下, 后续扫码业务也需要用到缓存, 避免资源浪费
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.37.0</version>
</dependency>
修改 Yml 配置文件填入 redis 配置参数
--- # redis 配置
spring:
data:
redis:
# 地址
host: 127.0.0.1
# 端口,默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password: '123123123123..'
# 连接超时时间
timeout: 10s
ssl:
enabled: false
# 分布式redis配置
redisson:
# redis key前缀
keyPrefix: xcx_qrcode
# 线程池数量
threads: 4
# Netty线程池数量
nettyThreads: 8
# 单节点配置
singleServerConfig:
# 客户端名称
clientName: yangbuyiya
# 最小空闲连接数
connectionMinimumIdleSize: 8
# 连接池大小
connectionPoolSize: 32
# 连接空闲超时,单位:毫秒
idleConnectionTimeout: 10000
# 命令等待超时,单位:毫秒
timeout: 3000
# 发布和订阅连接池大小
subscriptionConnectionPoolSize: 50
接着在 config 目录下 创建 redis 文件夹
/*
* 您可以更改此项目但请不要删除作者署名谢谢,否则根据中华人民共和国版权法进行处理.
* You may change this item but please do not remove the author's signature,
* otherwise it will be dealt with according to the Copyright Law of the People's Republic of China.
* yangbuyi Copyright (c) https://yby6.com 2024.
*/
package com.yby6.tencentwechatappletloginproject.config.redis;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.NameMapper;
/**
* redis缓存key前缀处理
*
* @author 杨不易呀
* Create By 2024/11/27
*/
public class KeyPrefixHandler implements NameMapper {
private final String keyPrefix;
public KeyPrefixHandler(String keyPrefix) {
//前缀为空 则返回空前缀
this.keyPrefix = StringUtils.isBlank(keyPrefix) ? "" : keyPrefix + ":";
}
/**
* 增加前缀
*/
@Override
public String map(String name) {
if (StringUtils.isBlank(name)) {
return null;
}
if (StringUtils.isNotBlank(keyPrefix) && !name.startsWith(keyPrefix)) {
return keyPrefix + name;
}
return name;
}
/**
* 去除前缀
*/
@Override
public String unmap(String name) {
if (StringUtils.isBlank(name)) {
return null;
}
if (StringUtils.isNotBlank(keyPrefix) && name.startsWith(keyPrefix)) {
return name.substring(keyPrefix.length());
}
return name;
}
}
/*
* 您可以更改此项目但请不要删除作者署名谢谢,否则根据中华人民共和国版权法进行处理.
* You may change this item but please do not remove the author's signature,
* otherwise it will be dealt with according to the Copyright Law of the People's Republic of China.
* yangbuyi Copyright (c) https://yby6.com 2024.
*/
package com.yby6.tencentwechatappletloginproject.config.redis;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.redisson.config.ReadMode;
import org.redisson.config.SubscriptionMode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Redisson 配置属性
*
* @author 杨不易呀
*/
@Data
@Component
@ConfigurationProperties(prefix = "redisson")
public class RedissonProperties {
/**
* redis缓存key前缀
*/
private String keyPrefix;
/**
* 线程池数量,默认值 = 当前处理核数量 * 2
*/
private int threads;
/**
* Netty线程池数量,默认值 = 当前处理核数量 * 2
*/
private int nettyThreads;
/**
* 单机服务配置
*/
private SingleServerConfig singleServerConfig;
/**
* 集群服务配置
*/
private ClusterServersConfig clusterServersConfig;
@Data
@NoArgsConstructor
public static class SingleServerConfig {
/**
* 客户端名称
*/
private String clientName;
/**
* 最小空闲连接数
*/
private int connectionMinimumIdleSize;
/**
* 连接池大小
*/
private int connectionPoolSize;
/**
* 连接空闲超时,单位:毫秒
*/
private int idleConnectionTimeout;
/**
* 命令等待超时,单位:毫秒
*/
private int timeout;
/**
* 发布和订阅连接池大小
*/
private int subscriptionConnectionPoolSize;
}
@Data
@NoArgsConstructor
public static class ClusterServersConfig {
/**
* 客户端名称
*/
private String clientName;
/**
* master最小空闲连接数
*/
private int masterConnectionMinimumIdleSize;
/**
* master连接池大小
*/
private int masterConnectionPoolSize;
/**
* slave最小空闲连接数
*/
private int slaveConnectionMinimumIdleSize;
/**
* slave连接池大小
*/
private int slaveConnectionPoolSize;
/**
* 连接空闲超时,单位:毫秒
*/
private int idleConnectionTimeout;
/**
* 命令等待超时,单位:毫秒
*/
private int timeout;
/**
* 发布和订阅连接池大小
*/
private int subscriptionConnectionPoolSize;
/**
* 读取模式
*/
private ReadMode readMode;
/**
* 订阅模式
*/
private SubscriptionMode subscriptionMode;
}
}
/*
* 您可以更改此项目但请不要删除作者署名谢谢,否则根据中华人民共和国版权法进行处理.
* You may change this item but please do not remove the author's signature,
* otherwise it will be dealt with according to the Copyright Law of the People's Republic of China.
* yangbuyi Copyright (c) https://yby6.com 2024.
*/
package com.yby6.tencentwechatappletloginproject.config.redis;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* redis配置
*
* @author 杨不易呀
*/
@Slf4j
@Configuration
@EnableCaching
@EnableConfigurationProperties(RedissonProperties.class)
public class RedisConfig {
@Autowired
private RedissonProperties redissonProperties;
@Autowired
private ObjectMapper objectMapper;
@Bean
public RedissonAutoConfigurationCustomizer redissonCustomizer() {
return config -> {
config.setThreads(redissonProperties.getThreads())
.setNettyThreads(redissonProperties.getNettyThreads())
.setCodec(new JsonJacksonCodec(objectMapper));
RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig();
if (ObjectUtil.isNotNull(singleServerConfig)) {
// 使用单机模式
config.useSingleServer()
//设置redis key前缀
.setNameMapper(new KeyPrefixHandler(redissonProperties.getKeyPrefix()))
.setTimeout(singleServerConfig.getTimeout())
.setClientName(singleServerConfig.getClientName())
.setIdleConnectionTimeout(singleServerConfig.getIdleConnectionTimeout())
.setSubscriptionConnectionPoolSize(singleServerConfig.getSubscriptionConnectionPoolSize())
.setConnectionMinimumIdleSize(singleServerConfig.getConnectionMinimumIdleSize())
.setConnectionPoolSize(singleServerConfig.getConnectionPoolSize());
}
// 集群配置方式 参考下方注释
RedissonProperties.ClusterServersConfig clusterServersConfig = redissonProperties.getClusterServersConfig();
if (ObjectUtil.isNotNull(clusterServersConfig)) {
config.useClusterServers()
//设置redis key前缀
.setNameMapper(new KeyPrefixHandler(redissonProperties.getKeyPrefix()))
.setTimeout(clusterServersConfig.getTimeout())
.setClientName(clusterServersConfig.getClientName())
.setIdleConnectionTimeout(clusterServersConfig.getIdleConnectionTimeout())
.setSubscriptionConnectionPoolSize(clusterServersConfig.getSubscriptionConnectionPoolSize())
.setMasterConnectionMinimumIdleSize(clusterServersConfig.getMasterConnectionMinimumIdleSize())
.setMasterConnectionPoolSize(clusterServersConfig.getMasterConnectionPoolSize())
.setSlaveConnectionMinimumIdleSize(clusterServersConfig.getSlaveConnectionMinimumIdleSize())
.setSlaveConnectionPoolSize(clusterServersConfig.getSlaveConnectionPoolSize())
.setReadMode(clusterServersConfig.getReadMode())
.setSubscriptionMode(clusterServersConfig.getSubscriptionMode());
}
log.info("初始化 redis 配置");
};
}
/**
* redis集群配置 yml
*
* --- # redis 集群配置(单机与集群只能开启一个另一个需要注释掉)
* spring:
* redis:
* cluster:
* nodes:
* - 192.168.0.100:6379
* - 192.168.0.101:6379
* - 192.168.0.102:6379
* # 密码
* password:
* # 连接超时时间
* timeout: 10s
* # 是否开启ssl
* ssl: false
*
* redisson:
* # 线程池数量
* threads: 16
* # Netty线程池数量
* nettyThreads: 32
* # 集群配置
* clusterServersConfig:
* # 客户端名称
* clientName: ${ruoyi.name}
* # master最小空闲连接数
* masterConnectionMinimumIdleSize: 32
* # master连接池大小
* masterConnectionPoolSize: 64
* # slave最小空闲连接数
* slaveConnectionMinimumIdleSize: 32
* # slave连接池大小
* slaveConnectionPoolSize: 64
* # 连接空闲超时,单位:毫秒
* idleConnectionTimeout: 10000
* # 命令等待超时,单位:毫秒
* timeout: 3000
* # 发布和订阅连接池大小
* subscriptionConnectionPoolSize: 50
* # 读取模式
* readMode: "SLAVE"
* # 订阅模式
* subscriptionMode: "MASTER"
*/
}
然后启动项目查看是否加载成功
如果无法启动请检查配置是否正确
/**
* 获取access_token 2小时
* <a href="https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html">...</a>
*
* @return access_token
*/
public String getAccessToken() {
// 存在缓存TOKEN继续使用
String accessToken = RedisUtils.getCacheObject(GlobalConstants.WECHAT_AUTH_TOKEN + WechatLoginConfig.appId);
if (StrUtil.isNotBlank(accessToken)) {
return accessToken;
}
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}";
String body = HttpUtil.get(StrUtil.format(url, WechatLoginConfig.appId, WechatLoginConfig.secret));
JSONObject object = JSONUtil.parseObj(body);
accessToken = object.getStr("access_token");
long expiresIn = Long.parseLong(object.getStr("expires_in"));
// 设置过期时间 expiresIn 秒
RedisUtils.setCacheObject(GlobalConstants.WECHAT_AUTH_TOKEN + WechatLoginConfig.appId,
accessToken, Duration.ofSeconds(expiresIn));
return accessToken;
}
在前面我们已经完成了生成小程序码、获取 token、在生成小程序码的时候我们需要传递一个 场景值(唯一 ID)
我们需要生成提供唯一的 ID 放入二维码当中,当扫码的时候小程序拿到的就是 scene=xxxxxx 这样子就知道当前是谁在扫码, ID 生成策略我这里使用 Hutool 工具当中 IdUtil
的Snowflake ID 来确保唯一性,当然你也可以增加一些时间戳签名等.
接着我们得要设计扫码的状态, 在前面的整体业务解析我解析了我们的业务
前端需要短轮训查询二维码的扫码状态, 我们需要定义一下各个阶段的状态, 并且状态对应不同的接口.
wait
scanned
close
共用取消状态,前端超时后主动发起取消接口
success
那么我们就要定义一个状态枚举
完善小程序码生成, 在生成二维码时持久化二维码状态, 设计规则:
接着根据代码规范我们得要为 Key 设置一个缓存前缀方便查看缓存知道是什么
创建 constants
常量目录 新增 GlobalConstants
类, 分别定义扫码状态、微信 accessToken 上面的修改一下.
package com.yby6.tencentwechatappletloginproject.constant;
/**
* 全局常量
*
* @author Yang Shuai
* Create By 2024/11/28
*/
public interface GlobalConstants {
/**
* 微信登录状态
*/
String WECHAT_LOGIN_STATE = "wechat_login_state:";
/**
* 微信授权TOKEN
*/
String WECHAT_AUTH_TOKEN = "wechat_auth_token:";
}
修改createQrCodeLogin 方法编写生成二维码逻辑
返回 qrcode
和 scene
提供前端渲染
启动后台使用 api 调试工具对其进行发起请求
可以看到数据成功返回, 复制这个 Base64 浏览器访问是否可以正常显示, 不可以则检查代码是否报错.
可以看到成功打入缓存, 过期时间设置为一分钟
上面就完成了接口调用生成小程序二维码,写下来我们编写其他不同状态下的接口.
扫码接口就很简单了, 前面解析了整个业务, 当用户扫码后会跳转到我们的小程序, 那么在小程序加载的时候获取到二维码当中的 scene 使用 场景值来发起接口调用更新为扫码成功状态.
接口设计 Restful: userScanQrcode/{scene}/applet
编写接口 userScanQrcode
方法
修改 AppletAuthService
服务实现扫码接口业务
思考? 扫码我们需要判断一些东西?
在这里可以校验改二维码生成时的 IP、和当前二维码的状态必须是 wait 否则都是异常操作. ip 我们前面没记录那么就不操作了感兴趣的同学可以极速操作一下. 那么代码如下:
先校验是否为 null 是那么表示取消了或者超时缓存过期了, 用户刚刚好那一时刻扫码, 为了严谨我们是不允许通过的. 那么在判断是否为刚开始默认的状态,变更那么表示存在恶意访问!
最后就直接更新缓存为 scanned
状态 并且过期时间不变
/**
* 修改扫码状态为 已扫码
*
* @param scene 场景
* @return {@code R<Void> }
*/
public R<Void> userScanQrcode(String scene) {
// 1.先判断本次请求是否合法 || 判断状态是否为 wait
String state = RedisUtils.getCacheObject(GlobalConstants.WECHAT_LOGIN_STATE + scene);
if (null == scene || !ScanStatus.WAIT.getStatus().equals(state)) {
return R.fail("非法操作,请勿重复扫码!");
}
// 2.修改状态为 已扫码
RedisUtils.setCacheObject(GlobalConstants.WECHAT_LOGIN_STATE + scene,
ScanStatus.SCANNED.getStatus(),
true);
return R.ok();
}
简单测试一下执行生成二维码, 把生成的 scene 参数拿过来访问观察环境的状态是否变更
扫码接口完毕那么就继续编写个简单的, 取消授权/超时授权接口.
这个就非常简单啦, 当扫码成功跳转到小程序授权页面的时候页面有两个按钮一个是授权一个取消授权如下图, 那么点击取消授权主动发起请求取消本次扫码状态更新为 close
直接
接口设计 Restful: userCancelAuth/{scene}/applet
编写接口 userCancelAuth
方法
修改 AppletAuthService
服务实现扫码接口业务
首先要判断一下是否还存在, 否则就不走后面代码逻辑了, 避免资源浪费
/**
* 用户取消认证
*
* @param scene 场景值
* @return {@code R<Void> }
*/
public R<Void> userCancelAuth(String scene) {
// 1.判断是否存在 避免走后面的逻辑了
String state = RedisUtils.getCacheObject(GlobalConstants.WECHAT_LOGIN_STATE + scene);
if (null == scene) {
return R.ok();
}
// 2.存在就更新缓存为关闭扫码
RedisUtils.setCacheObject(GlobalConstants.WECHAT_LOGIN_STATE + scene,
ScanStatus.CANCEL.getStatus(),
true);
return R.ok();
}
简单测试一下, 调用生成二维码接口, 拿到 scene 参数去主动取消观察缓存信息
接下来就是重中之重的业务, 用户授权扫码.
思考, 那么授权成功接口我们应该注意些什么呢?
在授权的时候, 顾名思义我们要向用户索要授权信息, 拿到是哪个微信用户进行的授权信息, 那么微信就提供了一个 openId
微信用户唯一 ID 字段, 通过此字段我们就可以绑定现有系统当中的用户表或者新增该用户.
那么如何获取到 OpenId 我们看看文档 点我前往
可以看到文档的描述, 需要调用小程序内置的 login 接口函数获取临时凭证 code
码, 根据这个 Code 码我们就可以获取到该用户的 OpenID,
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
需要传递四个参数 小程序 appId、小程序 appSecret、 code、 授权类型
需要四个参数这些我们都有直接编写代码吧简简单单的.切菜一样
属性 | 类型 | 必填 | 说明 |
---|---|---|---|
appid | string | 是 | 小程序 appId |
secret | string | 是 | 小程序 appSecret |
js_code | string | 是 | 登录时获取的 code,可通过wx.login 获取 |
grant_type | string | 是 | 授权类型,此处只需填写 authorization_code |
编写单元测试, 直接使用 Hutool 工具发送请求 code 码我们现在还拿不到我们身边设置观察报错信息, 调用成功即可
@Test
public void getOpenId () {
String url = "https://api.weixin.qq.com/sns/jscode2session";
Map<String, Object> map = new HashMap<>();
map.put("appId", appId);
map.put("secret", secret);
map.put("js_code", "这里就是小程序登录的code");
map.put("grant_type", "authorization_code");
String post = HttpUtil.post(url, map);
log.info("微信返回: {}", post);
JSONObject obj = JSONUtil.parseObj(post);
String openid = obj.getStr("openid");
}
编写好后直接运行测试, 可以看到调用成功, 返回了 Code 错误的问题
修改 AppletAuthService
服务, 新增 getOpenId 接口
/**
* 获取微信唯一凭证
*
* @param code 代码
* @return {@link String}
*/
private String getOpenId(String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session";
Map<String, Object> map = new HashMap<>();
map.put("appId", WechatLoginConfig.appId);
map.put("secret", WechatLoginConfig.secret);
map.put("js_code", code);
map.put("grant_type", "authorization_code");
String post = HttpUtil.post(url, map);
log.info("微信返回: {}", post);
JSONObject obj = JSONUtil.parseObj(post);
String openid = obj.getStr("openid");
if (StringUtils.isNoneBlank(openid)) {
return openid;
}
throw new RuntimeException("临时登录凭证错误");
}
接口设计 restful: userAuthWebPro/{scene}/{code}/applet
传递场景值和 code 码
编写 userAuthWebPro
方法
/**
* 用户点击确认授权
*
* @param scene 场景
* @param code 代码
* @return {@code R<Void> }
*/
@PostMapping("userAuthWebPro/{scene}/{code}/applet")
public R<Void> userAuthWebPro(@PathVariable String scene, @PathVariable String code) {
return appletAuthService.userAuthWebPro(scene,code);
}
修改 AppletAuthService
服务实现授权接口业务
还是老规矩, 首先要判断一下是否还存在, 否则就不走后面代码逻辑了, 避免资源浪费
然后根据小程序传递的 code
码去获取OpenId 这里就是做一些和你的系统绑定业务....我这里就简单记录一下当前登录人.
最后更改扫码状态为授权成功.
/**
* 用户点击确认授权
*
* @param scene 场景
* @param code 代码
* @return {@code R<Void> }
*/
public R<Void> userAuthWebPro(String scene, String code) {
// 1.判断是否存在 避免走后面的逻辑了
String status = RedisUtils.getCacheObject(GlobalConstants.WECHAT_LOGIN_STATE + scene);
// 只能是扫码状态进来授权否则都认定为异常操作
if (null == status || !status.equals(ScanStatus.SCANNED.getStatus())) {
return R.fail("扫码超时,请重新扫码授权!");
}
// 2.获取OpenId 这里就是做一些和你的系统绑定业务....我这里就简单记录一下当前登录人
String openId = getOpenId(code);
RedisUtils.setCacheObject(GlobalConstants.WECHAT_AUTH_INFO, openId);
// 3.修改扫码状态为 已授权 提供前端校验
RedisUtils.setCacheObject(GlobalConstants.WECHAT_LOGIN_STATE + scene,
ScanStatus.SUCCESS.getStatus(), true);
return R.ok();
}
/**
* 微信授权信息
*/
String WECHAT_AUTH_INFO = "wechat_auth_INFO:";
在上面我们已经将扫码的授权功能后端接口全部开发完毕, 接下来就是最后一个接口提供前端查询扫码状态信息,来判断页面的渲染.
接口设计 Restful: userAuthStatus/{scene}/applet
编写接口 userAuthStatus
方法
检测到success直接删除并且查询到登录信息传递给前端即可, 前端就可以根据 result === success 表示成功那么就显示 cacheObject 的信息参数.
/**
* 浏览器查询 当前二维码状态
*
* @param scene 场景值
* @return {@code R<String> }
*/
@GetMapping("userAuthStatus/{scene}/applet")
public R<Object> userAuthStatus(@PathVariable String scene) {
String result = RedisUtils.getCacheObject(GlobalConstants.WECHAT_LOGIN_STATE + scene);
// 检测到success直接删除并且传递给调用方
if (null != result && result.equals(ScanStatus.SUCCESS.getStatus())) {
RedisUtils.deleteObject(scene);
// 获取到登录信息
Object cacheObject = RedisUtils.getCacheObject(GlobalConstants.WECHAT_AUTH_INFO);
return R.ok(result, cacheObject);
}
return R.ok(result);
}
调用生成二维码, 然后去调用查询状态接口中途调用取消接口观察查询到的数据是什么.
可以看到初始化 wait 接下来我调用取消接口
ok 没有任何问题, 接下来就可以接入前端实现扫码效果.
在我们的服务当中, 这几个方法是不是可以抽出来?
如果以后有新的授权方式可能需要用到并且还解耦合防止工具代码存在业务当中不太好看
所以我们得要抽象出来就行, 就整洁许多.
修改 AppletAuthService
服务抽象工具代码
/*
* 您可以更改此项目但请不要删除作者署名谢谢,否则根据中华人民共和国版权法进行处理.
* You may change this item but please do not remove the author's signature,
* otherwise it will be dealt with according to the Copyright Law of the People's Republic of China.
* yangbuyi Copyright (c) https://yby6.com 2024.
*/
package com.yby6.tencentwechatappletloginproject.service.base;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.yby6.tencentwechatappletloginproject.constant.GlobalConstants;
import com.yby6.tencentwechatappletloginproject.config.WechatLoginConfig;
import com.yby6.tencentwechatappletloginproject.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* 抽象小程序服务
*
* @author 杨不易呀
* Create By 2024/11/26
*/
@Slf4j
public abstract class AbstractAppletService {
/**
* 带参数无限个数小程序码接口
* <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/qr-code/getUnlimitedQRCode.html">...</a>
*
* @param accessToken 访问令牌
* @param scene 场景
* @return {@code InputStream }
*/
public String getWechatQrCode(String accessToken, String scene) {
String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken;
JSONObject jsonParam = new JSONObject();
// 封装请求对象
// scene值
jsonParam.set("scene", scene);
// 跳往的小程序页面,一般为认证界面
jsonParam.set("page", WechatLoginConfig.authPage);
// 图片宽度,默认为 430
jsonParam.set("width", WechatLoginConfig.width);
// 检测页面是否存在,默认为 true
jsonParam.set("check_path", WechatLoginConfig.checkPath);
// 返回请求结果
return remoteQRCode(url, jsonParam);
}
/**
* 获取微信唯一凭证
*
* @param code 代码
* @return {@link String}
*/
public String getOpenId(String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session";
Map<String, Object> map = new HashMap<>();
map.put("appId", WechatLoginConfig.appId);
map.put("secret", WechatLoginConfig.secret);
map.put("js_code", code);
map.put("grant_type", "authorization_code");
String post = HttpUtil.post(url, map);
log.info("微信返回: {}", post);
JSONObject obj = JSONUtil.parseObj(post);
String openid = obj.getStr("openid");
if (StringUtils.isNoneBlank(openid)) {
return openid;
}
throw new RuntimeException("临时登录凭证错误");
}
/**
* 发送post请求
*
* @param url 请求地址
* @param jsonParam 请求参数
* @return 响应流
*/
public String remoteQRCode(String url, JSONObject jsonParam) {
try {
// 发送POST请求
byte[] responseBytes;
try (cn.hutool.http.HttpResponse response = HttpUtil.createPost(url)
.body(jsonParam.toString())
.header("Content-Type", "application/json")
.execute()) {
// 检查响应状态
if (!response.isOk()) {
log.error("[remoteQRCode] 请求失败,状态码: {}", response.getStatus());
throw new RuntimeException("暂未支持小程序扫码登录!");
}
responseBytes = response.bodyBytes();
}
// 尝试解析为JSON,判断是否为错误响应
String responseStr = new String(responseBytes, StandardCharsets.UTF_8);
if (JSONUtil.isTypeJSON(responseStr)) {
log.error("[remoteQRCode] 微信接口返回错误: {}", responseStr);
throw new RuntimeException("暂未支持小程序扫码登录!");
}
// 保存图片到文件中以确认图片是否损坏
// Files.write(Paths.get("output.png"), imageBytes);
String base64 = Base64.encodeBase64String(responseBytes);
// 返回带有 MIME 类型的 Base64 字符串
return "data:image/png;base64," + base64;
} catch (Exception e) {
log.error("获取小程序二维码异常", e);
throw new RuntimeException("暂未支持小程序扫码登录!");
}
}
/**
* 获取access_token 2小时
* <a href="https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html">...</a>
*
* @return access_token
*/
public String getAccessToken() {
// 存在缓存TOKEN继续使用
String accessToken = RedisUtils.getCacheObject(GlobalConstants.WECHAT_AUTH_TOKEN + WechatLoginConfig.appId);
if (StrUtil.isNotBlank(accessToken)) {
return accessToken;
}
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}";
String body = HttpUtil.get(StrUtil.format(url, WechatLoginConfig.appId, WechatLoginConfig.secret));
JSONObject object = JSONUtil.parseObj(body);
accessToken = object.getStr("access_token");
long expiresIn = Long.parseLong(object.getStr("expires_in"));
// 设置过期时间29分钟
RedisUtils.setCacheObject(GlobalConstants.WECHAT_AUTH_TOKEN + WechatLoginConfig.appId, accessToken, Duration.ofSeconds(expiresIn));
return accessToken;
}
}
接着集成一下抽象类即可
这样子一看代码就清晰许多~
那么到这里我们的后端业务差不多就已经完成了, 是不是很简单, 后面就都是前端的知识点啦, 冲冲冲!!!
本期结束咱们下次再见👋~
🌊 关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。