.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数据库可以设置数据的有效期,超过时间则会自动删除。
具体实现
第一步,导入spring boot 对 redis 的支持
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>1.5.10.RELEASE</version></dependency>
注:SpringBoot在8月的时候就抛弃了spring-boot-starter-redis转而使用spring-boot-starter-data-redis 并且相关资料全部在SpringData下而不是SpringBoot下,如果需要查看官方文档的朋友可以注意一下。
第二步,添加Redis数据库配置
SpringBoot支持 application.yml 和 application.properties 两种文件类型,这里我使用的是 application.yml 的文件类型
spring:redis:host: localhost #改成自己的IP地址,如果是本地就localhostport: 6379 #默认Redis数据库端口password: password #Redis数据库的密码,需要在Redis配置文件中配置好,如果没有密码就删掉database: 0pool:max-active: 8max-wait: -1max-idle: 500min-idle: 0timeout: 0
这样弄好就可以了,SpringBoot会为我们自动帮我们把参数注入到 StringRedisTemplate 对象中,我们只需要使用 @Autowired 注解取出被Spring注入的对象即可。
第三步,完成一个对Redis操作的DAO类
先看代码:
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: baofeidyz@* @data: /2/6 10:09* @desc: Redis数据库操作实例化对象*/@Repositorypublic class RedisDaoImpl implements RedisDao{// 通过Autowired注解拿到SpringBoot帮我们注入好参数的StringRedisTemplate 对象@Autowiredprivate 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<String,String> 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<String,String> 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.ponent;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 {@AutowiredRequestJsonUtil requestJsonUtil;@Overridepublic void init(FilterConfig filterConfig) throws ServletException {System.out.println("init");}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {//TODO 在这里写具体的过滤逻辑filterChain.doFilter(servletRequest,servletResponse);}@Overridepublic 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.ponent;import javax.servlet.http.HttpServletRequest;import java.io.IOException;import java.io.UnsupportedEncodingException;@Componentpublic 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: baofeidyz@* @data: /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() {@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener readListener) {}@Overridepublic int read() throws IOException {return byteArrayInputStream.read();}};}/*** @author: 暴沸* @email: baofeidyz@* @data: /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.ponent;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 {@AutowiredRequestJsonUtil requestJsonUtil;@Overridepublic void init(FilterConfig filterConfig) throws ServletException {System.out.println("init");}@Overridepublic 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);}@Overridepublic void destroy() {System.out.println("destory");}}
平时很少写这么长的文章,如果有看不懂的地方欢迎baofeidyz@
参考文章:解决在Filter中读取Request中的流后,后续controller或restful接口中无法获取流的问题