Chen's Blog

陈志昊的个人博客

在微信开发过程中,一些例如jsapi_token 和jsapi_ticket这些信息都有固定的生存时间,过了生存时间即失效,需要重新获取,然而微信方面对于这些信息的获取次数也加以限制。比如jsapi_token的有效时间为7200秒,日最大刷新次数为2000次。如果在每个业务周期内都获取一次的话,很有可能把接口刷爆掉,影响该公众号其他业务。

在传统开发中,上述那些敏感信息主要有以下几种存储形式:外部文件、内存、数据库、高速缓存等。介于一些信息都限制了有效时间,像是文件、内存、数据库这种形式存储的话每次获取前都需要判断一下token距上次获取的时间是否超过keytoken有效时间,若超过则调用token的刷新方法重新获取token。这个逻辑不难,但每次都要判断时间,这个很头疼。

当然,还有最后一种 高速缓存,比如redis,他可以很简单的在插入key的时候设置该key的生存时间,这样一来我们在读key的时候只需要判断有没有就行了,有就用没有就获取,是不是so easy。

好了,逻辑捋清楚了,再来看一下weixin4j给我们提供了哪些缓存的实现。

weixin4j的Token缓存

在此之前,我对该项目的缓存没有任何一点了解,完全出于我个人判断,因为微信开发中必须要涉及到关键性token的缓存问题,那么该框架一定会提供缓存功能,这个方向是对的,那么具体在哪里?怎么用?怎么实现的?如何扩展?就需要翻文档或者查作者源码了。很遗憾,项目官网并没有提供缓存使用方面的文档或介绍,但我在该项目的github主页中找到了一篇介绍,实际上只是说了提供了和默认提供哪种缓存而已,并没有提供别的信息。开启自悟模式。

在基础组件的cache包中,我找到了一组关于缓存的API。

  1. Cacheable
    接口,定义可缓存对象一定是Cacheable的子类实现
  2. CacheCreator
    接口,定义缓存的创建
  3. CacheStorager
    接口,定义缓存的存储。如果要定义自己的缓存存储实现,直接实现该接口即可。但weixin4j已经给我们提供了五个可选择的实现,日常够用。
  4. CacheManager
    缓存管理器,构造时需要传入缓存的创建对象和存储对象。
  5. 五个常用的缓存存储的实现

很巧,我的思路和作者一样,这里有五种缓存方式的具体实现,与我上面相比,多了一个redis集群配置和Memcache缓存配置,这个就不多说了。他们分别是

  1. FileCacheStorager 文件方式,是weixin4j默认提供的
  2. MemoryCacheStorager 内存方式,不推荐使用
  3. MemcacheCacheStorager Memcache的缓存方式,熟悉的童鞋可以用一下
  4. RedisCacheStorager 基于单个Redis缓存的配置方式
  5. RedisClusterCacheStorager Redis集群方式

默认情况下,weixin4j使用文件方式,即使用FileCacheStorager来管理token缓存,默认缓存路径:java.io.tmpdir。

Redis缓存token

五种方式都可以尝试,这里因为项目背景的原因,我使用单个Redis方式配置。

直接看RedisCacheStorager这个类的源码

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
public class RedisCacheStorager<T extends Cacheable> implements
CacheStorager<T> {

private Pool<Jedis> jedisPool;

private final static String HOST = "127.0.0.1";
private final static int PORT = 6379;
private final static int TIMEOUT = 5000;
private final static int MAX_TOTAL = 50;
private final static int MAX_IDLE = 5;
private final static int MAX_WAIT_MILLIS = 5000;
private final static boolean TEST_ON_BORROW = false;
private final static boolean TEST_ON_RETURN = true;

public RedisCacheStorager() {
this(HOST, PORT, TIMEOUT);
}

public RedisCacheStorager(String host, int port, int timeout) {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(MAX_TOTAL);
jedisPoolConfig.setMaxIdle(MAX_IDLE);
jedisPoolConfig.setMaxWaitMillis(MAX_WAIT_MILLIS);
jedisPoolConfig.setTestOnBorrow(TEST_ON_BORROW);
jedisPoolConfig.setTestOnReturn(TEST_ON_RETURN);
this.jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout);
}

