版权声明:本文为博主原创文章,未经博主允许不得转载。 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 = 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