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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35704236/article/details/80427480

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

1 缘起

分布式系统中需要一个单点登录系统,所以打算用 redis + cookie + filter 实现单点登录系统


2 大体思路

  • 1 登录的时候

1.0 进行正常用户登录流程 获取 User 对象

1.1 生成唯一id (token)可以用 uuid 或者 session.getId()

1.2 设置指定的 cookie name 和 cookie value = token ,并且将 cookie 写给客户端

1.3 在 redis 中 设置 key = tokenvalue = user(将用户对象序列化成json),并且设置过期时间
  • 2 获取用户信息的时候
1.0 从请求中获取 cooke ,从 cookie 中 获取 token

1.1 根据 token ,从 redis 中获取用户信息字符串,并且反序列化成对象
  • 3 退出登录的时候

1.0 从请求中获取 cooke ,从 cookie 中 获取 token

1.1 删除 浏览器端的 cookie 

1.2 删除 redis 中的 token
  • 4 访问需要用户的权限的借口的时候 延长 token 有效期

1.0 实现过滤器, 或者拦截器, 或者使用切面编程

1.1 获取请求中的 cookie , 从 cookie 中获取 token1.3 延长 token 有效期

3 撸起袖子干

3.1 登录的时候


...业务代码 获取用户的 user 对象


 // 将 sessionId (token), 写一个 cookie 给浏览器
 CookieUtil.writeLoginToken(httpServletResponse, session.getId());

 // 将 sessionId (token) ,user 存储在 redis 中
 RedisPoolUtil.setEx(session.getId(), JsonUtil.objToString(user), Constants.RedisCacheExtime.REDIS_SESSION_EXTIME);

3.2 获取用户信息


 String loginToken = CookieUtil.readLoginToken(request);

 if (StringUtils.isBlank(loginToken)) {
     return ServerResponse.createByErrorMessage("用户未登录, 无法获取用户信息");
 } else {
     String userJsonStr = RedisPoolUtil.get(loginToken);
     User user = JsonUtil.stringToObj(userJsonStr, User.class);

     if (user != null) {
        return ServerResponse.createBySuccess(user);
     } else {
        return ServerResponse.createByErrorMessage("用户未登录, 无法获取用户信息");
     }
 }

3.3 退出登录

String token = CookieUtil.readLoginToken(request);
CookieUtil.delLoginToken(request, httpServletResponse);
RedisPoolUtil.del(token);

3.4 在访问需要用户权限的接口前后,延长 token 时效,这里使用过滤器

public class SessionExpireFilter implements Filter {

    ...

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;

        String token = CookieUtil.readLoginToken(httpServletRequest);

        if (StringUtils.isNotBlank(token)) {
            String userJsonStr = RedisPoolUtil.get(token);
            User user = JsonUtil.stringToObj(userJsonStr, User.class);

            if (user != null) {
                // 重置 session 有效期
                RedisPoolUtil.expire(token, Constants.RedisCacheExtime.REDIS_SESSION_EXTIME);
            }
        }

        filterChain.doFilter(httpServletRequest, servletResponse);
    }

    ...

}
  • 配置 web.xml
<!-- 重置 session 的 filter -->
  <filter>
    <filter-name>sessionExpireFilter</filter-name>
    <filter-class>com.mmall.controller.common.SessionExpireFilter</filter-class>
  </filter>

  <!-- 拦截 .do 结尾的 -->
  <filter-mapping>
    <filter-name>sessionExpireFilter</filter-name>
    <url-pattern>*.do</url-pattern>
  </filter-mapping>

4 需要的工具类

4.1 CookieUtil


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

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

/**
 * Created by jun on 2018/5/21.
 */
@Slf4j
public class CookieUtil {

    private static final String COOKIE_DOMAIN = ".happymmall.com";

    private static final String COOKIE_NAME = "mmall_login_token";

