Redis+Cookie+Jackson+Filter实现单点登录

前言

本篇介绍使用 Redis+Jackson+Cookie+Filter实现单点登录的功能,解决Nginx+Tomcat分布式集群Session不共享的问题。

1 封装JedisPool

Redis客户端采用Jedis

package com.kay.common;

import com.kay.util.PropertiesUtil;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * 对JedisPool的封装
 * 此处JedisPool的配置是从文件中读取的
 */
public class RedisPool {

    private static JedisPool pool;

    private static String host = PropertiesUtil.getProperty("redis1.host");

    private static int port = Integer.parseInt(PropertiesUtil.getProperty("redis1.port"));

    //最大连接数
    private static Integer maxTotal = Integer.parseInt(PropertiesUtil.getProperty("redis.maxTotal", "20"));

    //最大空闲连接数
    private static Integer maxIdle=Integer.parseInt(PropertiesUtil.getProperty("redis.maxIdle", "10"));

    //最小空闲连接数
    private static Integer minIdle=Integer.parseInt(PropertiesUtil.getProperty("redis.minIdle", "2"));

    //从连接池拿出时是否进行验证,true-验证,取出的redis连接一定可用
    private static Boolean  testOnborrow=Boolean.parseBoolean(PropertiesUtil.getProperty("redis.testOnBorrow", "true"));

    //放回连接池时是否进行验证,true-验证,放回的redis连接一定可用
    private static Boolean testOnReturn=Boolean.parseBoolean(PropertiesUtil.getProperty("redis.testOnReturn", "false"));

    private static void initPool() {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(maxTotal);
        config.setMaxIdle(maxIdle);
        config.setMinIdle(minIdle);
        config.setBlockWhenExhausted(true); //资源耗尽时是否阻塞
        config.setTestOnBorrow(testOnborrow);
        config.setTestOnReturn(testOnReturn);

        pool = new JedisPool(config, host, port, 1000 * 2);
    }

    static {
        initPool();
    }

    public  static Jedis getJedis(){
        return pool.getResource();
    }

    public static void returnBrokenResource(Jedis jedis){
        pool.returnBrokenResource(jedis);
    }

    public static void returnResource(Jedis jedis){
        pool.returnResource(jedis);
    }

}

2 封装Jedis API的操作

package com.kay.util;

import com.kay.common.RedisPool;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;

/**
 *  对Jedis API 的一些封装
 * 日志框架采用logback,注解为lombok
 */
@Slf4j
public class RedisPoolUtil {

    public static String set(String key,String value){
        Jedis jedis = null;
        String result=null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.set(key, value);
        } catch (Exception e) {
            log.error("set key:{} value:{} error",key,value,e);
            RedisPool.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }

    public static String get(String key){
        Jedis jedis = null;
        String result=null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.get(key);
        } catch (Exception e) {
            log.error("get key:{} error",key,e);
            RedisPool.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }

    /**
     *
     * @param key
     * @param exTime seconds
     * @param value
     * @return
     */
    public static String setEx(String key,String value,int exTime){
        Jedis jedis = null;
        String result=null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.setex(key, exTime, value);
        } catch (Exception e) {
            log.error("setex key:{} value:{} error",key,value,e);
            RedisPool.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }

    /**
     *
     * @param key
     * @param exTime seconds
     * @return 1: success 0:fail
     */
    public static Long expire(String key,int exTime){
        Jedis jedis = null;
        Long result=null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.expire(key, exTime);
        } catch (Exception e) {
            log.error("expire key:{} error",key,e);
            RedisPool.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }

    public static Long del(String key){
        Jedis jedis = null;
        Long result=null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.del(key);
        } catch (Exception e) {
            log.error("del key:{} error",key,e);
            RedisPool.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }
}

3 Jackson 序列化工具封装

由于存入Redis中的对象为json字符串格式,故使用Jackson封装一个对象与json转换工具

package com.kay.util;

import com.kay.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.codehaus.jackson.type.JavaType;
import org.codehaus.jackson.type.TypeReference;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;

/**
 * 对 Jackson 序列化的封装
 */
@Slf4j
public class JsonUtil {

    private static ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 序列化与反序列化的全局配置
     */
    static {
        //序列化时包括所有字段
        objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.ALWAYS);

