版权声明:本文转载。 https://blog.csdn.net/sinat_25295611/article/details/80406172
目录
前言
本篇介绍使用 Redis+Jackson+Cookie+Filter原生实现单点登录的功能,采用这种方式实现的单点登录功能对业务代码有一定的侵入,不过封装好的组件可重复利用,可定制性也比较高。后面将会介绍使用Spring 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;
/**
* Created by kay on 2018/5/13.
* 对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;
/**
* Created by kay on 2018/5/16.
* 对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;
/**
* Created by kay on 2018/5/16.
* 对 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;
/**
* Created by kay on 2018/5/16.
* 对 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过期时间进行重置
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;
/**
* Created by kay on 2018/5/16.
* 重置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;
/**
* Created by kay on 2018/5/21.
* 分布式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;
/**
* Created by kay on 2018/5/21.
* 对分布式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;
}
}