    public static void writeLoginToken(HttpServletResponse response, String token) {
        Cookie cookie = new Cookie(COOKIE_NAME, token);
        cookie.setDomain(COOKIE_DOMAIN);
        cookie.setPath("/");
        // 防止脚本攻击,不允许脚本访问 cookie
        cookie.setHttpOnly(true);

        // -1 代表永不过期, 单位 秒 如果 maxage cookie 则不会写入硬盘,只写入内存, 只在当前页面有效
        cookie.setMaxAge(60 * 60 * 24 * 30);

        log.info("write cookie cookieName:" + cookie.getName() + " cookieValue:" + cookie.getValue());
    }

    public static String readLoginToken(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();

        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (StringUtils.equals(cookie.getName(), COOKIE_NAME)) {
                    return cookie.getValue();
                }
            }
        }

        return null;
    }

    public static void delLoginToken (HttpServletRequest request, HttpServletResponse response) {
        Cookie[] cookies = request.getCookies();

        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (StringUtils.equals(cookie.getName(), COOKIE_NAME)) {
                    cookie.setDomain(COOKIE_DOMAIN);
                    cookie.setPath("/");

                    // 設置為 0 代表刪除
                    cookie.setMaxAge(0);
                    response.addCookie(cookie);

                    return;
                }
            }
        }
    }
}

4.2 RedisPoolUtil

package com.mmall.util;


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

/**
 * Created by jun on 2018/5/3.
 */
@Slf4j
public class RedisPoolUtil {

    /**
     * 设置 key 有效期
     *
     * @param key
     * @param exTime
     * @return
     */
    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("jedis expire error", e);
            RedisPool.returnBrokenResource(jedis);
        }

        RedisPool.returnResource(jedis);
        return result;
    }

    /**
     * 带有过期时间的 set
     * @param key
     * @param value
     * @param exTime 秒
     * @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("jedis setEx error", e);
            RedisPool.returnBrokenResource(jedis);
        }

        RedisPool.returnResource(jedis);
        return result;
    }

    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("jedis set error", e);
            RedisPool.returnBrokenResource(jedis);
        }

        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("jedis get error", e);
            RedisPool.returnBrokenResource(jedis);
        }

        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("jedis del error", e);
            RedisPool.returnBrokenResource(jedis);
        }

        RedisPool.returnResource(jedis);
        return result;
    }
}

4.3 RedisPool

package com.mmall.common;

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

/**
 * Created by jun on 2018/5/2.
 */
public class RedisPool {

    // jedis 连接池
    private static JedisPool jedisPool;

    // 最大连接数
    private static Integer maxTotal = Integer.valueOf(PropertiesUtil.getProperty("redis.max.total"));

    // 最大空闲连接数
    private static Integer maxIdle = Integer.valueOf(PropertiesUtil.getProperty("redis.max.idel"));

    // 最小空闲连接数
    private static Integer minIdle = Integer.valueOf(PropertiesUtil.getProperty("redis.min.idel"));

    // 测试jedis实例是否可用,在 borrow 的时候
    private static Boolean testOnBorrow = Boolean.valueOf(PropertiesUtil.getProperty("redis.test.borrow"));

    // 测试jedis实例是否可用,在 return 的时候
    private static Boolean testOnReturn = Boolean.valueOf(PropertiesUtil.getProperty("redis.test.return"));

    // ip
    private static String ip = PropertiesUtil.getProperty("redis.ip");
    // port
    private static Integer port = Integer.valueOf(PropertiesUtil.getProperty("redis.port"));

    public static void initPool() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(maxTotal);
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMinIdle(minIdle);
        jedisPoolConfig.setTestOnBorrow(testOnBorrow);
        jedisPoolConfig.setTestOnReturn(testOnReturn);

        // 连接耗尽时,是否阻塞,false会抛出异常,true阻塞直到超时,默认 true
        jedisPoolConfig.setBlockWhenExhausted(true);

        jedisPool = new JedisPool(jedisPoolConfig, ip, port, 1000 * 2);
    }

    static {
        initPool();
    }

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

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

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

4.4 JsonUtil

package com.mmall.util;

import com.mmall.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.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.Inclusion;
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;

/**
 * Created by jun on 2018/5/15.
 */
@Slf4j
public class JsonUtil {

