微信小程序UGC类项目上线技巧|文本和媒体安全验证实现

如果你的小程序审核时遇到这种情况,这篇帖子会完美解决你的问题

一般UGC类小程序上线都会遇到这种情况,微信官方的运行攻略:微信开放社区,说白了就是想推销微信的安全接口,你就算用了其它的安全接口也不让你过,哈哈哈哈被资本做局了吧~

什么服务或功能的小程序是UGC小程序?

小程序中的功能或服务中,涉及用户将自己自定义编辑的文字、图片、音频、视频等内容通过小程序进行展示或提供给其他用户的,属于UGC小程序。

AccessToken获取

官方文档:接口调用凭证 / 获取接口调用凭据

先上Java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private final RedisUtils redisUtils;
/**
* 获取access_token
* @return
*/
public String getAccessToken(){
String url = "https://api.weixin.qq.com/cgi-bin/token";
String requestUrl = UriComponentsBuilder.fromHttpUrl(url)
.queryParam("appid", APP_ID)
.queryParam("secret", APP_SECRET)
.queryParam("grant_type", "client_credential")
.toUriString();
HttpResponse response = HttpUtil.createGet(requestUrl).execute();

// 获取 session_key 和 openid
JSONObject parseObj = JSONUtil.parseObj(response.body());
//存入redis
redisUtils.setValue("access_token", (String)parseObj.get("access_token"));
redisUtils.expire("access_token", 7200);
return (String)parseObj.get("access_token");
}

我这里获取到后存在了redis里,因为这个access_token有效期只有2小时,所以还加了个时间限制,2小时后自动消失。

记得替换APP_ID和APP_SECRET

RedisUtils工具类,可以用用挺好用的,或者用你自己的redis存储方式也行。

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
package com.cn.bdth.utils;

import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;


