😊 @ 作者: 一恍过去
💖 @ 主页: https://blog.csdn.net/zhuocailing3390
🎊 @ 社区: Java技术栈交流
🎉 @ 主题: 微信公众号开发—扫描二维码实现登录方案
⏱️ @ 创作时间: 2022年12月21日
目录
- 1、准备工作
- 2、二维码扫码登录说明
- 3、yaml配置
- 4、定义工具类
- 5、扫描带参数二维码事件
- 6、扫码登录流程示例代码
1、准备工作
1、调用微信公众号接口,需要实现获取AccessToken
,参考《获取AccessToken接口调用凭据》
2、在本地进行联调时,为让微信端能够访问到本地服务,需要进行内网穿透,参考《本地服务器内网穿透实现(NATAPP)》
3、配置微信接口配置信息
,用于告诉微信接收消息的回调地址
2、二维码扫码登录说明
- 前端调用接口,后端生成
二维码
以及一个loginId
参数(就是一个微信扫码场景值 sceneStr,loginId存入redis中,时效为5分钟); - 同时前端通过loginId进行轮询访问判断是否成功登录
- 如果扫码后,用户没有进行任何绑定,则直接回复用户消息并且附带上进行绑定的引导链接(绑定操作就是一个前端页面,进入页面后用户需要同意微信网页授权、并且输入手机号、短信验证码,后端将授权后得到的code查询到openId,最后将手机号与openId进行绑定),绑定操作参考《微信网页授权自动登录业务系统》;
- 如果扫码后,loginId已过期则要求用户重新刷新二维码,并且只要是授权不成功都需要重新刷新二维码再次执行授权流程;
- 如果扫码后 发现以及已经绑定了openid则授权登录成功,将用户信息及token信息通过轮询接口返回到前端,并且删除loginId值;
3、yaml配置
wx:
# 来源于测试平台
appid: wx79ec4331f29311b9
secret: 1c79a199560f94096f26b8caa2a73a08
apiUrl: https://api.weixin.qq.com/
openApiUrl: https://open.weixin.qq.com/
authRedirectUri: http://6uks3d.natappfree.cc/wechat/auth
4、定义工具类
MapUtils:
public class MapUtils {
/**
* Map转换为 Entity
*
* @param params 包含参数的Map
* @param t 需要赋值的实体
* @param <T> 类型
*/
public static <T> T mapToEntity(Map<String, Object> params, T t) {
if (null == params) {
return t;
}
Class<?> clazz = t.getClass();
Field[] declaredFields = clazz.getDeclaredFields();
try {
for (Field declaredField : declaredFields) {
declaredField.setAccessible(true);
String name = declaredField.getName();
if (null != params.get(name)) {
declaredField.set(t, params.get(name));
}
}
} catch (Exception e) {
throw new RuntimeException("属性设置失败!");
}
return t;
}
/**
* 将对象转换为HashMap
*
* @param t 转换为Map的对象
* @param <T> 转换为Map的类
* @return Map
*/
public static <T> Map<String, Object> entityToMap(T t) {
Class<?> clazz = t.getClass();
List<Field> allField = getAllField(clazz);
Map<String, Object> hashMap = new LinkedHashMap<>(allField.size());
try {
for (Field declaredField : allField) {
declaredField.setAccessible(true);
Object o = declaredField.get(t);
if (null != o) {
hashMap.put(declaredField.getName(), o);
}
}
} catch (Exception e) {
throw new RuntimeException("属性获取失败!");
}
return hashMap;
}
/**
* 获取所有属性
*
* @param clazz class
* @param <T> 泛型
* @return List<Field>
*/
public static <T> List<Field> getAllField(Class<T> clazz) {
List<Field> fields = new ArrayList<>();
Class<?> superClazz = clazz;
while (null != superClazz) {
fields.addAll(Arrays.asList(superClazz.getDeclaredFields()));
superClazz = superClazz.getSuperclass();
}
return fields;
}
/**
* 将Map参数转换为字符串
*
* @param map
* @return
*/
public static String mapToString(Map<String, Object> map) {
StringBuffer sb = new StringBuffer();
map.forEach((key, value) -> {
sb.append(key).append("=").append(value.toString()).append("&");
});
String str = sb.toString();
str = str.substring(0, str.length() - 1);
return str;
}
/**
* 将Bean对象转换Url请求的字符串
*
* @param t
* @param <T>
* @return
*/
public static <T> String getUrlByBean(T t) {
String pre = "?";
Map<String, Object> map = entityToMap(t);
return pre + mapToString(map);
}
}
5、扫描带参数二维码事件
@Service
@Slf4j
public class MessageService {
@Resource
private ScanAuthService scanAuthService;
public String receiveAndResponseMessage(HttpServletRequest request) {
log.info("------------微信消息开始处理-------------");
try {
// 调用消息工具类MessageUtil解析微信发来的xml格式的消息,解析的结果放在HashMap里;
Map<String, String> map = WeixinMessageUtil.xmlToMap(request);
String jsonString = JSON.toJSONString(map);
// 发送方账号(用户方)
String fromUserName = map.get("FromUserName");
// 接受方账号(公众号)
String toUserName = map.get("ToUserName");
// 消息类型
String msgType = map.get("MsgType");
log.info("fromUserName is:" + fromUserName + " toUserName is:" + toUserName + " msgType is:" + msgType);
// 事件类型
String eventType = map.get("Event");
if (eventType.equals(MessageType.EVENT_TYPE_SCAN)) {
BaseEvent message = JSON.parseObject(jsonString, BaseEvent.class);
log.info("扫码成功 {}", message);
// 事件 KEY 值,qrscene_为前缀,后面为二维码的参数值
String eventKey = map.get("EventKey");
String openId = message.getFromUserName();
// 进行后续的授权处理
if (!StringUtils.isEmpty(eventKey)) {
scanAuthService.getAuth(openId, eventKey);
}
}
} catch (Exception e) {
e.printStackTrace();
log.error("系统出错");
return null;
}
}
}
6、扫码登录流程示例代码
扫码请求实体类定义:
@Data
public class QrCodeRep {
/**
* 该二维码有效时间,以秒为单位。 最大不超过2592000(即30天),此字段如果不填,则默认有效期为60秒
*/
private Integer expire_seconds;
/**
* 二维码类型
* QR_SCENE为临时的整型参数值,QR_STR_SCENE为临时的字符串参数值
* QR_LIMIT_SCENE为永久的整型参数值,QR_LIMIT_STR_SCENE为永久的字符串参数值
*/
private String action_name;
/**
* 二维码详细信息
*/
private ActionInfo action_info;
@Data
public static class ActionInfo {
private Scene scene;
}
@Data
public static class Scene {
/**
* 场景值ID(字符串形式的ID),字符串类型,长度限制为1到64
*/
private String scene_str;
}
}
扫码请求响应类定义:
@Data
public class QrCodeRes {
/**
* 获取的二维码ticket,凭借此 ticket 可以在有效时间内换取二维码。
*/
private String ticket;
/**
* 通过ticket换取的二维码
*/
private String ticketUrl;
/**
* 该二维码有效时间,以秒为单位。 最大不超过2592000(即30天)
*/
private Integer expire_seconds;
/**
* 二维码图片解析后的地址,开发者可根据该地址自行生成需要的二维码图片
*/
private String url;
/**
* 场景值ID(字符串形式的ID),字符串类型,长度限制为1到64
*/
private String sceneStr;
}
Service层:
@Slf4j
@Service
public class ScanAuthService {
@Resource
private WeChantService weChantService;
@Resource
private RestHttpRequest restHttpRequest;
@Resource
private UserInfoMapper userInfoMapper;
@Resource
private WxBean wxBean;
public String getAuth(String openId, String loginId) {
String msg = "扫码登录成功!";
// 如果扫码后,loginId已过期则要求用户重新刷新二维码,并且只要授权不成功都需要重新刷新二维码再次执行授权流程
if (!RedisUtils.hasKey(loginId)) {
msg = "扫码登录已过期,请重新刷新二维码!";
}
Object o = RedisUtils.get(loginId);
if (o == null) {
msg = "扫码登录已过期,请重新刷新二维码!";
} else {
ScanStatusAuthRes authRes = JSON.parseObject(o.toString(), ScanStatusAuthRes.class);
UserInfo userInfo = userInfoMapper.selectByOpenId(openId);
if (userInfo == null) {
// 如果扫码后,如果没有进行任何绑定,则直接回复用户消息并且附带上进行绑定的链接
msg = "<a href =\"https://www.baidu.com/\">请前往绑定</a>";
RedisUtils.del(loginId);
}
// 如果扫码后 发现以及已经绑定了openid则授权登录成功,将用户信息及token信息通过轮询接口返回到前端,并且删除loginId值
authRes.setStatus(2);
authRes.setUserInfo(userInfo);
long expire = RedisUtils.getExpire(loginId);
RedisUtils.setEx(loginId, JSON.toJSONString(authRes), expire, TimeUnit.SECONDS);
}
return msg;
}
public PageResult qrcodeCreate() {
String url = wxBean.getApiUrl() + InterfaceConstant.QR_CODE_CREATE;
BaseRep rep = new BaseRep();
rep.setAccess_token(weChantService.getAccessToken());
url = url + MapUtils.getUrlByBean(rep);
QrCodeRep codeRep = new QrCodeRep();
codeRep.setAction_name("QR_STR_SCENE");
codeRep.setExpire_seconds(5 * 60);
QrCodeRep.ActionInfo actionInfo = new QrCodeRep.ActionInfo();
QrCodeRep.Scene scene = new QrCodeRep.Scene();
// 设置场景值
String loginId = UUIDUtils.getUuId();
scene.setScene_str(loginId);
actionInfo.setScene(scene);
codeRep.setAction_info(actionInfo);
Map map = restHttpRequest.doHttp(url, HttpMethod.POST, codeRep);
QrCodeRes res = new QrCodeRes();
MapUtils.mapToEntity(map, res);
res.setSceneStr(scene.getScene_str());
try {
// 通过ticket获取二维码
String ticket = res.getTicket();
String ticketUrl = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + URLEncoder.encode(ticket, "UTF-8");
res.setTicketUrl(ticketUrl);
} catch (
UnsupportedEncodingException e) {
e.printStackTrace();
}
// 将状态写入redis
ScanStatusAuthRes authRes = new ScanStatusAuthRes();
authRes.setStatus(1);
RedisUtils.setEx(loginId, JSON.toJSONString(authRes), res.getExpire_seconds(), TimeUnit.SECONDS);
return ResultUtils.success(res);
}
public PageResult scanStatus(String loginId) {
// 通过redis查询是否存在
if (!RedisUtils.hasKey(loginId)) {
return ResultUtils.fail("扫码登录已过期,请重新刷新二维码!");
}
Object o = RedisUtils.get(loginId);
ScanStatusAuthRes authRes = JSON.parseObject(o.toString(), ScanStatusAuthRes.class);
if (authRes.getStatus() == 2) {
// 删除key
RedisUtils.del(loginId);
}
return ResultUtils.success(authRes);
}
}
Controller层:
@Slf4j
@RestController
public class ScanAuthController {
@Resource
private ScanAuthService scanAuthService;
/**
* 获取扫码登录二维码
*
* @return
*/
@ApiOperation(value = "获取扫码登录二维码", notes = "获取扫码登录二维码")
@GetMapping("/scanLogin")
public PageResult qrcodeCreate() {
return ResultUtils.success(scanAuthService.qrcodeCreate());
}
/**
* 轮询获取登录状态
*
* @param loginId
* @return
*/
@ApiOperation(value = "轮询获取登录状态", notes = "轮询获取登录状态")
@GetMapping("/scanStatus")
public PageResult scanStatus(@RequestParam("loginId") String loginId) {
return ResultUtils.success(scanAuthService.scanStatus(loginId));
}
}