    private static final String STANDARD_FORMAT = "yyyy-MM-dd HH:mm:ss";

    private static ObjectMapper objectMapper = new ObjectMapper();

    static {
        // 对象的所有字段全部列入
        objectMapper.setSerializationInclusion(Inclusion.ALWAYS);
        // 取消默认转换 timestamp 形式
        objectMapper.configure(SerializationConfig.Feature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
        // 忽略空 bean 转 json 的错误
        objectMapper.configure(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS, false);
        // 所有的日期 统一为以下格式
        objectMapper.setDateFormat(new SimpleDateFormat(STANDARD_FORMAT));

        // 忽略 在 json字符串中存在, 但是在 Java 对象中不存在对应属性的情况。防止错误。
        objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    /**
     * java 对象转 字符串
     * @param obj
     * @param <T>
     * @return
     */
    public static <T> String objToString(T obj) {
        if (obj == null) {
            return null;
        }

        try {
            return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
        } catch (Exception e) {
            log.warn("parse error", e);
            return null;
        }
    }

    /**
     * java 对象转 字符串, 返回格式化好的字符串
     * @param obj
     * @param <T>
     * @return
     */
    public static <T> String objToStringPretty(T obj) {
        if (obj == null) {
            return null;
        }

        try {
            return obj instanceof String ? (String) obj : objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
        } catch (Exception e) {
            log.warn("parse error", e);
            return null;
        }
    }

    /**
     * 字符串转对象
     *
     * @param str
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T stringToObj(String str, Class<T> clazz) {
        // 第一个 T 是将方法声明成泛形方法
        // 第二个 T 是 返回值类型
        // 第三个 T 是 入参类型
        if (StringUtils.isBlank(str) || clazz == null) {
            return null;
        }

        try {
            return clazz.equals(String.class) ? (T) str : objectMapper.readValue(str, clazz);
        } catch (IOException e) {
            log.warn("parse error", e);
            return null;
        }
    }

    /**
     * json 字符串转对象
     * @param str
     * @param typeReference
     * @param <T>
     * @return
     */
    public static <T> T stringToObj(String str, TypeReference<T> typeReference) {
        if (StringUtils.isBlank(str) || typeReference == null) {
            return null;
        }

        try {
            return (T) (typeReference.getType().equals(String.class) ?  str : objectMapper.readValue(str, typeReference));
        } catch (IOException e) {
            log.warn("parse error", e);
            return null;
        }
    }

    /**
     * json 字符串转对象
     * @param str
     * @param collectionClass
     * @param elementClasses
     * @param <T>
     * @return
     */
    public static <T> T stringToObj(String str, Class<?> collectionClass, Class<?>... elementClasses) {
        JavaType javaType = objectMapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);

        try {
            return objectMapper.readValue(str, javaType);
        } catch (IOException e) {
            log.warn("parse error", e);
            return null;
        }
    }
}

4.5 PropertiesUtil 用于读取配置

package com.mmall.util;


import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;


/**
 * Created by jun on 2018/4/8.
 */
public class PropertiesUtil {

    private static Logger logger = LoggerFactory.getLogger(PropertiesUtil.class);

    private static Properties properties;

    static {
        String fileName = "mmall.properties";
        properties = new Properties();
        try {
            properties.load(new InputStreamReader(PropertiesUtil.class.getClassLoader().getResourceAsStream(fileName), "UTF-8"));
        } catch (IOException e) {
            logger.error("config file read fail");
        }
    }

    public static String getProperty(String key) {
        if (StringUtils.isNotBlank(key)) {
            String value = properties.getProperty(key.trim());

            if (StringUtils.isNotBlank(key)) {
                return value.trim();
            } else {
                return value;
            }
        } else {
            return null;
        }
    }

}

4.6 redis 连接池配置

# redis config start
redis.max.total=20
redis.max.idel=10
redis.min.idel=2
redis.test.borrow=true
redis.test.return=true

redis.ip=localhost
redis.port=6379
# redis config end

猜你喜欢

转载自blog.csdn.net/qq_35704236/article/details/80427480