【狼人杀plus全记录】SpringBoot结合Redis实现微信小程序登录态维护

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数据库可以设置数据的有效期,超过时间则会自动删除。


具体实现

第一步,导入spring boot 对 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下,如果需要查看官方文档的朋友可以注意一下。

第二步,添加Redis数据库配置

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注入的对象即可。

第三步,完成一个对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: [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接口中无法获取流的问题

你可能感兴趣的:(springboot,redis,微信小程序)