        //取消默认转换日期为时间戳的设置
        objectMapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS,false);

        //格式化时间日期
        objectMapper.setDateFormat(new SimpleDateFormat(DateTimeUtil.STANDARD_FORMAT_STR));

        //忽略空bean转json的报错,默认情况下 empty bean 会报错
        objectMapper.configure(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS, false);

        //忽略在json中存在属性却在bean无对应属性转换时报错
        objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    public static <T> String  obj2string(T obj){
        if (obj == null) {
            return null;
        }
        try {
            return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
        } catch (Exception e) {
            log.warn("parse obj2string warn",e);
            return null;
        }
    }

    /**
     * 格式化json字符串
     * @param obj
     * @param <T>
     * @return
     */
    public static <T> String  obj2stringPretty(T obj){
        if (obj == null) {
            return null;
        }
        try {
            return obj instanceof String ? (String) obj : objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
        } catch (Exception e) {
            log.warn("parse obj2string warn",e);
            return null;
        }
    }

    /**
     * 转换单个对象,无法正确处理集合
     * @param str
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T string2obj(String str, Class<T> clazz) {
        if (StringUtils.isEmpty(str) || clazz == null) {
            return null;
        }
        try {
            return clazz.equals(String.class) ? (T)str :  objectMapper.readValue(str, clazz);
        } catch (Exception e) {
            log.warn("parse string2obj warn",e);
            return null;
        }
    }

    /**
     * 通过 TypeReference 指定要返回的集合对象
     * @param str
     * @param typeReference
     * @param <T>
     * @return
     */
    public static <T> T string2obj(String str, TypeReference<T> typeReference){
        if (StringUtils.isEmpty(str) || typeReference == null) {
            return null;
        }
        try {
            return (T)(typeReference.getType().equals(String.class) ? str : objectMapper.readValue(str, typeReference));
        } catch (Exception e) {
            log.warn("parse string2obj warn",e);
            return null;
        }
    }

    /**
     * 通过传入多个class对象来进行类型转换
     * @param str
     * @param collectionClass
     * @param classes
     * @param <T>
     * @return
     */
    public static <T> T string2obj(String str, Class<?> collectionClass,Class<?>... classes){
        if (StringUtils.isEmpty(str) || collectionClass == null || classes==null) {
            return null;
        }
        JavaType javaType = objectMapper.getTypeFactory().constructParametricType(collectionClass, classes);
        try {
            return objectMapper.readValue(str, javaType);
        } catch (Exception e) {
            log.warn("parse string2obj warn",e);
            return null;
        }
    }
}

4 Cookie读写的封装

登录成功时,将token写入本地cookie以及redis中,再次登入时检测cookie中存放的token是否存在

package com.kay.util;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 对 cookie 操作的封装
 */
@Slf4j
public class CookieUtil {

    //cookie 写入域
    private final static String COOKIE_DOMAIN = ".kaymmall.com"; //一级域名

    //login token
    private final static String COOKIE_NAME = "mmall_login_token"; //cookieName

    /**
     * 写入login cookie
     * @param response
     * @param token
     */
    public static void writeLoginToken(HttpServletResponse response,String token) {
        Cookie cookie = new Cookie(COOKIE_NAME,token);
        cookie.setDomain(COOKIE_DOMAIN);
        cookie.setPath("/");
        cookie.setHttpOnly(true);

        //不设置maxAge 只会存在于内存,不会写入硬盘
        cookie.setMaxAge(3600*24*10); //有效期10天
        log.info("write cookieName:{} ,cookieValue:{}",cookie.getName(),cookie.getValue());
        response.addCookie(cookie);
    }

    /**
     * 读取 login cookie
     * @param request
     * @return
     */
    public static String readLoginToken(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie ck : cookies) {
                log.info("read cookieName:{},cookieValue:{}", ck.getName(), ck.getValue());
                if (StringUtils.equals(ck.getName(), COOKIE_NAME)) {
                    log.info("return cookieName:{},cookieValue:{}", ck.getName(), ck.getValue());
                    return ck.getValue();
                }
            }
        }
        return null;
    }

    /**
     * 删除cookie
     * @param request
     * @param response
     */
    public static void delLoginToken(HttpServletRequest request, HttpServletResponse response) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie ck : cookies) {
                log.info("read cookieName:{},cookieValue:{}", ck.getName(), ck.getValue());
                if (StringUtils.equals(ck.getName(), COOKIE_NAME)) {
                    ck.setDomain(COOKIE_DOMAIN); //注意配置域名之后,在本机用localhost测试不生效,需要修改host,用域名来访问
                    ck.setPath("/");
                    ck.setMaxAge(0); //立即删除cookie

                    log.info("del cookieName:{},cookieValue:{}", ck.getName(), ck.getValue());
                    response.addCookie(ck);
                    return;
                }
            }
        }
    }
}

5 封装Filter刷新登录Session时间

由于用户登录成功后,session的过期时间为30分钟,在用户每次进行操作之后需要对session过期时间进行重置。
编写完成之后再wen.xml中配置Filter。

package com.kay.controller.common;

