共计 6588 个字符,预计需要花费 17 分钟才能阅读完成。
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:
<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
})
}
}
}
正文完