public RedisCacheStorager(JedisPoolConfig jedisPoolConfig) {
this(new JedisPool(jedisPoolConfig, HOST, PORT, TIMEOUT));
}

public RedisCacheStorager(String host, int port, int timeout,
JedisPoolConfig jedisPoolConfig) {
this(new JedisPool(jedisPoolConfig, host, port, timeout));
}

public RedisCacheStorager(Pool<Jedis> jedisPool) {
this.jedisPool = jedisPool;
}

//省略其他方法 ...
}

在基于单个Redis的token缓存实现中,默认使用本地无密码认证的6379端口的Redis作为缓存介质,但也提供了四个构造方法去创建自己的token缓存。

第一种,直接使用默认的方式,不多说

第二种,使用主机地址、端口、超时时间配置

第三种,直接使用jedis连接池配置对象,但主机地址、端口和超时时间均使用默认

第四种,全部自定义

第五种,直接把jedis连接池对象传入

配置使用

在Weixin4jConfig对象中,加入RedisCacheStorager对象Bean配置。这里我的项目基于SpringBoot,配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
@Autowired
private JedisConnectionFactory jedisConnectionFactory;

//配置基于Redis的token缓存管理器
@Bean
public RedisCacheStorager<Token> redisCacheStorager() {
return new RedisCacheStorager<Token>(
jedisConnectionFactory.getHostName(),
jedisConnectionFactory.getPort(),
jedisConnectionFactory.getTimeout(),
jedisConnectionFactory.getPoolConfig()
);
}

直接将Jedis的连接工厂拿到,配置RedisCacheStorager的时候注入四个信息即可。

在WeixinProxy对象中可以找到这样一个构造函数

1
2
3
public WeixinProxy(CacheStorager<Token> cacheStorager) {
this(Weixin4jConfigUtil.getWeixinAccount(), cacheStorager);
}

在构造该对象的时候传入一个Token对象的缓存存储管理器。这里直接在昨天的配置类中Weixin4jProxy的Bean配置处使用该构造函数初始化,并传入一个RedisCacheStorager对象。

1
2
3
4
@Bean
public WeixinProxy mpWeixinProxy() {
return new WeixinProxy(redisCacheStorager());
}

测试

调用分享接口,进入redis查看缓存信息
缓存结果

使用weixin4j第二天,今天搞一下JSSDK,也就是分享到朋友圈,分享到QQ之类的接口,官方称JS接口。废话不说,开干。

微信JS接口开发逻辑

在使用weixin4j进行微信JSSDK开发之前,先熟悉一下sdk的开发逻辑,这样在使用weixin4j的时候如果遇到问题,解决起来比较方便,逻辑也比较清楚。下面捋一下步骤

  1. 绑定安全域名,没的说
  2. 编写用于提供前端JSSDK配置信息的后端接口【参数:页面完整带参数url地址,去掉#后部分】
    1. 拿appid和secret换取access_token(公众号的全局唯一票据)。有效期7200秒,因此需要缓存起来,防治刷爆。此token非彼token,和我们上一篇文章中的token不一样,别混了。这个token是appid和secret加密后的结果,可以理解成该公众号的身份凭证,只不过该凭证每2小时(7200秒)需要刷新一次而已。接下来拿着我们的身份证去换票吧
    2. 用得到的access_token换取api_ticket。有效期也是7200秒,也需要做缓存。ticket相当于一张门票,在微信的一些接口调用中,都需要拿着这张票才使用。此时我们有了两样东西,一个是身份证(access_token)另一个是门票(api_ticket),他们都有过期时间,我们需要定期去重新获取他们。
    3. 生成签名
      该签名是JSSDK接口的一个调用凭证,80%的错误都来自于签名的生成。
      1. 签名生成需要如下几个参数:
        1. 被分享页面的完整带参url地址,去掉#后部分,这里已经作为参数传递进来了
        2. 时间戳,10位字符串
        3. 随机字符串
        4. 门票(api_ticket)
      2. 签名算法,引用微信官方文档

        签名生成规则如下:参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。

    4. 将appid、时间戳、随机字符串、签名四个信息返回。到此后端接口就编写完了
  3. 编写前端JS代码
    1. 通过后端接口获得配置信息
    2. 注入微信JSSDK配置函数中
    3. 在ready函数中配置要调用的JSSDK api信息

