前言
- 开发的小程序手机号短信验证码登录这一个功能,入参只有手机号。
结局
- 盗刷、 恶意刷。
解决方案
1. nginx
只允许referer是小程序来源的请求
# 你的接口
location ^~ /api/
{
if ($http_referer !~* "^https://servicewechat.com/【你的小程序appid】/\d+/page-frame.html$") {
return 444;
}
2. 利用小程序code来进行验证
- 利用小程序code,在后端静默获取openid,根据openid结合当前时间戳,生成加密串返回给后端。
- 前端请求短信接口时需要将openid和生成的加密串传给后端。
- 后端解析加密串后,判断解密后的openid是否一致、时间戳是否有效。
加密和解密过程都在后端进行,避免微信小程序被抓包反编译后能够找到加密方法。
伪代码实现逻辑
- 前端
export default {
mounted() {
wx.login({
success: function(res) {
const {
code
} = res
if (code) {
// 后端code获取openid接口
codeToOpenid({
code
}).then(data => {
if (data) {
const { openid, cryptoKey } = data
that.openid = data.openid
that.cryptoKey = data.cryptoKey
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
});
},
methods: {
// 获取验证码
getCode() {
const checkPhone = /^1[3-9]\d{9}$/.test(this.phone)
if (!checkPhone) {
uni.showToast({
icon: 'none',
title: '请输入正确的手机号码'
})
return;
}
wx.showLoading({
title: '正在获取验证码'
})
// 获取手机号验证码接口
getPhoneCode({
phone: this.phone,
openid: this.openid,
cryptoKey: this.cryptoKey,
}).then((code) => {
if (code) {
uni.showToast({
title: '验证码获取成功'
})
}
})
}
}
}
- 后端
const rp = require('request-promise');
const CryptoJS = require("crypto-js");
const key = 'xxxxxxxxx'
const iv = '1234567887654321'
// 加密
export function Encrypt(text) {
return CryptoJS.AES.encrypt(text, CryptoJS.enc.Utf8.parse(key), {
iv: CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString()
}
// 解密
export function Decrypt(text) {
let decrypted = CryptoJS.AES.decrypt(text, CryptoJS.enc.Utf8.parse(key), {
iv: CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
})
return decrypted.toString(CryptoJS.enc.Utf8)
}
module.exports = {
async getOpenIdByCode(code) {
const secret = "xxxxxxx"
const apppid = "xxxxxxx"
const options = {
method: 'GET',
url: 'https://api.weixin.qq.com/sns/jscode2session',
qs: {
grant_type: 'authorization_code',
js_code: code,
secret,
appid,
}
};
return new Promise(async (resolve, reject) => {
try {
// 获取openid
const res = await rp(options);
const sessionData = JSON.parse(res);
if (sessionData.openid) {
resolve(sessionData)
} else {
throw sessionData;
}
} catch (e) {
reject(e)
}
})
}
// 后端code获取openid接口逻辑
async codeToOpenid() {
const code = this.post('code');
let openid;
try {
// 通过微信接口使用code获取openid
const { openid } = await this.getOpenIdByCode(code);
// 返回成功结果
return this.success({
openid,
cryptoKey: Encrypt(`${Date.now()}:${openid}`) // 加密内容为 当前时间戳:openid
});
} catch (e) {
// 返回失败结果
return this.fail(500, 'openid获取失败', e);
}
}
// 获取手机号验证码
async getPhoneCode() {
const openid = this.post('openid')
const cryptoKey = this.post('cryptoKey')
const phone = this.post('phone')
// 校验解密的cryptoKey是否和openid一致
const decryptStr = Decrypt(cryptoKey)
const [timestamp, decryptOpenid] = decryptStr.split(':')
if(decryptOpenid !== openid) {
return this.fail('无效凭证!')
}
const now = Date.now()
// 5min有效期
if( timestamp > now || (now - timestamp > 5 * 60 * 1000)) {
return this.fail('凭证过期')
}
const oldCode = await this.cache(phone); // 判断是否redis中已存在该手机号的验证码
if (oldCode) {
return this.fail('60秒内不可重复获取')
}
let code = randomCode() // 随机生成验证码
const smsSerivce = this.service('sms'); // 短信服务
const msg = { code }
try {
const res = await smsSerivce.send(phone, msg) // 发送短信验证码
} catch (e) {
return this.fail('短信发送失败')
}
// 存储redis中1分钟,避免重复请求
await this.cache(phone, code, { timeout: 60 * 1000 })
return this.success(code) // 返回验证码给前端
}
}
标签:openid,code,const,cryptoKey,微信,验证码,接口,return,CryptoJS
From: https://blog.csdn.net/weixin_41895625/article/details/140991520