/**
* redis工具类
*
* @author @github dulaiduwang003
* @version 1.0
*/
@Component
@SuppressWarnings("all")
@RequiredArgsConstructor
public final class RedisUtils {

private final RedisTemplate<String, Object> redisTemplate;

public RedisTemplate getRedisTemplate() {
return this.redisTemplate;
}

/**
* 设置指定键的过期时间(以秒为单位)
*
* @param key 键
* @param timeout 过期时间,单位:秒
* @return 是否成功设置过期时间
*/
public boolean expire(final String key, final long timeout) {
return expire(key, timeout, TimeUnit.SECONDS);
}

/**
* 获取指定键的剩余生存时间
*
* @param key 键
* @return 剩余生存时间(秒)
*/
public Long getExpire(final String key) {
return redisTemplate.getExpire(key);
}

/**
* 设置指定键的过期时间
*
* @param key 键
* @param timeout 过期时间
* @param unit 时间单位
* @return 是否成功设置过期时间
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit) {
Boolean ret = redisTemplate.expire(key, timeout, unit);
return ret != null && ret;
}

/**
* 检查给定键是否存在
*
* @param key 键
* @return 键是否存在
*/
public boolean hasKey(final String key) {
return redisTemplate.hasKey(key);
}

/**
* 对指定键的数字值进行增量操作
*
* @param key 键
* @param delta 增量值
* @return 增量操作后的新值
*/
public long increment(final String key, final long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}

/**
* 删除指定的键
*
* @param key 键
* @return 是否成功删除
*/
public boolean delKey(final String key) {
Boolean ret = redisTemplate.delete(key);
return ret != null && ret;
}

/**
* 批量删除指定的键
*
* @param keys 键集合
* @return 删除的键数量
*/
public long delKeys(final Collection<String> keys) {
Long ret = redisTemplate.delete(keys);
return ret == null ? 0 : ret;
}

/**
* 将对象存储到Redis中,使用指定的键
*
* @param key 键
* @param value 值
*/
public void setValue(final String key, final Object value) {
redisTemplate.opsForValue().set(key, value);
}

/**
* 将对象存储到Redis中,并设置过期时间
*
* @param key 键
* @param value 值
* @param timeout 过期时间(秒)
*/
public void setValueTimeout(final String key, final Object value, final long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}

/**
* 根据键获取对应的值
*
* @param key 键
* @return
*/
public Object getValue(final String key) {
return redisTemplate.opsForValue().get(key);
}

/**
* 检查哈希表中是否存在指定的字段
*
* @param key 哈希表键
* @param hkey 字段名
* @return 是否存在
*/
public boolean hasHashKey(final String key, String hkey) {
Boolean ret = redisTemplate.opsForHash().hasKey(key, hkey);
return ret != null && ret;
}

/**
* 向哈希表中添加一个字段-值对
*
* @param key 哈希表键
* @param hKey 字段名
* @param value 值
*/
public void hashPut(final String key, final String hKey, final Object value) {
redisTemplate.opsForHash().put(key, hKey, value);
}

/**
* 向哈希表中批量添加字段-值对
*
* @param key 哈希表键
* @param values 多个字段-值对
*/
public void hashPutAll(final String key, final Map<String, Object> values) {
redisTemplate.opsForHash().putAll(key, values);
}

/**
* 获取哈希表中指定字段的值
*
* @param key 哈希表键
* @param hKey 字段名
* @return 字段对应的值
*/
public Object hashGet(final String key, final String hKey) {
return redisTemplate.opsForHash().get(key, hKey);
}

/**
* 获取哈希表中所有的字段和值
*
* @param key 哈希表键
* @return 所有字段和值的映射表
*/
public Map<Object, Object> hashGetAll(final String key) {
return redisTemplate.opsForHash().entries(key);
}

/**
* 获取哈希表中多个字段的值
*
* @param key 哈希表键
* @param hKeys 多个字段名
* @return 多个字段对应的值列表
*/
public List<Object> hashMultiGet(final String key, final Collection<Object> hKeys) {
return redisTemplate.opsForHash().multiGet(key, hKeys);
}

/**
* 检查哈希表中是否存在指定的字段(功能同 hasHashKey)
*
* @param key 哈希表键
* @param hashKey 字段名
* @return 是否存在
*/
public boolean hashExists(String key, String hashKey) {
return redisTemplate.opsForHash().hasKey(key, hashKey);
}

/**
* 删除哈希表中的一个或多个字段
*
* @param key 哈希表键
* @param hKeys 多个字段名
* @return 被删除的字段数量
*/
public long hashDeleteKeys(final String key, final Collection<Object> hKeys) {
return redisTemplate.opsForHash().delete(key, hKeys);
}

/**
* 删除哈希表中的一个或多个字段(可变参数形式)
*
* @param key 哈希表键
* @param hashKey 多个字段名
* @return 被删除的字段数量
*/
public Long hashDelete(final String key, final Object... hashKey) {
return redisTemplate.opsForHash().delete(key, hashKey);
}

/**
* 向集合中添加多个元素
*
* @param key 集合键
* @param values 元素数组
* @return 添加成功的元素数量
*/
public long setSet(final String key, final Object... values) {
Long count = redisTemplate.opsForSet().add(key, values);
return count == null ? 0 : count;
}

/**
* 从集合中移除指定的元素
*
* @param key 集合键
* @param values 要移除的元素数组
* @return 成功移除的元素数量
*/
public long setDel(final String key, final Object... values) {
Long count = redisTemplate.opsForSet().remove(key, values);
return count == null ? 0 : count;
}

/**
* 获取集合中的所有成员
*
* @param key 集合键
* @return 集合的所有成员
*/
public Set<Object> getSetAll(final String key) {
return redisTemplate.opsForSet().members(key);
}

/**
* 向有序集合中添加多个带分数的元素
*
* @param key 有序集合键
* @param values 包含元素及其分数的集合
* @return 添加成功的元素数量
*/
public long zsetSetAll(final String key, final Set<ZSetOperations.TypedTuple<Object>> values) {
Long count = redisTemplate.opsForZSet().add(key, values);
return count == null ? 0 : count;
}

/**
* 获取有序集合中某个元素的分数
*
* @param key 有序集合键
* @param value 元素值
* @return 元素对应的分数
*/
public Double zsetSetGetSource(final String key, final Object value) {
return redisTemplate.opsForZSet().score(key, value);
}

/**
* 对有序集合中的某个元素的分数进行增量操作
*
* @param key 有序集合键
* @param value 元素值
* @param increment 分数增量
* @return 更新后的分数
*/
public Double zsetIncrementScore(final String key, final Object value, final Double increment) {
return redisTemplate.opsForZSet().incrementScore(key, value, increment);
}

/**
* 向有序集合中添加一个带分数的元素
*
* @param key 有序集合键
* @param values 元素值
* @param source 分数值
* @return 是否成功添加
*/
public Boolean zsetSet(final String key, final Object values, final Double source) {
final Boolean add = redisTemplate.opsForZSet().add(key, values, source);
return add;
}

/**
* 获取有序集合中排名在指定范围内的元素及其分数(按分数由高到低排序)
*
* @param key 有序集合键
* @param start 起始索引
* @param end 结束索引
* @return 元素及其分数的集合
*/
public Set<ZSetOperations.TypedTuple<Object>> zsetReverseRangeWithScores(final String key, final Long start, final Long end) {
return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
}

/**
* 获取有序集合中排名在指定范围内的元素(按分数由高到低排序)
*
* @param key 有序集合键
* @param start 起始索引
* @param end 结束索引
* @return 元素集合
*/
public Set<Object> zsetReverseRange(final String key, final Long start, final Long end) {
return redisTemplate.opsForZSet().reverseRange(key, start, end);
}

/**
* 原子性地增加指定键的值并返回新值(适用于计数器场景)
*
* @param key 键
* @return 自增后的新值
*/
public long selfIncrease(final String key) {
return redisTemplate.execute(new SessionCallback<Long>() {
@Override
public Long execute(RedisOperations operations) throws DataAccessException {
operations.multi();
Long count = operations.opsForValue().increment(key);
operations.exec();
return count;
}
});
}

/**
* 原子性地增加有序集合中某个元素的分数并返回新分数
*
* @param key 有序集合键
* @param value 元素值
* @return 自增后的新分数
*/
public Double selfIncreaseSource(final String key, final Object value) {
return redisTemplate.execute(new SessionCallback<Double>() {
@Override
public Double execute(RedisOperations operations) throws DataAccessException {
operations.multi();
Double count = operations.opsForZSet().incrementScore(key, value, 1);
operations.exec();
return count;
}
});
}

/**
* 从有序集合中移除多个指定的元素
*
* @param key 有序集合键
* @param values 要移除的元素集合
* @return 成功移除的元素数量
*/
public long zsetDelAll(final String key, final Set<ZSetOperations.TypedTuple<Object>> values) {
Long count = redisTemplate.opsForZSet().remove(key, values);
return count == null ? 0 : count;
}

/**
* 从有序集合中移除指定的元素
*
* @param key 有序集合键
* @param values 要移除的元素
* @return 成功移除的元素数量
*/
public long zsetDel(final String key, Object values) {
Long count = redisTemplate.opsForZSet().remove(key, values);
return count == null ? 0 : count;
}

/**
* 向列表尾部追加一个元素
*
* @param key 列表键
* @param value 元素值
* @return 列表在添加元素后的长度
*/
public long listPush(final String key, final Object value) {
Long count = redisTemplate.opsForList().rightPush(key, value);
return count == null ? 0 : count;
}

/**
* 检查指定键是否存在(功能同 hasKey)
*
* @param key 键
* @return 键是否存在
*/
public Boolean doesItExist(final String key) {
return redisTemplate.hasKey(key);
}

/**
* 向列表尾部追加多个元素(集合形式)
*
* @param key 列表键
* @param values 要追加的元素集合
* @return 列表在添加元素后的长度
*/
public long listPushAll(final String key, final Collection<Object> values) {
Long count = redisTemplate.opsForList().rightPushAll(key, values);
return count == null ? 0 : count;
}

/**
* 向列表尾部追加多个元素(可变参数形式)
*
* @param key 列表键
* @param values 要追加的元素数组
* @return 列表在添加元素后的长度
*/
public long listPushAll(final String key, final Object... values) {
Long count = redisTemplate.opsForList().rightPushAll(key, values);
return count == null ? 0 : count;
}

/**
* 获取列表中指定范围的元素
*
* @param key 列表键
* @param start 起始索引
* @param end 结束索引
* @return 指定范围的元素列表
*/
public List<Object> listGet(final String key, final int start, final int end) {
return redisTemplate.opsForList().range(key, start, end);
}

/**
* 获取列表的长度
*
* @param key 列表键
* @return 列表的长度
*/
public Long keySize(final String key) {
return redisTemplate.opsForList().size(key);
}
}

