1:背景:
对于小程序端或者其他端的ugc(用户产生的文本内容[文本、图片...])是需要加入内容的安全校验的。
参考链接(https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.imgSecCheck.html)
2:应用场景:
1: 文本
检查一段文本是否含有违法违规内容
用户个人资料违规文字检测
媒体新闻类用户发表文章,评论内容检测
游戏类用户编辑上传的素材等
2: 图片
校验一张图片是否含有违法违规内容
图片智能鉴黄
敏感人脸识别:用户头像;媒体类用户文章里的图片检测;社交类用户上传的图片检测等e
3: 接口对接(https):
3.1: config的配置
const REDIS = {
host: process.env.REDIS_HOST || 'www.exapmle.com',
port: process.env.REDIS_PORT || 'xxxx',
password: process.env.REDIS_PASS || 'xxxxxxxx'
}
const AppID = 'xxxxxxxxxxxx'
const AppSecret = 'xxxxxxxxxxxx'
export {
REDIS,
AppID,
AppSecret
}
3.2: redis的配置(redisConfig)
import redis from 'redis'
import { promisifyAll } from 'bluebird'
import config from './index'
const options = {
host: config.REDIS.host,
port: config.REDIS.port,
password: config.REDIS.password,
detect_buffers: true,
retry_strategy: function (options) {
if (options.error && options.error.code === 'ECONNREFUSED') {
return new Error('The server refused the connection')
}
if (options.total_retry_time > 1000 * 60 * 60) {
return new Error('Retry time exhausted')
}
if (options.attempt > 10) {
return undefined
}
return Math.min(options.attempt * 100, 3000)
}
}
let client = promisifyAll(redis.createClient(options))
client.on('error', (err) => {
console.log('Redis Client Error:' + err)
})
const setValue = (key, value, time) => {
if (!client.connected) {
client = promisifyAll(redis.createClient(options))
}
if (typeof value === 'undefined' || value == null || value === '') {
return
}
if (typeof value === 'string') {
if (typeof time !== 'undefined') {
client.set(key, value, 'EX', time, (err, result) => {
})
} else {
client.set(key, value)
}
} else if (typeof value === 'object') {
Object.keys(value).forEach((item) => {
})
}
}
const getValue = (key) => {
if (!client.connected) {
client = promisifyAll(redis.createClient(options))
}
return client.getAsync(key)
}
export {
setValue,
getValue
}
3.3: 获取小程序全局唯一后台接口调用凭据(`access_token`)
import axios from 'axios'
import { getValue, setValue } from 'redisConfig'
import config from 'config'
export const wxGetAccessToken = async (flag = false) => {
let accessToken = await getValue('accessToken')
if (!accessToken || flag) {
const result = await axios.get(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.AppID}&secret=${config.AppSecret}`)
if (result.data === 200) {
await setValue('accessToken', result.data.access_token, result.data.expires_in)
accessToken = result.data.access_token
if (result.data.errcode && result.data.errmsg) {
logger.error(`Wx-GetAccessToken Error: ${result.data.errcode} - ${result.data.errmsg}`)
}
}
}
return accessToken
}
3.4: 内容安全
export const wxMsgCheck = async (content) => {
const accessToken = await wxGetAccessToken()
try {
const result = await axios.post(`https://api.weixin.qq.com/wxa/msg_sec_check?access_token=${accessToken}`, { content })
if (result.status === 200) {
return result.data
} else {
logger.error(`wxMsgCheck Error: ${result.statis}`)
}
} catch (error) {
logger.error(`wxMsgCheck Error: ${error.message}`)
}
}
3.5: 文本安全校验
import { wxMsgCheck } from 'WxUtils'
async addWxPost (ctx) {
const { body } = ctx.request
const content = body.content
const result = await wxMsgCheck(content)
...
}
3.6: 图片安全校验?
3.6.1: 文件目录检查
import fs from 'fs'
import path from 'path'
const getStats = (path) => {
return new Promise (resolve => {
fs.stat(path, (err, stats) => err ? resolve(false) : resolve(stats))
})
}
const mkdir = (dir) => {
return new Promise((resolve) => {
fs.mkdir(dir, err => err ? resolve(false) : resolve(true))
}
}
const dirExists = async (dir) => {
const isExists = await getStats(dir)
// 如果该路径存在且不是文件,返回 true
if (isExists && isExists.isDirectory()) {
return true
} else if (isExists) {
// 路径存在,但是是文件,返回 false
return false
}
// 如果该路径不存在
const tempDir = path.parse(dir).dir
// 循环遍历,递归判断如果上级目录不存在,则产生上级目录
const status = await dirExists(tempDir)
if (status) {
const result = await mkdir(dir)
console.log('TCL: dirExists -> result', result)
return result
} else {
return false
}
}
const getHeaders = (form) => {
return new Promise((resolve, reject) => {
form.getLength((err, length) => {
if (err) {
reject(err)
}
const headers = Object.assign(
{ 'Content-Length': length },
form.getHeaders()
)
resolve(headers)
})
})
}
3.6.2: 图片内容校验
import fs from 'fs'
import path from 'path'
import del from 'del'
import { dirExists } from '/Utils'
import { v4 as uuidv4 } from 'uuid'
import sharp from 'sharp'
import FormData from 'form-data'
import pathExists from 'path-exists'
export const wxImgCheck = async (file) => {
const accessToken = await wxGetAccessToken()
let newPath = file.path
const tmpPath = path.resolve('./tmp')
try {
// 1.准备图片的form-data
// 2.处理图片 - 要检测的图片文件,格式支持PNG、JPEG、JPG、GIF,图片尺寸不超过 750px x 1334px
const img = sharp(file.path)
const meta = await img.metadata() // 分辨率
if (meta.width > 750 || meta.height > 1334) {
await dirExists(tmpPath)
newPath = path.join(tmpPath, uuidv4() + '.png')
await img.resize(750, 1334, {
fit: 'inside'
}).toFile(newPath)
}
const stream = fs.createReadStream(newPath)
const form = new FormData()
form.append('media', stream)
const headers = await getHeaders(form)
const result = await axios.post(`https://api.weixin.qq.com/wxa/img_sec_check?access_token=${accessToken}`, form, { headers })
const stats = await pathExists(newPath)
if (stats) {
await del([tmpPath + path.extname(newPath)], { force: true })
}
if (result.status === 200) {
if (result.data.errcode !== 0) {
await wxGetAccessToken(true)
await wxImgCheck(file)
return
}
return result.data
} else {
logger.error(`wxMsgCheck Error: ${result.statis}`)
}
} catch (error) {
const stats = await pathExists(newPath)
if (stats) {
await del([tmpPath + path.extname(newPath)], { force: true })
}
logger.error(`wxMsgCheck Error: ${error.message}`)
}
}
3.6.3:
import { wxImgCheck } from '/WxUtils'
async uploadImg (ctx) {
const file = ctx.request.files.file
const result = await wxImgCheck(file)
...
}
4: web
4.1: config
export default {
baseUrl: {
dev: 'http://xxx.xxx.xx.xxx:3000',
pro: 'http://api.xxx.xxx.com:22000'
},
publicPath: [/^\/public/, /^\/login/]
}
4.2: wx
import { promisifyAll } from 'miniprogram-api-promise'
const wxp = {}
// promisify all wx's api
promisifyAll(wx, wxp)
export default wxp
4.3: wx.store
import wx from './wx'
class Storage {
constructor (key) {
this.key = key
}
async set (data) {
const result = await wx.setStorage({
key: this.key,
data: data
})
return result
}
async get () {
let result = ''
try {
result = await wx.getStorage({ key: this.key })
} catch (error) {
console.log('Storage -> get -> error', error)
}
return result.data ? result.data : result
}
}
const StoreToken = new Storage('token')
export { Storage, StoreToken }
4.4: upload.js
import config from 'config'
import wx from '/wx'
import { StoreToken } from '/wxstore'
const baseUrl =
process.env.NODE_ENV === 'development'
? config.baseUrl.dev
: config.baseUrl.pro
export const uploadImg = async (file) => {
try {
const token = await StoreToken.get()
const upTask = await wx.uploadFile({
url: baseUrl + '/content/upload',
filePath: file.path,
name: 'file',
header: {
'Authorization': 'Bearer ' + token
},
formData: {
file
}
})
if (upTask.statusCode === 200) {
return JSON.parse(upTask.data)
}
} catch (error) {
console.log('UploadImg -> error', error)
}
}
4.5:
async afterRead (e) {
const file = e.mp.detail.file
uploadImg(file).then((res) => {
if (res.code === 200) {
this.fileList.push(file)
wx.showToast({
title: '上传成功',
icon: 'none',
duraction: 2000
})
}
}
}