Java Redis + Cookie + Filter 實現單點登入
阿新 • • 發佈:2018-11-19
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 = token ,value = 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 中獲取 token,
1.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