文本安全内容识别

官方文档:小程序安全 / 内容安全 / 文本内容安全识别

这里我用Java实现文本识别:

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
/**
* 文本内容检测
* Scene场景枚举值(1 资料;2 评论;3 论坛;4 社交日志)
* @param content
* @return
*/

public String textCheck(String content,Integer scene,String openId){
String url = "https://api.weixin.qq.com/wxa/msg_sec_check";
//获取access_token
String accessToken = (String) redisUtils.getValue("access_token");
if (accessToken == null){
accessToken = getAccessToken();
}
String requestUrl = UriComponentsBuilder.fromHttpUrl(url)
.queryParam("access_token", accessToken)
.toUriString();

String jsonBody = JSONUtil.createObj()
.put("content", content)
.put("version", 2)
.put("scene", scene)
.put("openid", openId)
.toString();

HttpResponse response = HttpUtil.createPost(requestUrl)
.body(jsonBody)
.execute();

// 获取响应内容并解析
JSONObject parseObj = JSONUtil.parseObj(response.body());
String errmsg = (String) parseObj.get("errmsg");
if (!"ok".equals(errmsg)){
return errmsg;
}
JSONObject result = JSONUtil.parseObj(parseObj.get("result"));
int label = ((Number) result.get("label")).intValue();
return String.valueOf(label);
//命中标签枚举值,100 正常;10001 广告;20001 时政;20002 色情;20003 辱骂;20006 违法犯罪;20008 欺诈;20012 低俗;20013 版权;21000 其他
}