逻辑都知道了吧,建议用原生去写一下,一步步按照开发逻辑捋一遍再看这篇文章,就会更清晰一点。这里不再写原生了,直接使用weixin4j快速开发后端接口

weixin4j开发思路

上一篇文章在获取用户信息的时候单独创建了一个配置类Weixin4jConfig,其中配置了开发代理类WeixinProxy,通过看这个Bean的源码,我们发现了如下的方法:

1
2
3
4
5
6
7
8
9
10
11
/**
* 获取JSSDK Ticket的tokenManager
*
* @param ticketType
* 票据类型
* @return
*/
public TokenManager getTicketManager(TicketType ticketType) {
return new TokenManager(new WeixinTicketCreator(weixinAccount.getId(),
ticketType, this.tokenManager), this.cacheStorager);
}

看到这个方法是关于JSSDK的,而且直接操作的是Ticket,我就乐坏了,哈哈。

不难发现该方法可以获取Ticket票据管理对象,需要传如一个参数TicketType,用我多年体育老师教的英语经验来看,这个参数名叫票据类型,让我们再看一下这个票据类型到底是啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum TicketType {
/**
* jsapi
*/
jsapi,
/**
* 公众平台-卡券
*/
wx_card,
/**
* 企业号-选取联系人
*/
contact;
}

这是一个枚举类,发现jsapi,思路清晰了,需要根据票据类型获取票据管理对象。查阅票据管理对象的源代码发现并没有生成签名的算法。

接着沿着我们的思路,我们需要找到一个可以将四个参数生成签名的函数,或者如果weixin4j封装的更完善一点直接传入一个url得到组配置结果信息。

翻源码翻了好一会,发现在weixin4j-base组件com.foxinmy.weixin4j.jssdk包中有两个类,分别是JSSDKAPI和JSSDKConfigurator,真是踏破草鞋无觅处,得来全不费功夫呀

JSSDKConfigurator是真正的JSSDK配置信息的操作对象,而JSSDKAPI对象则是API信息的对象,后续才会用到它,先不着急。来看一下这个对象的结构:

方法截图

  1. 该对象有一个构造方法,需要传入一个TokenManager,咦,这个构造函数的注释中说可以通过调用WeixinProxy#getTicketManager获取,兴奋不已。
  2. debugMode()方法是开启JSSDKdebug模式,该方法只是向返回信息中添加了一个debug为true的参数,很显然,该参数的信息会在前端被使用,我们只需要接受就好
  3. appid(),其实并没什么卵用,如果在weixin4j配置了开发者账号在初始化的时候直接回从配置文件中获得,这里不管他
  4. apis(),可以传入一个或一组API数据
  5. toJSONConfig,这个方法传入一个String类型的url,返回一组信息。这个方法就是我们需要的构造签名的方法,代码找到了,那就开干

配置Bean

根据思路我们知道,使用weixin4j开发JSSDK需要三个组件,一个是TokenManager另一个是JSSDKConfigurator,还有上一篇的WeixinProxy代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
public WeixinProxy mpWeixinProxy() {
return new WeixinProxy();
}

