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 apipromisifyAll(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.proexport 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:
<van-uploader @afterRead="afterRead" :fileList="fileList"></van-uploader>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 }) } }}