1. 程式人生 > >Java Redis + Cookie + Filter 實現單點登入

Java Redis + Cookie + Filter 實現單點登入

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