@Bean
public TokenManager ticketTokenManager() {
//通过WeixinProxy对象获取Jsapi的票据管理对象
return mpWeixinProxy().getTicketManager(TicketType.jsapi);
}

@Bean
public JSSDKConfigurator jssdkConfigurator() {
//new一个JSSDK配置工具,该工具需要传入票据管理对象,这里传入的同时直接开启JSSDK的debug模式
return new JSSDKConfigurator(ticketTokenManager()).debugMode();
}

到此我们的配置工作就结束了, 接着来搞一下api接口

API接口

首先注入刚刚配置的两个Bean

1
2
3
4
@Autowired
private JSSDKConfigurator jssdkConfigurator;
@Autowired
private TokenManager ticketTokenManager;

接着 编写web接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GetMapping(value = "jssdk_jsonconfig")
public APIResult getJssdkJsonConfig(@RequestParam("url") String url) {
try {
//将公众号开发中所有的api全部添加进去。
jssdkConfigurator.apis(JSSDKAPI.MP_ALL_APIS);
//生成配置信息
String jsonConfig = jssdkConfigurator.toJSONConfig(url);
//weixin4j在生成签名的时候没有提供ticket打印功能,如果想使用微信官方的签名校验的童鞋,请使用票据管理器获取ticket票据
logger.info("jssdk ticket:{}", ticketTokenManager.getAccessToken());
return asSuccess(jsonConfig);
} catch (WeixinException e) {
e.printStackTrace();
return asError(e.getMessage());
}
}

后端的接口代码就是这样。关键代码就两行,是不是特别一贼

前端JS代码

根据我们的原生开发逻辑,直接上代码,不啰嗦

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
<!-- js -->
<script src='http://img.neusoft.edu.cn/templates/neusoft.edu.cn/js/jquery.min.js'></script>
<script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
<script>
$(function () {
var config = {
debug: '', // 开启调试模式
appId: '', // 必填,公众号的唯一标识
timestamp: '', // 必填,生成签名的时间戳
nonceStr: '', // 必填,生成签名的随机串
signature: '',// 必填,签名,见附录1
jsApiList: [] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
};
$.ajax({
url: '接口请求地址',
type: 'GET',
dataType: 'json',
async:false,
data: {url: window.location.href}
}).success(function(date){
var json = JSON.parse(date.result);
config.debug = json.debug;
config.appId = json.appId;
config.timestamp = json.timestamp;
config.nonceStr = json.nonceStr;
config.signature = json.signature;
config.jsApiList = json.jsApiList;
});
wx.config(config);
wx.ready(function(){
//分享接口
wx.onMenuShareTimeline({
title: '这里是分享标题', // 分享标题
link: window.location.href, // 分享链接
imgUrl: '分享图标地址.没有,先这么放着吧', // 分享图标
success: function () {
// 用户确认分享后执行的回调函数
alert("分享成功");
},
cancel: function () {
// 用户取消分享后执行的回调函数
alert("不分享拉倒");
}
});
});
wx.error(function(res){
console.log("error:" + res);
});
});
</script>

测试结果

1
2

因为最近的项目需要做微信方面的开发,之前也有过微信开发的经验,但每逢项目中遇到跟微信沾边的东西就得从头写起,一直也没单独把微信开发方面的代码单独独立出来。

首先到微信开放平台申请一个测试号,绑定安全域名

weixin4j环境配置

这是一个封装了相当完善的Java微信开发工具,项目主页:weixin4j

建议先了解微信传统Oauth2.0开发流程再使用该工具

引入Maven配置

1
2
3
4
5
6
//这里引入weixin4j公众号开发Api,还有企业号和服务器的这里不引入
<dependency>
<groupId>com.foxinmy</groupId>
<artifactId>weixin4j-mp</artifactId>
<version>1.7.4</version>
</dependency>

weixin4j相关依赖还有fastjson和HttpClient

配置开发代理Bean

将com.foxinmy.weixin4j.mp.WeixinProxy类注入Spring容器管理

