2018.04.13更新:这种方案不好,我后面有时间会重新写,主要错误点是在于应该把mySessionKey放在HTTP协议的header中,而且微信小程序的wx.request()方法也是支持的,我的小程序已经更新了,但是没有更新博客。
前文:微信小程序API文档中,有一个关于wx.login()的登录态维护的时序图,这篇文章就是记录如何实现的。
首先,再开始这篇文章内容之前,我们已经拿到了openid与session_key这两个参数,但是这两个参数的命名不符合java的驼峰命名法,所以后面我会将openid称为openID,session_key称为sessionKey。
其次,关于为什么要这样做的原因。
以往我们在网站应用的开发中,我们会在用户登录后,将用户名或者是唯一标识放在服务器端的session对象中,当用户在session会话允许时间内再次访问的时候,我们的servlet会从http请求报文中拿到session id,然后替我们找到该用户的session对象以及session对象中我们存入的用户名或者是唯一标识。然后我们就可以通过这个信息获取到这个用户的相关信息,从而完成业务逻辑。
但是微信小程序并没有H5中的session会话机制,因此我们不能像平时的网站应用那样在服务器端的session对象中存储用户名或者是唯一标识来区别谁是谁。所以我们需要自己去实现一个session。
我们生成自己生成一个key,我称之为mySessionKey,然后用key-value键值对的形式,将mySessionKey作为key,openID与sessionKey作为value。然后我们在每一次微信小程序用户发送请求的时候带上mySessionKey,服务器端则通过mySessionKey来读取存储在服务器端的value,也就是openID与sessionKey。
选择Redis的原因主要有两点:
第一个是高效;
第二个是Redis数据库可以设置数据的有效期,超过时间则会自动删除。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<version>1.5.10.RELEASEversion>
dependency>
注:SpringBoot在2017年8月的时候就抛弃了spring-boot-starter-redis转而使用spring-boot-starter-data-redis 并且相关资料全部在SpringData下而不是SpringBoot下,如果需要查看官方文档的朋友可以注意一下。
SpringBoot支持 application.yml 和 application.properties 两种文件类型,这里我使用的是 application.yml 的文件类型
spring:
redis:
host: localhost #改成自己的IP地址,如果是本地就localhost
port: 6379 #默认Redis数据库端口
password: password #Redis数据库的密码,需要在Redis配置文件中配置好,如果没有密码就删掉
database: 0
pool:
max-active: 8
max-wait: -1
max-idle: 500
min-idle: 0
timeout: 0
这样弄好就可以了,SpringBoot会为我们自动帮我们把参数注入到 StringRedisTemplate 对象中,我们只需要使用 @Autowired 注解取出被Spring注入的对象即可。
先看代码:
package com.baofeidyz.langrenshaplusbackground2.dao.impl;
import com.baofeidyz.langrenshaplusbackground2.dao.RedisDao;
import com.baofeidyz.langrenshaplusbackground2.pojo.entity.UserDO;
import com.google.gson.Gson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Repository;
import java.util.concurrent.TimeUnit;
/**
* @author: 暴沸
* @email: [email protected]
* @data: 2018/2/6 10:09
* @desc: Redis数据库操作实例化对象
*/
@Repository
public class RedisDaoImpl implements RedisDao{
// 通过Autowired注解拿到SpringBoot帮我们注入好参数的StringRedisTemplate 对象
@Autowired
private StringRedisTemplate redisTemplate;
// 传入参数解析:
// key -> 自己生成的mySessionKey
// openID -> 从微信服务器端获取到的openid
// sessionKey -> 从微信服务器端获取到的session_key
// l -> 在Redis数据库中保存的时长
// timeUnit -> 在Redis数据库中保存的时长单位,TimeUnit是一个枚举类,建议点开查看一下源码就可以理解了。我
public void set(String key, String openID, String sessionKey, Long l, TimeUnit timeUnit){
//准备redis数据操作对象(String类型)
ValueOperations ops = redisTemplate.opsForValue();
//UserDO 封装了微信用户的相关属性,比如openID 和 sessionKey
//准备user封装数据
UserDO user = new UserDO();
user.setOpenID(openID);
user.setSessionKey(sessionKey);
//这里使用的是Google的Gson包,个人喜好,可以随意更改,主要目的就是将user对象转换为JSON字符串,便于存放在Redis数据库中
//准备Gson对象封装json数据
Gson gson = new Gson();
// 将json数据存入redis数据库中
ops.set(key,gson.toJson(user),l,timeUnit);
}
//传入参数解析:
//key -> 我自己生成的mySessionKey,用于获取对应的value,也就是被转换为json字符串的user对象
//这里参数不写mySessionKey而写key是因为这个DAO类并不设计业务逻辑,仅是单纯的对Redis数据库操作
public UserDO get(String key){
//准备redis数据操作对象(String类型)
ValueOperations ops = redisTemplate.opsForValue();
//准备user对象封装数据
UserDO user = new UserDO();
//准备gson对象封装json数据
Gson gson = new Gson();
//从redis数据库中获取json字符串
String str = ops.get(key);
//使用gson对象方法序列化json字符串为user对象
user = gson.fromJson(str, UserDO.class);
return user;
}
}
就这样,我们完成了对Redis数据库的操作
实现了mySessionKey换openID与sessionKey以后,我们还需要设置一个会话过滤器。实现的逻辑也是类似于我们网站应用,但是也有所不同。不同的点在后面再细说,我们先实现一个会话过滤器:
先看代码:
package com.baofeidyz.langrenshaplusbackground2.filter;
import com.baofeidyz.langrenshaplusbackground2.util.BodyReaderHttpServletRequestWrapperUtil;
import com.baofeidyz.langrenshaplusbackground2.util.RequestJsonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
@Component
@WebFilter(urlPatterns = "/*")
public class MiniFilter implements Filter {
@Autowired
RequestJsonUtil requestJsonUtil;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//TODO 在这里写具体的过滤逻辑
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
System.out.println("destory");
}
}
实现逻辑非常简单:
第一,实现Filter接口;
第二,重载三个方法;
第三,我们在doFilter()方法中添加我们自己的逻辑代码;
第四,添加 @Component 注解,这样SpringBoot在启动的时候就会把这个类的对象注入到容器中了;
第五,添加 @WebFilter(urlPatterns = “/*”) 注解,urlPatterns参数标识需要处理的url,这里是匹配所有,建议大家根据实际情况而定,比如/wechat/* 等。
不同点就在于以往我们是在session对象中取用户名或者是唯一标识,现在我们只能在request对象中去拿信息。
由于微信小程序与后台的交互主要是使用json格式,所以我的方案是将mySessionKey放在json数据中传输(当然也可以通过修改http的header完成,但是我并不知道应该如何在微信端操作,就不讲这个了),json格式如下:
{
code: null,
mySessionKey: "9fafe6cf8da4463a9c69cc60773fd505",
msg: null,
data: null
}
但是这里就会发生一个问题,由于json数据不同于以往我们的get或者post表单请求,如果你抓包的话,你会看到在http报文的response header中,有一个属性变了:
Content-Type:application/json;charset=UTF-8
所以我们没有办法通过request.getParameter() 或者是 request.getAttribute() 方法拿到json参数。
我们只能通过流来拿到json数据,方法如下:
package com.baofeidyz.langrenshaplusbackground2.util;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@Component
public class RequestJsonUtil {
public String getJsonStr(HttpServletRequest request){
String jsonStr = null;
String httpMethod = request.getMethod();
if ("".equals(httpMethod)){
return null;
}else if ("POST".equals(httpMethod)){
try {
jsonStr = this.getJsonStrByPOSTRquest(request);
} catch (IOException e) {
e.printStackTrace();
}
}else if ("GET".equals(httpMethod)){
try {
jsonStr = this.getJsonStrByGETRquest(request);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return jsonStr;
}
private String getJsonStrByGETRquest(HttpServletRequest request) throws UnsupportedEncodingException {
String json = null;
String httpMethod = request.getMethod();
if ("GET".equals(httpMethod)) {
json = new String(request.getQueryString().getBytes("iso-8859-1"), "utf-8").replaceAll("%22", "\"");
}
return json;
}
private String getJsonStrByPOSTRquest(HttpServletRequest request) throws IOException {
String json = null;
String httpMethod = request.getMethod();
if ("POST".equals(httpMethod)){
//判断是否为空
int contentLength = request.getContentLength();
if (contentLength < 0) {
return null;
}
byte buffer[] = new byte[contentLength];
for (int i = 0; i < contentLength; ) {
int readlen = request.getInputStream().read(buffer, i, contentLength - i);
if (readlen == -1) {
break;
}
i += readlen;
}
String charEncoding = request.getCharacterEncoding();
if (charEncoding == null) {
charEncoding = "UTF-8";
}
json = new String(buffer, charEncoding);
}
return json;
}
}
这里就不分析代码块了,具体思路就是通过这个工具类,将在过滤器中的request对象传入,然后从request中拿到我们想要的json数据。
至此,实际上我们已经完成了在过滤器拿到我们微信小程序发回来的json数据,But 你如果实操就会发现,这样子做了以后,我们的Controller拿不到数据了,永远都是null。具体的原因也非常的简单,就是流getInputStream()只能被读取一次,所以当你在过滤器中拿到了数据以后,你在Controller中也就拿不到了。
所以我们需要重写getInputStream()方法:
package com.baofeidyz.langrenshaplusbackground2.util;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
//继承HttpServletRequestWrapper类
public class BodyReaderHttpServletRequestWrapperUtil extends HttpServletRequestWrapper{
//私有化一个byte数据,用于存放我们读取出来的数据
private final byte[] body;
//修改构造函数,调用私有化的getBodyByte()方法读取我们的json数据,当然是以byte数据的方式
public BodyReaderHttpServletRequestWrapperUtil(HttpServletRequest request) {
super(request);
body = this.getBodyByte(request);
}
/**
* @author: 暴沸
* @email: [email protected]
* @data: 2018/2/7 16:57
* @desc: 重写getInputStream()将获取的数据重新写入以保证Controller可以正常运行
*/
@Override
// 重写getInputStream()方法,主要逻辑就是将在构造方法中读取的byte数组,重新写入,并生成一个新的ServletInputStream()流,以此方式让我们的Controller可以顺利获取到数据
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
}
/**
* @author: 暴沸
* @email: [email protected]
* @data: 2018/2/7 16:56
* @desc: 获取request对象传入的json数据并返回byte数组用于后面重新写入流
*/
private byte[] getBodyByte(ServletRequest request){
StringBuffer stringBuffer = new StringBuffer();
InputStream inputStream = null;
BufferedReader bufferedReader = null;
try{
inputStream = request.getInputStream();
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = null;
while ((line = bufferedReader.readLine()) != null) {
stringBuffer.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(null != bufferedReader){
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return stringBuffer.toString().getBytes(Charset.forName("UTF-8"));
}
}
至此我们顺利完成了整个登录态的维护demo,当然这里面还涉及到一个过滤器获取到mySessionKey以后如何从Redis数据库中去获取openID与sessionKey的问题,但这个并不是难点,也就不再赘叙了。
最后重新贴一下实现了获取json数据的过滤器代码:
package com.baofeidyz.langrenshaplusbackground2.filter;
import com.baofeidyz.langrenshaplusbackground2.util.BodyReaderHttpServletRequestWrapperUtil;
import com.baofeidyz.langrenshaplusbackground2.util.RequestJsonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
@Component
@WebFilter(urlPatterns = "/*")
public class MiniFilter implements Filter {
@Autowired
RequestJsonUtil requestJsonUtil;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//通过这行代码拿到request对象,然后传入filterChain.doFilter()方法中
HttpServletRequestWrapper httpServletRequestWrapper = new BodyReaderHttpServletRequestWrapperUtil((HttpServletRequest) servletRequest);
String json = requestJsonUtil.getJsonStr(httpServletRequestWrapper);
//这里我们就拿到了我们想要的json数据,然后我们通过mySessionKey去获取Redis数据库中的数据就可以拿到该用户的相关数据了。具体逻辑就根据大家的实际情况而定
System.out.println("json: "+json);
filterChain.doFilter(httpServletRequestWrapper,servletResponse);
}
@Override
public void destroy() {
System.out.println("destory");
}
}
平时很少写这么长的文章,如果有看不懂的地方欢迎[email protected]
参考文章:解决在Filter中读取Request中的流后,后续controller或restful接口中无法获取流的问题