import com.kay.common.Const;
import com.kay.pojo.User;
import com.kay.util.CookieUtil;
import com.kay.util.JsonUtil;
import com.kay.util.RedisShardedPoolUtil;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * 重置session过期时间
 */
public class SessionExpireFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    /**
     * 判断用户是否登录,登录则重置redis里的session时间
     * 1.读取cookie中的loginToken
     * 2.token判空,从redis中获取user信息
     * 3.user判空,刷新redis中对于key的过期时间
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String loginToken = CookieUtil.readLoginToken(request);
        if (StringUtils.isNotEmpty(loginToken)) {
            String userStr = RedisShardedPoolUtil.get(loginToken);
            User user = JsonUtil.string2obj(userStr, User.class);
            if (user != null) {
                RedisShardedPoolUtil.expire(loginToken, Const.RedisCacheExTime.REDIS_SESSION_EXTIME);
            }
        }
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {
    }
}

6 测试

其他业务代码省略,思路就是凡是在单服务器时采用HttpSession 存放用户信息的地方替换为Cookie验证+Redis读取和存放。

配置Nginx 反向代理2个Tomcat服务器,分别启动2个Web服务器。

进入统一域名访问,如 www.kaymmall.com ,若此时访问tomcat1,进行登录成功,cookie写入本地,同时存入统一的Session管理中心(Redis),刷新浏览器,Nginx进行跳转到tomcat2,此时浏览器带着本地cookie进行访问,cookie有上次访问的token(比如是sessionid或者自定义的uuid等),服务器读取到cookie中存放的token到session管理中心(redis)进行认证,获取用户信息,验证完毕即已登录成功。

以下是示例代码。

示例登录代码:

/**
     *用户登录
     * @param username
     * @param password
     * @param session
     * @return
     */
    @RequestMapping(value = "login.do",method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<User> login(String username, String password, HttpSession session,HttpServletResponse httpServletResponse){
        ServerResponse<User> response = iUserService.login(username, password);
        if (response.isSuccess()) {
//            session.setAttribute(Const.CURRENT_USER,response.getData());
            //写入cookie
            CookieUtil.writeLoginToken(httpServletResponse,session.getId());
            //将登录用户信息存入redis,有效时间为30分钟
            RedisShardedPoolUtil.setEx(session.getId(), JsonUtil.obj2string(response.getData()), Const.RedisCacheExTime.REDIS_SESSION_EXTIME);
        }
        return response;
    }

示例其他需要用户登录的操作:

/**
     * 获取当前用户信息
     */
    @RequestMapping(value = "get_user_info.do",method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<User> getUserInfo(HttpServletRequest request) {
//        User user = (User) session.getAttribute(Const.CURRENT_USER);
        String loginToken = CookieUtil.readLoginToken(request);
        if (StringUtils.isEmpty(loginToken)) {
            return ServerResponse.createByErrorMessage("用户未登录,无法获取当前用户的信息");
        }
        User user = JsonUtil.string2obj(RedisShardedPoolUtil.get(loginToken), User.class);
        if(user != null){
            return ServerResponse.createBySuccess(user);
        }
        return ServerResponse.createByErrorMessage("用户未登录,无法获取当前用户的信息");
    }

7 改进-Redis分布式

以上方式封装的Redis操作均是对单服务器Redis的操作,一方面是数据容量有限,另一方面的可扩展性不强,一般情况下会采用分布式Redis便于扩展和维护。

假设有2台Redis服务器作为Session的集中管理中心,下面利用Jedis提供的客户端分片能力进行存取(ShardedJedis)

使用时将单服务器封装的RedisPoolUtil替换为RedisShardedPoolUtil即可,若需要扩展,添加节点到RedisShardedPool中,jedis客户端会采用一致性哈希算法进行数据迁移。

注:Jedis客户端的分片策略及虚拟节点的配置可在Sharded类源码中查看。

1 重新封装RedisShardedPool

package com.kay.common;

import com.kay.util.PropertiesUtil;
import redis.clients.jedis.*;
import redis.clients.util.Hashing;
import redis.clients.util.Sharded;

import java.util.ArrayList;
import java.util.List;

/**
 * 分布式Redis分片解决方案
 */
public class RedisShardedPool {

    private static ShardedJedisPool pool;
    //redis1节点
    private static String host1 = PropertiesUtil.getProperty("redis1.host");

    private static int port1 = Integer.parseInt(PropertiesUtil.getProperty("redis1.port"));
    //redis2节点
    private static String host2 = PropertiesUtil.getProperty("redis2.host");

    private static int port2 = Integer.parseInt(PropertiesUtil.getProperty("redis2.port"));

    //最大连接数
    private static Integer maxTotal = Integer.parseInt(PropertiesUtil.getProperty("redis.maxTotal", "20"));

    //最大空闲连接数
    private static Integer maxIdle=Integer.parseInt(PropertiesUtil.getProperty("redis.maxIdle", "10"));

    //最小空闲连接数
    private static Integer minIdle=Integer.parseInt(PropertiesUtil.getProperty("redis.minIdle", "2"));

    //从连接池拿出时是否进行验证,true-验证,取出的redis连接一定可用
    private static Boolean  testOnborrow=Boolean.parseBoolean(PropertiesUtil.getProperty("redis.testOnBorrow", "true"));

    //放回连接池时是否进行验证,true-验证,放回的redis连接一定可用
    private static Boolean testOnReturn=Boolean.parseBoolean(PropertiesUtil.getProperty("redis.testOnReturn", "false"));

    private static void initPool() {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(maxTotal);
        config.setMaxIdle(maxIdle);
        config.setMinIdle(minIdle);
        config.setBlockWhenExhausted(true); //资源耗尽时是否阻塞
        config.setTestOnBorrow(testOnborrow);
        config.setTestOnReturn(testOnReturn);

        JedisShardInfo info1 = new JedisShardInfo(host1, port1,1000*2,2);  //超时时间默认是2s
        JedisShardInfo info2 = new JedisShardInfo(host2, port2,1000*2,1);  //分布权重

        List<JedisShardInfo> jedisShardInfoList = new ArrayList<>(2);
        jedisShardInfoList.add(info1);
        jedisShardInfoList.add(info2);

        //Hashing.MURMUR_HASH 一致性hash算法分片,Sharded中有默认分配虚拟节点策略
        pool = new ShardedJedisPool(config, jedisShardInfoList, Hashing.MURMUR_HASH, Sharded.DEFAULT_KEY_TAG_PATTERN);
    }

    static {
        initPool();
    }

    public  static ShardedJedis getJedis(){
        return pool.getResource();
    }

    public static void returnBrokenResource(ShardedJedis jedis){
        pool.returnBrokenResource(jedis);
    }

    public static void returnResource(ShardedJedis jedis){
        pool.returnResource(jedis);
    }
}

2 ShardedJedis封装

对RedisPoolUtil中Jedis全部替换为ShardedJedis

package com.kay.util;

import com.kay.common.RedisShardedPool;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ShardedJedis;

/**
 * 对分布式Jedis API 的一些封装
 */
@Slf4j
public class RedisShardedPoolUtil {

    public static String set(String key,String value){
        ShardedJedis jedis = null;
        String result=null;
        try {
            jedis = RedisShardedPool.getJedis();
            result = jedis.set(key, value);
        } catch (Exception e) {
            log.error("set key:{} value:{} error",key,value,e);
            RedisShardedPool.returnBrokenResource(jedis);
            return result;
        }
        RedisShardedPool.returnResource(jedis);
        return result;
    }

    public static String get(String key){
        ShardedJedis jedis = null;
        String result=null;
        try {
            jedis = RedisShardedPool.getJedis();
            result = jedis.get(key);
        } catch (Exception e) {
            log.error("get key:{} error",key,e);
            RedisShardedPool.returnBrokenResource(jedis);
            return result;
        }
        RedisShardedPool.returnResource(jedis);
        return result;
    }

    /**
     *
     * @param key
     * @param exTime seconds
     * @param value
     * @return
     */
    public static String setEx(String key,String value,int exTime){
        ShardedJedis jedis = null;
        String result=null;
        try {
            jedis = RedisShardedPool.getJedis();
            result = jedis.setex(key, exTime, value);
        } catch (Exception e) {
            log.error("setex key:{} value:{} error",key,value,e);
            RedisShardedPool.returnBrokenResource(jedis);
            return result;
        }
        RedisShardedPool.returnResource(jedis);
        return result;
    }

    /**
     *
     * @param key
     * @param exTime seconds
     * @return 1: success 0:fail
     */
    public static Long expire(String key,int exTime){
        ShardedJedis jedis = null;
        Long result=null;
        try {
            jedis = RedisShardedPool.getJedis();
            result = jedis.expire(key, exTime);
        } catch (Exception e) {
            log.error("expire key:{} error",key,e);
            RedisShardedPool.returnBrokenResource(jedis);
            return result;
        }
        RedisShardedPool.returnResource(jedis);
        return result;
    }

    public static Long del(String key){
        ShardedJedis jedis = null;
        Long result=null;
        try {
            jedis = RedisShardedPool.getJedis();
            result = jedis.del(key);
        } catch (Exception e) {
            log.error("del key:{} error",key,e);
            RedisShardedPool.returnBrokenResource(jedis);
            return result;
        }
        RedisShardedPool.returnResource(jedis);
        return result;
    }
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值