1
2
3
4
@Bean
public WeixinProxy mpWeixinProxy() {
return new WeixinProxy();
}

获取用户信息需要使用的几个对象

  • com.foxinmy.weixin4j.mp.api.OauthApi
    公众号Oauth开发流程API
  • com.foxinmy.weixin4j.mp.model.OauthToken
    Token实体
  • com.foxinmy.weixin4j.mp.model.User
    微信用户信息封装实体

配置开发者账号

在classpath下创建weixin4j.properties配置文件,配置Appid和secret

1
weixin4j.account={"id":appid,"secret":secret}

用户信息获取接口

贴上获取用户信息的代码,按照微信逻辑走即可

  1. snapi_userinfo 授权,需用户手动确认授权,因此无需关注或与公众号产生消息交互
1
2
3
4
5
6
7
8
@GetMapping(value = "/user_authenticator")
public APIResult userAuthenticator(@RequestParam(name = "code") String code) {
OauthApi oauthApi = weixinProxy.getOauthApi();
OauthToken oauthToken = oauthApi.getAuthorizationToken(code);
logger.info("{}", oauthToken.toString());
User user = oauthApi.getAuthorizationUser(oauthToken);
return asSuccess(user);
}
  1. snapi_base 授权,获取用户openid,并通过openid获取用户信息。用户信息中带有用户是否关注公众号的状态字段。用此字段来判断用户是否已关注公众号,达到强制关注的效果
    1
    2
    3
    4
    5
    6
    @GetMapping(value = "/user_authenticator")
    public APIResult userAuthenticator(@RequestParam(name = "code") String code) {
    OauthApi oauthApi = weixinProxy.getOauthApi();
    OauthToken oauthToken = oauthApi.getAuthorizationToken(code);
    return weixinProxy.getUser(oauthToken.getOpenId());
    }

创建授权连接

weixin4j提供了构造授权连接的Api,传入回调地址、state、scope即可

com.foxinmy.weixin4j.mp.api.OauthApi#getUserAuthorizationURL(redirectUri, state, scope)

scope分为两种snsapi_base和snsapi_userinfo,具体请查阅微信开发文档

通过微信访问授权连接,应该会得到用户信息的输出。

总结

使用weixin4j大大简化了java微信开发的时间,而且weixin4j还提供了一套非常灵活的token缓存机制。这篇先到这里,下一篇会分享通过weixin4j开发分享到朋友圈的功能。

问题现象

在调用php程序员的接口时,我们约定对参数进行字典排序并encode后按照约定的逻辑加密生成签名,加到请求头一同传输,php端对提交的参数执行同样的逻辑并且与请求头中的签名对比,起到防止数据在传输过程中被篡改。当然了,使用ssl的自然不用这么麻烦。我的问题出现在java方面给php接口方面post时中文乱码的问题。

数据乱码截图

初步想法

谈到中文乱码问题,我首先想到肯定是字符集在某个环节出现不匹配的情况。在使用HttpClient进行Http请求操作时,涉及到编码的无非在请求头处,但这里我们按照接口定义,对参数生成签名时使用了encode(UTF-8)编码,经过与接口方面的校验签名生成正确,也就说明是请求头出了问题。看一下post代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static StringBuilder post(String url, Map<String, String> headers, Map<String, String> params) throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(url);
if (params != null && !params.isEmpty()) {
List<NameValuePair> nvps = new ArrayList<NameValuePair>();
for (Map.Entry<String, String> param : params.entrySet()) {
nvps.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
httpPost.setEntity(new UrlEncodedFormEntity(nvps,"utf-8"));
}
if (headers != null && !headers.isEmpty()) {
for (Map.Entry<String, String> header : headers.entrySet()) {
httpPost.setHeader(header.getKey(), header.getValue());
}
}
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
return getContent(response.getEntity());
}
}

