Redis+Cookie+Jackson+Filter實現單點登入
1.Redis連線池構建
首先maven匯入依賴包
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.6.0</version> </dependency>
下面正式寫一個連線池
public class RedisPool { private static JedisPool pool;//jedis連線池private static Integer maxTotal = Integer.valueOf(PropertiesUtil.getProperty("redis.max.total","20")); //最大連線數 private static Integer maxIdle = Integer.valueOf(PropertiesUtil.getProperty("redis.max.idle","10"));//在jedispool中最大的idle狀態(空閒的)的jedis例項的個數 private static Integer minIdle = Integer.valueOf(PropertiesUtil.getProperty("redis.min.idle","2"));//在jedispool中最小的idle狀態(空閒的)的jedis例項的個數 private static Boolean testOnBorrow = Boolean.valueOf(PropertiesUtil.getProperty("redis.test.borrow","true"));//在borrow一個Jedis例項的時候,是否要進行驗證操作,如果賦值true,那麼得到的jedis例項肯定是可用的 private static Boolean testOnReturn = Boolean.valueOf(PropertiesUtil.getProperty("redis.test.return","true"));//在borrow一個Jedis例項的時候,是否要進行驗證操作,如果賦值true,則放回jedispool的jedis例項肯定是可用的 private static String redisIp = PropertiesUtil.getProperty("redis.ip");//在jedispool中最大的idle狀態(空閒的)的jedis例項的個數 private static Integer redisPort = Integer.valueOf(PropertiesUtil.getProperty("redis.port"));//在jedispool中最小的idle狀態(空閒的)的jedis例項的個數 private static void initPool(){ JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(maxTotal); config.setMaxIdle(maxIdle); config.setMinIdle(minIdle); config.setTestOnBorrow(testOnBorrow); config.setTestOnReturn(testOnReturn); config.setBlockWhenExhausted(true);//連線耗盡的時候是否阻塞,true:阻塞,false:丟擲異常 pool = new JedisPool(config,redisIp,redisPort,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); } public static void main (String[] args){ //Jedis jedis = pool.getResource(); Jedis jedis = RedisPool.getJedis(); jedis.set("key","value"); RedisPool.returnResource(jedis); pool.destroy();//臨時呼叫 System.out.println("program is end"); } }
程式碼內的PropertiesUtil是一個讀取properties的工具類
public class PropertiesUtil { private static Logger logger = LoggerFactory.getLogger(PropertiesUtil.class); private static Properties props; static { String fileName = "zfb.properties"; props = new Properties(); try { props.load(new InputStreamReader(PropertiesUtil.class.getClassLoader().getResourceAsStream(fileName),"UTF-8")); } catch (IOException e) { logger.error("配置檔案讀取異常",e); } } public static String getProperty(String key){ String value = props.getProperty(key.trim()); if(StringUtils.isBlank(value)){ return null; } return value.trim(); } public static String getProperty(String key,String defaultValue){ String value = props.getProperty(key.trim()); if(StringUtils.isBlank(value)){ value = defaultValue; } return value.trim(); } }
與redis的連線搞定了,那接下來就要封裝一下操作redis的方法了
2.Jedis API封裝
@Slf4j public class RedisPoolUtil { /*設定key的有效期,單位是秒*/ 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:{} exTime:{} error",key,exTime,e); RedisPool.returnBrokenResource(jedis); return result; } RedisPool.returnResource(jedis); return result; } //exTime的單位是秒 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:{} exTime:{} value:{} error",key,exTime,value,e); RedisPool.returnBrokenResource(jedis); return result; } 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("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; } 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; } public static void main (String[] args){ Jedis jedis = RedisPool.getJedis(); RedisShardedPoolUtil.set("keyTest","value"); String value = RedisShardedPoolUtil.get("keyTest"); RedisShardedPoolUtil.setEx("kevex","valueex",60*10); RedisShardedPoolUtil.expire("keyTest",60*20); RedisShardedPoolUtil.del("keyTest"); System.out.println("end"); } }
類裡面封裝了一些常用的操作redis的方法,set(),設定鍵值;del(),刪除鍵值;setEx(),set的時候就把生存時間設定了等等
3.JSONUtil封裝
RedisPoolUtil 工具類是對String操作的,但是我們在登入的時候,使用者User是一個物件,裡面有具體的引數,那麼我們就需要把這個物件User序列化,然後儲存到redis裡面
<dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-mapper-asl</artifactId> <version>1.9.12</version> </dependency>
public class JsonUtil { private static ObjectMapper objectMapper = new ObjectMapper(); static { //物件的所有欄位全部列入 objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.ALWAYS); //取消預設轉換timestamps形式 objectMapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS,false); //忽略空bean轉json的錯誤 objectMapper.configure(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS,false); //所有日期格式都統一為一下格式,即yyyy-MM--dd HH:mm:ss objectMapper.setDateFormat(new SimpleDateFormat(DateTimeUtil.STANDARD_FORMAT)); //忽略 在json字串中存在,但在Java物件中不存在對應屬性的情況,防止錯誤 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 object to String error",e); return null; } } 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 object to String error",e); return null; } } 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 (IOException e) { log.warn("Parse String to Object error",e); return null; } } public static <T> T string2Obj(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 String to Object error",e); return null; } } 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 (IOException e) { log.warn("Parse String to Object error",e); return null; } } public static void main (String[] args){ User u1 = new User(); u1.setId(1); u1.setEmail("[email protected]"); User u2 = new User(); u2.setId(2); u2.setEmail("[email protected]"); String user1Json = JsonUtil.obj2String(u1); String user1JsonPretty = JsonUtil.obj2StringPretty(u1); log.info("user1Json:{}",user1Json); log.info("user1JsonPretty:{}",user1JsonPretty); User user = JsonUtil.string2Obj(user1Json,User.class); List<User> userList = Lists.newArrayList(); userList.add(u1); userList.add(u2); String userListStr = JsonUtil.obj2StringPretty(userList); log.info("=================="); log.info(userListStr); List<User> userListObj1 = JsonUtil.string2Obj(userListStr, new TypeReference<List<User>>() { }); List<User> userListObj2 = JsonUtil.string2Obj(userListStr,List.class,User.class); System.out.println("end"); } }
準備工作做完以後,就是使用redis來儲存session了
4.單點登入Redis儲存Session
以下程式碼是以單機多部署兩個tomcat叢集為基礎的,如果只是一個tomcat,那麼執行程式碼
RedisShardedPoolUtil.setEx(session.getId(), JsonUtil.obj2String(serverResponse.getData()),Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
瀏覽器端cookie的value值和redis儲存的key值肯定是一樣的。
如果是兩個tomcat,使用tomcat1登入,那麼Cookie有了值,且儲存到redis上了,再次發起請求,負載均衡到tomcat2上了,那麼這個時候session.getId()得到的值在redis上是沒有這個key,就會認為使用者沒有登入
@RequestMapping(value = "login.do",method = RequestMethod.POST) @ResponseBody public ServerResponse<User> login(String username, String password, HttpSession session, HttpServletResponse httpServletResponse){ ServerResponse<User> serverResponse = iUserService.login(username,password); if (serverResponse.isSuccess()){ // session.setAttribute(Const.CURRENT_USER,serverResponse.getData()); RedisShardedPoolUtil.setEx(session.getId(), JsonUtil.obj2String(serverResponse.getData()),Const.RedisCacheExtime.REDIS_SESSION_EXTIME); } return serverResponse; }
那麼我們就寫一個Cookie的工具類
@Slf4j public class CookieUtil { private final static String COOKIE_DOMAIN = ".XXX.com"; private final static String COOKIE_NAME = "study_login_token"; public static String readLoginToken(HttpServletRequest request){ Cookie[] cks = request.getCookies(); if (cks != null){ for (Cookie ck : cks){ log.info("read cookieName:{},cookieValue:{}",ck.getName(),ck.getValue()); if (StringUtils.equals(ck.getName(),COOKIE_NAME)){ log.info("read cookieName:{},cookieValue:{}",ck.getName(),ck.getValue()); return ck.getValue(); } } } return null; } public static void writeLoginToken(HttpServletResponse response,String token){ Cookie ck = new Cookie(COOKIE_NAME,token); ck.setDomain(COOKIE_DOMAIN); ck.setPath("/");//代表設定在根目錄下 ck.setHttpOnly(true); //單位是秒。 //如果這個maxage不設定的話,cookie就不會寫入硬碟,而是寫在記憶體。只在當前頁面有效 ck.setMaxAge(60*60*24*365);//如果是-1,代表永久 log.info("write cookieName:{},cookieValue:{}",ck.getName(),ck.getValue()); response.addCookie(ck); } public static void delLoginToken(HttpServletRequest request,HttpServletResponse response){ Cookie[] cks = request.getCookies(); if (cks != null){ for (Cookie ck : cks){ if (StringUtils.equals(ck.getName(),COOKIE_NAME)){ ck.setDomain(COOKIE_DOMAIN); ck.setPath("/"); ck.setMaxAge(0);//設定成0,代表刪除此cookie log.info("del cookieName:{},cookieValue:{}",ck.getName(),ck.getValue()); response.addCookie(ck); return; } } } } }
通過這個類,我們登入的時候就從服務端給客戶端種上了一個cookie,只要我們是在.XXX.com這個一級域名下進行操作,就能拿到這個cookie
@RequestMapping(value = "login.do",method = RequestMethod.POST) @ResponseBody public ServerResponse<User> login(String username, String password, HttpSession session, HttpServletResponse httpServletResponse){ ServerResponse<User> serverResponse = iUserService.login(username,password); if (serverResponse.isSuccess()){ // session.setAttribute(Const.CURRENT_USER,serverResponse.getData());
//登入的時候我們就把cookie種上返回給客戶端,那麼我們校驗是否登入的時候就readLoginToken
//就是說我們現在不管你走哪個tomcat,cookie是一樣的 CookieUtil.writeLoginToken(httpServletResponse,session.getId()); RedisShardedPoolUtil.setEx(session.getId(), JsonUtil.obj2String(serverResponse.getData()),Const.RedisCacheExtime.REDIS_SESSION_EXTIME); } return serverResponse; }
校驗是否登入
@RequestMapping("cancel.do") @ResponseBody public ServerResponse cancel(HttpServletRequest request, Long orderNo){ //User user = (User) session.getAttribute(Const.CURRENT_USER); String loginToken = CookieUtil.readLoginToken(request); if(StringUtils.isEmpty(loginToken)){ return ServerResponse.createByErrorMessage("使用者未登入,無法獲取當前使用者的資訊"); } String userJsonStr = RedisShardedPoolUtil.get(loginToken); User user = JsonUtil.string2Obj(userJsonStr,User.class); if (user == null){ return ServerResponse.createByErrorCode(ResponseCode.NEED_LOGIN.getCode(),ResponseCode.NEED_LOGIN.getDesc()); } return iOrderService.cancle(user.getId(),orderNo); }
tomcat1和tomcat2還是會產生兩個不同的Cookie,但同時我們又自己增加了name為study_login_token的cookie,所以我們只需要讀取這個name,就可以判斷使用者登入狀態
但現在又有了一個問題,我們儲存在redis裡面的“session”,一次登入以後有效期是30分鐘,我們後面再執行其他操作,這個session的有效期是不會更新的,就是說一次登入只能玩30分鐘,所以我們就用過濾器來解決這個問題
5.過濾器Filter重置session有效期
首先寫好類
public class SessionExpireFilter implements Filter{ @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String loginToken = CookieUtil.readLoginToken(httpServletRequest); if (StringUtils.isNotEmpty(loginToken)){ //判斷logintoken是否為空或者"" //如果不為空,符合條件,繼續拿user資訊 String userJsonStr = RedisShardedPoolUtil.get(loginToken); User user = JsonUtil.string2Obj(userJsonStr,User.class); if (user != null){ //如果user不為空,則重置session的時間,即呼叫expire命令 RedisShardedPoolUtil.expire(loginToken, Const.RedisCacheExtime.REDIS_SESSION_EXTIME); } } filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { } }
然後進入web.xml配置上這個過濾器
<filter> <filter-name>sesssionExpireFilter</filter-name> <filter-class>com.study.controller.commom.SessionExpireFilter</filter-class> </filter> <filter-mapping> <filter-name>sesssionExpireFilter</filter-name> <url-pattern>*.do</url-pattern> </filter-mapping>
現在單點登入就到這裡了