需要传入content要识别的文本内容,openId,scene应用场景,我会返回label值

参考错误码:

错误码 错误码取值 解决方案
-1 system error 系统繁忙,此时请开发者稍候再试
40001 invalid credential access_token isinvalid or not latest access_token 无效或不为最新获取的 access_token,请开发者确认access_token的有效性
40003 invalid openid 不合法的 OpenID ,请开发者确认 OpenID 的有效性
40129 invalid scene 场景值错误(目前支持场景 1 资料;2 评论;3 论坛;4 社交日志)
43104 The openid does not match the appid appid与 openid 不匹配
43002 require POST method 方法调用错误,请用 post 方法调用
44002 empty post data POST 的数据包为空。post请求body参数不能为空。
44991 reach max api minute frequence 超出接口每分钟调用限制
45009 reach max api daily quota limit 超出接口每日调用限制
47001 data format error 解析 JSON/XML 内容错误;post 数据中参数缺失;检查修正后重试。
61010 code is expired 用户访问记录超时(用户未在近两小时访问小程序)

多媒体安全内容识别

官方文档:小程序安全 / 内容安全 / 多媒体内容安全识别

这个就比较麻烦了,首先要去微信公众平台申请消息推送:

填写信息:

然后在里服务器编写接收信息的接口,接收的地址要跟你写的url一样

其中,signature签名的生成方式是:

  1. 将Token、timestamp、nonce三个参数进行字典序排序。
  2. 将三个参数字符串拼接成一个字符串进行sha1计算签名,即可获得signature。 开发者需要校验signature是否正确,以判断请求是否来自微信服务器,验签通过后,请原样返回echostr字符串。

举例:假设填写的URL=”https://www.qq.com/revice", Token=”AAAAA”。

  1. 推送的URL链接:https://www.qq.com/revice?signature=f464b24fc39322e44b38aa78f5edd27bd1441696&echostr=4375120948345356249&timestamp=1714036504&nonce=1514711492
  2. 将token、timestamp、nonce三个参数进行字典序排序,排序后结果为:[“1514711492”,”1714036504”,”AAAAA”]。
  3. 将三个参数字符串拼接成一个字符串:”15147114921714036504AAAAA”
  4. 进行sha1计算签名:f464b24fc39322e44b38aa78f5edd27bd1441696
  5. 与URL链接中的signature参数进行对比,相等说明请求来自微信服务器,合法。
  6. 构造回包返回微信,回包消息体内容为URL链接中的echostr参数4375120948345356249。

验证接口代码:

1
2
3
4
5
6
7
8
@GetMapping("/wx/revice")
public String revice(@RequestParam String signature,@RequestParam String timestamp,@RequestParam String nonce,@RequestParam String echostr) {
String res = nonce + timestamp + "hepingan";
String sha1Signature = sha1(res);
if (Objects.equals(sha1Signature, signature)){
return echostr;
}else return echostr;
}