第九行位置,可以看到我在发起post请求时使用的是默认UrlEncodedFormEntity,该对象会对参数进行自动encode操作,但问题来了,编码呢?
看一下这个对象的构造函数

1
2
3
4
5
6
7
8
9
10
11
/**
* Constructs a new {@link UrlEncodedFormEntity} with the list
* of parameters with the default encoding of {@link HTTP#DEFAULT_CONTENT_CHARSET}
*
* @param parameters list of name/value pairs
* @throws UnsupportedEncodingException if the default encoding isn't supported
*/
public UrlEncodedFormEntity (
final List <? extends NameValuePair> parameters) throws UnsupportedEncodingException {
this(parameters, (Charset) null);
}

可以看到在单参数构造函数中还使用了一个第二个参数是字符编码的构造函数,看注释可以看到,在不指定字符编码的时候默认使用的是DEFAULT_CONTENT_CHARSET字符编码(ISO-8859-1)
好了,现在已经定位到问题了。问题出现在这里,现在可以使用第二个参数为字符编码的构造函数构造该对象。使用utf-8构造该对象即可解决问题。

在macbook系统上安装phpMyadmin工具管理mysql数据库。

环境

  • macOS 10.12.1 (mysql5.7官方说支持到10.11,其实这个版本也可以安装)
  • mysql5.7.16
  • apache2.4.23 (macOS自带)
  • PHP5.6.25 (macOS自带)
  • MySQL Workbench (GPL)6.3.8
  • phpMyAdmin4.4.10

安装mysql

  1. 这里选择mac版mysql5.7.16(发帖最高)版本,通过Oracle官网下载
  2. 安装后注意弹窗,会提示默认的初始密码,接下来会用到改密码初始化数据库。

安装MySQL Workbench工具初始化mysql数据库root账户

  1. 在上一步mysql下载页面的下方会找到MySQL Workbench的下载链接,下载并安装
  2. 启动软件,添加一个本地mysql数据库连接会话。输入初始密码后会提示密码未修改。再次提交后会提示输入Oldpassword和输入两次新密码。完成后 root账户到此初始化成功

将phpMyAdmin源码放入web主目录

目录地址:/Library/WebServer/Documents/ 为apache 的www目录

开启mac本地apache和php环境

使用 终端su命令输入密码后切换管理员账号,并输入一下命令

1
nano  /etc/apache2/httpd.conf

找到以下信息并修改

1
2
3
4
5
6
7
8
9
10
11
#LoadModule php5_module libexec/apache2/libphp5.so
改为
LoadModule php5_module libexec/apache2/libphp5.so
添加以下信息到该文件结尾处

<Directory "phpmyadmin存放目录">
Options Indexes FollowSymLinks MultiViews
AllowOverride all
Order Deny,Allow
Allow from all
</Directory>

配置phpMyAdmin

复制配置文件

1
cp config.sample.inc.php config.inc.php

修改权限

1
chmod -R 777 /Library/WebServer/Documents/phpmyadmin/

启动apache测试环境

1
/usr/sbin/apachectl start

访问 http://localhost/phpmyadmin 输入帐密登录

从字面意义上来讲,泛型,即广泛的类型,这样就不难理解了。泛型是java程序设计的一个手段,可以想想为使用泛型编写的代码即是一个模板,使用泛型机制编写java程序在安全性和可读性上都会更有帮助。

阅读全文 »

开发时最痛恨的是灯下黑的bug,bug就在眼皮子地下被自己的“自作聪明”忽略掉。就在刚刚,我花了近十分钟去解决一个Mybatis报的异常: Result Maps collection already contains value for #¥%。先解释一下该异常信息的意思:Result Maps集合已经存在于#¥%。总之通过异常信息可以确定的是 该文件中有相同的某某某,即有同名的东西存在于一个文件中。着手解决一下吧

阅读全文 »

今天手贱升级max最新系统后,打开Idea提示git环境异常,提示如下信息:

xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun

完美解决方法:xcode-select –install

0%