前言

在做项目的过程中,遇到的一个业务场景。

前端使用微信小程序进行课堂扫码签到,后端接收签到请求,并且需要对于一些可能的作弊手段予以反制,比如一个人登录多个人账号帮他们签到,或者一部手机切换不同微信进行签到。

问题分析

想要限制签到作弊,需要的就是在用户签到时,对于一次签到,一个人,一个设备,进行唯一性标识,对于这部分重复的进行限制。

而如果需要唯一性标识,对于web页面来说也有cookie来实现,但是微信小程序很难找到一个可以标识设备唯一性的方案,微信官方也没有提供相关接口。

解决方案

阅读了微信小程序的相关开发文档之后,想出了一套解决方案。

首先,一个微信账号对于小程序会有一个唯一的OpenID

微信小程序登录流程

  1. 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
  2. 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key

之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

那么对于一次签到来说,限制OpenID的重复即可限制用户使用一个微信切换绑定不同账号完成签到,只需要给数据库增加一个字段,记录该次签到的OpenID即可。


切换绑定账号的问题就解决了,那么对于同一设备切换微信账号来说,微信小程序官方提供了一个接口可以获取设备的系统相关信息。

微信获取系统信息接口文档

wx.getSystemInfo(Object object)

功能描述:获取系统信息。

能够获取设备型号,设备信息,字体大小,系统设置等等相关的一系列信息。

而这些设备信息可以拼接起来并md5加密生成一个设备指纹来作为设备唯一标识,这样就可以得到一串长度固定的设备标识码deviceInfo

再加上获取签到请求的ip地址和deviceInfo拼接,就能得到小范围内基本不会出现重复的设备指纹deviceCode

PS:对于ip地址的获取,需要对真实ip进行过滤,否则获取到的可能是代理服务器的链路ip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 各种代理
*
* X-Forwarded-For:Squid服务代理
* Proxy-Client-IP:apache服务代理
* WL-Proxy-Client-IP:weblogic服务代理
* X-Real-IP:nginx服务代理
* HTTP_CLIENT_IP:有些代理服务器
*/
private static final String[] PROXYS = {"x-forwarded-for", "Proxy-Client-IP", "WL-Proxy-Client-IP", "X-Real-IP", "HTTP_CLIENT_IP"};
/**
* 获取客户端ip
*/
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;

try {
for (String proxy : PROXYS) {
ipAddress = request.getHeader(proxy);
if (StringUtils.isNotBlank(ipAddress) && !"unknown".equalsIgnoreCase(ipAddress)) {
return ipAddress;
}
}
if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
// "***.***.***.***".length() = 15
if (ipAddress != null && ipAddress.length() > 15) {
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
return ipAddress;
}

最终结论

微信小程序扫码签到业务,用OpenId来防止微信绑定不同账号,用设备指纹deviceCode来防止同一台设备一定范围内切换不同微信签到

deviceCode=md5(请求ip+deviceInfo)deviceCode = md5(请求ip+deviceInfo)

deviceInfo=md5(若干设备信息字段)deviceInfo = md5(若干设备信息字段)

生成设备信息deviceInfo所取的字段值越多越能准确标记一部设备的唯一性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 微信小程序端生成deviceInfo,去掉一些容易发生改变设备信息
// (微信的接口可能有更新,字段可能有变化)
wx.getSystemInfo({
success(res) {
delete(res.deviceOrientation)//设备方向
delete(res.enableDebug)//是否已打开调试。可通过右上角菜单或 wx.setEnableDebug 打开调试。
delete(res.wifiEnabled)//Wi-Fi 的系统开关
delete(res.bluetoothEnabled)//蓝牙的系统开关
delete(res.locationEnabled)//地理位置的系统开关

// 生成 deviceInfo设备信息
app.globalData.deviceInfo = hexMD5(JSON.stringify(res))
}
})
1
2
3
4
5
6
7
8
9
10
// 后端代码
public Result signIn(String openId, int signInId, HttpServletRequest request, String deviceInfo) {
// ***********************
// 省略业务代码
// ***********************

// 生成 deviceCode设备指纹
String deviceCode = md5(IpUtil.getIpAddr(request)+deviceInfo);
}

方案缺点

  1. 这不能作为长时间段下用户设备唯一性的标识,因为比如换个网络环境,ip地址就可能发生变化,只能作为小范围内标识唯一设备的方案,比如本文描述的课堂扫码签到场景
  2. 如果极端场景下,比如完全一模一样的两部手机,从型号品牌到各种设置都完全一致,并且处于机房局域网之类的导致请求ip相同的场景,会导致该方案出问题,没法标识唯一设备